Share
Embed
Model View Intent
A presentation about Model View Intent, what it is, and what components make up Model View Intent with kotlin and RxJava on Android
Yousuf Haque
October 10, 2018
Transcript
64 slides
-
data class LoginViewState( val username: String, val password: String, val
isUsernameFieldEnabled: Boolean, val isPasswordFieldEnabled: Boolean, val isProgressSpinnerVisible: Boolean, val isSubmitButtonEnabled: Boolean, val submitButtonCopy: String, val errorMessageOption: Option<String>, val submitButtonIntentOption: Option<LoginIntent>, val currentTimeStringOption: Option<String> ) -
private fun View.update(viewState: LoginViewState) { progress_pb.isVisible = viewState.isProgressSpinnerVisible submit_btn.isEnabled =
viewState.isSubmitButtonEnabled submit_btn.text = viewState.submitButtonCopy username_et.isEnabled = viewState.isUsernameFieldEnabled password_et.isEnabled = viewState.isPasswordFieldEnabled error_message_tv.setTextOrHide(viewState.errorMessageOption) current_time_tv.setTextOrHide(viewState.currentTimeStringOption) submit_btn.setOnClickListener { viewState.submitButtonIntentOption.forSome { intentRelay.accept(it) } } } // in onAttach() viewStateStream.subscribe { view.update(it) } -
data class LoginViewState( val username: String, val password: String, val
isUsernameFieldEnabled: Boolean, val isPasswordFieldEnabled: Boolean, val isProgressSpinnerVisible: Boolean, val isSubmitButtonEnabled: Boolean, val submitButtonCopy: String, val errorMessageOption: Option<String>, val submitButtonIntentOption: Option<LoginIntent>, val currentTimeStringOption: Option<String> ) -
sealed class LoginState { data class Entering( val username: String,
val password: String, val currentTime: Option<Date> ) : LoginState() data class Submitting( val username: String, val password: String, val currentTime: Option<Date> ) : LoginState() data class Error( val username: String, val password: String, val error: LoginError, val currentTime: Option<Date> ) : LoginState() enum class LoginError { IncorrectCredentials, NetworkError } } -
fun LoginState.render(): LoginViewState { return LoginViewState( username = getUserName(), isPasswordFieldEnabled
= isPasswordFieldEnabled(), isUsernameFieldEnabled = isUsernameFieldEnabled(), password = getPassword(), submitButtonCopy = getSubmitButtonCopy(), isSubmitButtonEnabled = isSubmitButtonEnabled(), errorMessageOption = getErrorMessageOption(), isProgressSpinnerVisible = isProgressSpinnerVisible(), submitButtonIntentOption = getSubmitButtonIntent(), currentTimeStringOption = getCurrentTimeStringOption() ) } -
fun LoginState.isProgressSpinnerVisible(): Boolean { return when (this) { is Entering
-> false is Error -> false is Submitting -> true } } -
fun LoginState.isPasswordFieldEnabled(): Boolean { return when (this) { is Entering
-> true is Error -> true is Submitting -> false } } -
fun LoginState.isSubmitButtonEnabled(): Boolean { return when (this) { is Entering
-> username.isNotBlank() && password.isNotBlank() is Error -> username.isNotBlank() && password.isNotBlank() is Submitting -> false } } -
fun LoginState.getSubmitButtonCopy(): String { return when (this) { is Entering
-> "Submit" is Error -> "Submit" is Submitting -> "Submitting" } } -
fun LoginState.getErrorMessageOption(): Option<String> { return when (this) { is Entering
-> none() is Submitting -> none() is Error -> when (error) { IncorrectCredentials -> "Incorrect credentials".some() NetworkError -> "Network error".some() } } } -
fun LoginState.render(): LoginViewState { return LoginViewState( username = getUserName(), isPasswordFieldEnabled
= isPasswordFieldEnabled(), isUsernameFieldEnabled = isUsernameFieldEnabled(), password = getPassword(), submitButtonCopy = getSubmitButtonCopy(), isSubmitButtonEnabled = isSubmitButtonEnabled(), errorMessageOption = getErrorMessageOption(), isProgressSpinnerVisible = isProgressSpinnerVisible(), submitButtonIntentOption = getSubmitButtonIntent(), currentTimeStringOption = getCurrentTimeStringOption() ) } -
fun buildLoginStateStream( intentStream: Observable<LoginIntent>, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, currentTimeStream: Observable<Date>,
buildGoToLoggedInCompletable: (userId: String) -> Completable ): Observable<LoginState> { TODO("Build and return a state stream") } -
fun buildLoginStateStream( intentStream: Observable<LoginIntent>, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, currentTimeStream: Observable<Date>,
buildGoToLoggedInCompletable: (userId: String) -> Completable ): Observable<LoginState> { val initialState = Entering( username = "", password = "", currentTime = none() ) val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) return updateTimeReducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
fun buildLoginStateStream( intentStream: Observable<LoginIntent>, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, currentTimeStream: Observable<Date>,
buildGoToLoggedInCompletable: (userId: String) -> Completable ): Observable<LoginState> { val initialState = Entering( username = "", password = "", currentTime = none() ) val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) return updateTimeReducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
fun buildLoginStateStream( intentStream: Observable<LoginIntent>, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, currentTimeStream: Observable<Date>,
buildGoToLoggedInCompletable: (userId: String) -> Completable ): Observable<LoginState> { val initialState = Entering( username = "", password = "", currentTime = none() ) val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) return updateTimeReducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
fun buildLoginStateStream( intentStream: Observable<LoginIntent>, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, currentTimeStream: Observable<Date>,
buildGoToLoggedInCompletable: (userId: String) -> Completable ): Observable<LoginState> { val initialState = Entering( username = "", password = "", currentTime = none() ) val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) return updateTimeReducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
fun getCurrentTimeReducerStream( currentTimeStream: Observable<Date> ): Observable<LoginExampleStateReducer> { fun buildOnCurrentTimeUpdateReducer(date: Date):
LoginExampleStateReducer { return { oldState: LoginState -> when (oldState) { is LoginState.Entering -> oldState.copy(currentTime = date.some()) is LoginState.Submitting -> oldState.copy(currentTime = date.some()) is LoginState.Error -> oldState.copy(currentTime = date.some()) } } } return currentTimeStream.map { buildOnCurrentTimeUpdateReducer(it) } } -
fun getCurrentTimeReducerStream( currentTimeStream: Observable<Date> ): Observable<LoginExampleStateReducer> { fun buildOnCurrentTimeUpdateReducer(date: Date):
LoginExampleStateReducer { return { oldState: LoginState -> when (oldState) { is LoginState.Entering -> oldState.copy(currentTime = date.some()) is LoginState.Submitting -> oldState.copy(currentTime = date.some()) is LoginState.Error -> oldState.copy(currentTime = date.some()) } } } return currentTimeStream.map { buildOnCurrentTimeUpdateReducer(it) } } -
fun getCurrentTimeReducerStream( currentTimeStream: Observable<Date> ): Observable<LoginExampleStateReducer> { fun buildOnCurrentTimeUpdateReducer(date: Date):
LoginExampleStateReducer { return { oldState: LoginState -> when (oldState) { is LoginState.Entering -> oldState.copy(currentTime = date.some()) is LoginState.Submitting -> oldState.copy(currentTime = date.some()) is LoginState.Error -> oldState.copy(currentTime = date.some()) } } } return currentTimeStream.map { buildOnCurrentTimeUpdateReducer(it) } } -
fun buildLoginStateStream( intentStream: Observable<LoginIntent>, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, currentTimeStream: Observable<Date>,
buildGoToLoggedInCompletable: (userId: String) -> Completable ): Observable<LoginState> { val initialState = Entering( username = "", password = "", currentTime = none() ) val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) return updateTimeReducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
fun buildLoginStateStream( intentStream: Observable<LoginIntent>, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, currentTimeStream: Observable<Date>,
buildGoToLoggedInCompletable: (userId: String) -> Completable ): Observable<LoginState> { val initialState = Entering( username = "", password = "", currentTime = none() ) val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) return updateTimeReducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
Starting with an initial state, we apply each reducer and
transition from state to state -
fun buildLoginStateStream( intentStream: Observable<LoginIntent>, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, currentTimeStream: Observable<Date>,
buildGoToLoggedInCompletable: (userId: String) -> Completable ): Observable<LoginState> { val initialState = Entering( username = "", password = "", currentTime = none() ) val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) return updateTimeReducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
fun buildLoginStateStream( intentStream: Observable<LoginIntent>, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, currentTimeStream: Observable<Date>,
buildGoToLoggedInCompletable: (userId: String) -> Completable ): Observable<LoginState> { val initialState = Entering( username = "", password = "", currentTime = none() ) val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) return updateTimeReducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
fun buildLoginStateStream( intentStream: Observable<LoginIntent>, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, currentTimeStream: Observable<Date>,
buildGoToLoggedInCompletable: (userId: String) -> Completable ): Observable<LoginState> { val initialState = Entering( username = "", password = "", currentTime = none() ) val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) return updateTimeReducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
sealed class LoginIntent { data class UpdateCredentials( val username: String,
val password: String ) : LoginIntent() data class SubmitLoginIntent( val username: String, val password: String ): LoginIntent() } -
fun getIntentReducerStream( intent: LoginIntent, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, buildGoToLoggedInCompletable: (userId:
String) -> Completable ): Observable<LoginExampleStateReducer> { return when (intent) { is UpdateCredentials -> getUpdateCredentialsReducerStream(intent) is SubmitLoginIntent -> buildSubmitLoginRequestReducerStream( intent, loginRequestBuilder, buildGoToLoggedInCompletable ) } } -
fun buildLoginStateStream(...): Observable<LoginState> { val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) val
intentReducerStream = intentStream.flatMap { getIntentReducerStream( it, loginRequestBuilder, buildGoToLoggedInCompletable ) } val reducerStream: Observable<LoginExampleStateReducer> = Observable.merge(intentReducerStream, updateTimeReducerStream) return reducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
fun buildLoginStateStream(...): Observable<LoginState> { val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) val
intentReducerStream = intentStream.flatMap { getIntentReducerStream( it, loginRequestBuilder, buildGoToLoggedInCompletable ) } val reducerStream: Observable<LoginExampleStateReducer> = Observable.merge(intentReducerStream, updateTimeReducerStream) return reducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
fun buildLoginStateStream(...): Observable<LoginState> { val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) val
intentReducerStream = intentStream.flatMap { getIntentReducerStream( it, loginRequestBuilder, buildGoToLoggedInCompletable ) } val reducerStream: Observable<LoginExampleStateReducer> = Observable.merge(intentReducerStream, updateTimeReducerStream) return reducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
fun buildLoginStateStream(...): Observable<LoginState> { val updateTimeReducerStream: Observable<LoginExampleStateReducer> = getCurrentTimeReducerStream(currentTimeStream) val
intentReducerStream = intentStream.flatMap { getIntentReducerStream( it, loginRequestBuilder, buildGoToLoggedInCompletable ) } val reducerStream: Observable<LoginExampleStateReducer> = Observable.merge(intentReducerStream, updateTimeReducerStream) return reducerStream .scan(initialState) { oldState: LoginState, reducer: LoginExampleStateReducer -> val newState = reducer(oldState) newState } } -
fun getIntentReducerStream( intent: LoginIntent, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, buildGoToLoggedInCompletable: (userId:
String) -> Completable ): Observable<LoginExampleStateReducer> { return when (intent) { is UpdateCredentials -> getUpdateCredentialsReducerStream(intent) is SubmitLoginIntent -> buildSubmitLoginRequestReducerStream( intent, loginRequestBuilder, buildGoToLoggedInCompletable ) } } -
fun getIntentReducerStream( intent: LoginIntent, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, buildGoToLoggedInCompletable: (userId:
String) -> Completable ): Observable<LoginExampleStateReducer> { return when (intent) { is UpdateCredentials -> getUpdateCredentialsReducerStream(intent) is SubmitLoginIntent -> buildSubmitLoginRequestReducerStream( intent, loginRequestBuilder, buildGoToLoggedInCompletable ) } } -
fun buildSubmitLoginRequestReducerStream( intent: SubmitLoginIntent, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, buildGoToLoggedInCompletable: (userId:
String) -> Completable ): Observable<LoginExampleStateReducer> { // Omitted Code return loginRequestBuilder( LoginRequest( username = intent.username, password = intent.password ) ) .flatMapObservable { loginResult -> loginResult.fold( ifFailure = { getOnErrorStateReducer(it).just() }, ifSuccess = { buildGoToLoggedInCompletable( it.userId ).toObservable<LoginExampleStateReducer>() } ) } .startWith(onSubmitStateReducer) } -
fun buildSubmitLoginRequestReducerStream( intent: SubmitLoginIntent, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, buildGoToLoggedInCompletable: (userId:
String) -> Completable ): Observable<LoginExampleStateReducer> { // Omitted Code return loginRequestBuilder( LoginRequest( username = intent.username, password = intent.password ) ) .flatMapObservable { loginResult -> loginResult.fold( ifFailure = { getOnErrorStateReducer(it).just() }, ifSuccess = { buildGoToLoggedInCompletable( it.userId ).toObservable<LoginExampleStateReducer>() } ) } .startWith(onSubmitStateReducer) } -
fun buildSubmitLoginRequestReducerStream( intent: SubmitLoginIntent, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, buildGoToLoggedInCompletable: (userId:
String) -> Completable ): Observable<LoginExampleStateReducer> { // Omitted Code return loginRequestBuilder( LoginRequest( username = intent.username, password = intent.password ) ) .flatMapObservable { loginResult -> loginResult.fold( ifFailure = { getOnErrorStateReducer(it).just() }, ifSuccess = { buildGoToLoggedInCompletable( it.userId ).toObservable<LoginExampleStateReducer>() } ) } .startWith(onSubmitStateReducer) } -
fun buildSubmitLoginRequestReducerStream( intent: SubmitLoginIntent, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, buildGoToLoggedInCompletable: (userId:
String) -> Completable ): Observable<LoginExampleStateReducer> { // Omitted Code return loginRequestBuilder( LoginRequest( username = intent.username, password = intent.password ) ) .flatMapObservable { loginResult -> loginResult.fold( ifFailure = { getOnErrorStateReducer(it).just() }, ifSuccess = { buildGoToLoggedInCompletable( it.userId ).toObservable<LoginExampleStateReducer>() } ) } .startWith(onSubmitStateReducer) } -
fun buildSubmitLoginRequestReducerStream( intent: SubmitLoginIntent, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, buildGoToLoggedInCompletable: (userId:
String) -> Completable ): Observable<LoginExampleStateReducer> { // Omitted Code return loginRequestBuilder( LoginRequest( username = intent.username, password = intent.password ) ) .flatMapObservable { loginResult -> loginResult.fold( ifFailure = { getOnErrorStateReducer(it).just() }, ifSuccess = { buildGoToLoggedInCompletable( it.userId ).toObservable<LoginExampleStateReducer>() } ) } .startWith(onSubmitStateReducer) } -
fun buildSubmitLoginRequestReducerStream( intent: SubmitLoginIntent, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, buildGoToLoggedInCompletable: (userId:
String) -> Completable ): Observable<LoginExampleStateReducer> { // Omitted Code return loginRequestBuilder( LoginRequest( username = intent.username, password = intent.password ) ) .flatMapObservable { loginResult -> loginResult.fold( ifFailure = { getOnErrorStateReducer(it).just() }, ifSuccess = { buildGoToLoggedInCompletable( it.userId ).toObservable<LoginExampleStateReducer>() } ) } .startWith(onSubmitStateReducer) } -
fun buildSubmitLoginRequestReducerStream( intent: SubmitLoginIntent, loginRequestBuilder: (LoginRequest) -> Single<Try<UserInfo>>, buildGoToLoggedInCompletable: (userId:
String) -> Completable ): Observable<LoginExampleStateReducer> { // Omitted Code return loginRequestBuilder( LoginRequest( username = intent.username, password = intent.password ) ) .flatMapObservable { loginResult -> loginResult.fold( ifFailure = { getOnErrorStateReducer(it).just() }, ifSuccess = { buildGoToLoggedInCompletable( it.userId ).toObservable<LoginExampleStateReducer>() } ) } .startWith(onSubmitStateReducer) } -
fun Router.buildGoToLoggedInScreenCompletable(userId: String): Completable { return { replaceTopController(RouterTransaction.with(LoggedInController(userId))) } .toCompletable()
.subscribeOn(AndroidSchedulers.mainThread()) } -
fun Router.buildGoToLoggedInScreenCompletable(userId: String): Completable { return { replaceTopController(RouterTransaction.with(LoggedInController(userId))) } .toCompletable()
.subscribeOn(AndroidSchedulers.mainThread()) } -
Resources 1. Managing State with RxJava by Jake Wharton a.
https://www.youtube.com/watch?v=0IKHxjkgop4 2. Borrowing the Best of the Web to Make Native Better by Christina Lee a. https://www.youtube.com/watch?v=GOVMkQp3LZ4 3. Unidirectional data flow architectures by Andre Staltz a. https://www.youtube.com/watch?v=1c6XiQsnh_U b. https://staltz.com/unidirectional-user-interface-architectures.html 4. Reactive Apps with Model View Intent by Hannes Dorfmann a. http://hannesdorfmann.com/android/mosby3-mvi-1 5. My Take on Model View Intent by Zak Taccardi a. https://hackernoon.com/model-view-intent-mvi-part-1-state-renderer-187e270db15c 6. Login MVI by Yousuf Haque a. https://github.com/yousuf-haque/LoginMvi