In this tutorial, we will be implementing a simple login flow with the following requirements
- User should be able to enter his email and password.
- Sign In button
- Disabled (grayish) when the email/password fields are empty. Colored using the main theme otherwise.
- Button text is "Sign In" but changes to "Signing In" once the user clicks on it.
- It could be that the user has already entered his email somewhere in the app before kickstarting the login process. The input of the login flow will be the email that we would like to start with.
Getting Started
You can download the starter project here. The starter project includes:
- The dependencies needed, FeedbackTree, RxJava, and, RxBinding.
- The login XML layout.
- The
AuthenticationManager
that contains the authentication logic.
The Login Flow
Create a new package called login under flows package and add to it a new Kotlin file called LoginFlow.kt, then. add the code below to it:
import com.feedbacktree.flow.core.*
import io.reactivex.Observable
val LoginFlow = Flow<String, State, Event, Unit, LoginScreen>( // 1
id = "LoginFlow",
initialState = { lastEmailUsed -> State(email = lastEmailUsed) }, // 2
stepper = { state, event ->
when (event) {
is Event.EnteredEmail -> state.copy(email = event.email).advance()
is Event.EnteredPassword -> state.copy(password = event.password).advance()
Event.ClickedLogin -> state.copy(isLoggingIn = true).advance()
is Event.ReceivedLogInResponse -> {
if (event.success) {
endFlow() // 3
} else {
state.copy(isLoggingIn = false).advance() // 4
}
}
}
},
feedbacks = listOf(),
render = { state, context ->
LoginScreen(state, context.sink)
}
)
data class State(
val email: String = "",
val password: String = "",
val isLoggingIn: Boolean = false
)
sealed class Event {
data class EnteredEmail(val email: String) : Event()
data class EnteredPassword(val password: String) : Event()
object ClickedLogin : Event()
data class ReceivedLogInResponse(val success: Boolean) : Event()
}
data class LoginScreen(
private val state: State,
val sink: (Event) -> Unit
) {
val emailText: String
get() = state.email
val passwordText: String
get() = state.password
val loginButtonTitle: String // 5
get() = if (state.isLoggingIn) "Signing In" else "Sign In"
val isLoginButtonEnabled: Boolean
get() = state.email.isNotEmpty() && state.password.isNotEmpty()
}
Here's the breakdown of the code above:
- Here is the signature of the FeedbackTree Flow
Flow<Input, State, Event, Output, Screen>
:- The
Input
of theLoginFlow
is a String that represents the last email used. - The
State
of the Flow - The
Event
that is used to update the state of theFlow
- The
Output
is of typeUnit
which means theFlow
can complete. - The
Screen
produced is aLoginScreen
.
- The
- We build the initial state using the String input.
- When the login succeeds, we will terminate the flow.
- When the login fails, we set back
isLoggingIn
in to false. Ideally, we should tell the user that something went wrong. We will do this in the next sections - When we are logging in we will change the sign in button title to "Signing In" to tell the user that the operation is running.
Where is the Sign in logic?
You use Feedbacks
to perform side effects, like calling an API, reading from a bluetooth device, running database operations or even updating the UI...
We have seen in the Counter tutorial a way to build a UI binding Feedback
using the bind
operator. Here we will use the react
operator to perform the authentication logic.
private data class LoginQuery(
val email: String,
val password: String
)
private fun loginFeedback(): Feedback<State, Event> = react<State, LoginQuery, Event>(
query = { state -> // 1
if (state.isLoggingIn) {
LoginQuery(email = state.email, password = state.password)
} else {
null
}
},
effects = { queryResult -> // 2
val authenticationSuccess: Observable<Boolean> = AuthenticationManager.login(
email = queryResult.email,
password = queryResult.password
) // 3
authenticationSuccess.map { loginSucceeded ->
Event.ReceivedLogInResponse(loginSucceeded) // 4
}
}
)
A react
feedback loop is a declarative way to perform side effects. Here's a detailed breakdown of what how the Feedback above will run:
- For every state the flow gets into, the
query
will be evaluated. As soon the evaluated value is different thannull
, theeffects
will kickstart. - The
effects
is a block of code that takes thequeryResult
, evaluated in thequery
block, perform the side effect like executing the authentication logic, and emit back anEvent
when done. The signature of the effets is(Query) -> Observable<Event>
authenticationSuccess
is anObservable<Boolean>
that will perform the authentication logic and return true when done.- map the
Observable<Boolean>
into anObservable<Event>
that is returned to theeffects
block. The events being produced will be sent back to theFlow
to update the state.
Now it's time to add the loginFeedback()
to the list of feedbacks
in the Flow
val LoginFlow = Flow<String, State, Event, Unit, LoginScreen>(
...
feedbacks = listOf(loginFeedback()),
...
)
The Login UI
The LoginFlow
renders a LoginScreen
. The LoginScreen
is a UI representation of the state. What we need to complete the puzzle is some code that will create the corresponding Login View, update its ui elements when the state updates and consume clicks and events generated by the user and pass them back to the Flow.
In the login package, add a new file called LoginLayoutBinder.kt and the code below to it:
import android.widget.Button
import com.feedbacktree.flow.ui.views.LayoutBinder
import com.feedbacktree.tutorials.R
import com.feedbacktree.utils.FTEditText
import com.jakewharton.rxbinding3.view.clicks
import com.jakewharton.rxbinding3.widget.textChanges
val LoginLayoutBinder = LayoutBinder.create(
layoutId = R.layout.login,
sink = LoginScreen::sink,
) { view ->
val emailEditText: FTEditText = view.findViewById(R.id.inputEmail) // 3
val passwordEditText: FTEditText = view.findViewById(R.id.inputPassword)
val btnLogin: Button = view.findViewById(R.id.btnLogin)
bind { screen ->
subscriptions = listOf(
screen.map { it.emailText }.subscribe { emailEditText.text = it }, // 2
screen.map { it.passwordText }.subscribe { passwordEditText.text = it },
screen.map { it.loginButtonTitle }.subscribe { btnLogin.text = it },
screen.map { it.isLoginButtonEnabled }.subscribe { btnLogin.isEnabled = it }
)
events = listOf(
emailEditText.textChanges().map { Event.EnteredEmail(it.toString()) }, // 1
passwordEditText.textChanges().map { Event.EnteredPassword(it.toString()) },
btnLogin.clicks().map { Event.ClickedLogin } // 4
)
}
}
emailEditText.textChanges().map { Event.EnteredEmail(it.toString()) }
uses thetextChanges()
from RxBinding to capture theemailEditText
updates and map it to anEvent
.- We subscribe the
emailText
inscreen.map { it.emailText }.subscribe { emailEditText.text = it }
for two puposes:- The
Flow
can start with the last email that was used to login. So theemailEditText
can initially be non-empty. - It is recommended to always rely on the state as the single source of truth. In other terms, store the values of the textfields in the state and use what's in the state to drive the UI. This technique comes handy when the device configuration changes and a new layout is be inflated which would allow FeedbackTree to automatically refill the new layout from what is stored in the state.
- The
- If you haven't noticed yet, we are doing a two-way binding for the
emailEditText.text
property, which means that we set theemailEditText.text
in subscriptions and listen to the text changes in the events.
The problem of theEditText
is that watchers are notified when thetext
is updated programmatically which will cause infinte update cycles/loops when two-way binding is applied. TheFTEditText
breaks the infinite update cycles. TheFTEditText
mainly removes theTextWatchers
and updates thetext
property before adding back the watchers that were removed. You can check here the full implementation in case you want to apply the same logic for other controls like switches. - We are using
clicks()
from RxBinding to capture the the View clicks.
Starting the Flow
Combining all the pieces together:
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.feedbacktree.flow.core.startFlow
import com.feedbacktree.flow.ui.views.core.ViewRegistry
import com.feedbacktree.tutorials.flows.login.LoginFlow
import com.feedbacktree.tutorials.flows.login.LoginLayoutBinder
import io.reactivex.disposables.Disposable
class MainActivity : AppCompatActivity() {
var disposable: Disposable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewRegistry = ViewRegistry(LoginLayoutBinder) // 1
disposable = startFlow(
input = "developer@feedbacktree.com", // 2
flow = LoginFlow,
viewRegistry = viewRegistry,
onOutput = {
// Do something with the output if you want
})
}
override fun onPause() {
super.onPause()
// 3
if (isFinishing) {
disposable?.dispose()
disposable = null
}
}
}
- A
ViewRegistry
is a lookup table that FeedbackTree uses to create the corresponding layout when someScreen
is produced by theFlow
. We are registering theLoginLayoutBinder
companion object
from the previous section to theviewRegistry
variable. - Start the
LoginFlow
with the last email used. - Terminate the flow when the activity finishes.
Where to Go From Here?
In this tutorial you learned how to create a non-UI feedback loop that will perform the authentication. In the next tutorial, we will see how to start children flows.
The full code can be downloaded from here