Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Model View Intent

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
Tweet

More Decks by Yousuf Haque

Other Decks in Programming

Transcript

  1. Model View Intent Embracing Reactive architecture @YousufHaque

  2. What is Model View Intent?

  3. Presentation Layer Architecture MVP, MVVM, MV*

  4. Explicit Definition of State

  5. Unidirectional + Fully Reactive

  6. https://staltz.com/unidirectional-user-interface-architectures.html Andre Staltz

  7. None
  8. None
  9. None
  10. 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> )
  11. 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) }
  12. 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> )
  13. ViewState is derived from State

  14. None
  15. 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 } }
  16. Render function maps State to ViewState

  17. 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() ) }
  18. fun LoginState.isProgressSpinnerVisible(): Boolean { return when (this) { is Entering

    -> false is Error -> false is Submitting -> true } }
  19. fun LoginState.isPasswordFieldEnabled(): Boolean { return when (this) { is Entering

    -> true is Error -> true is Submitting -> false } }
  20. fun LoginState.isSubmitButtonEnabled(): Boolean { return when (this) { is Entering

    -> username.isNotBlank() && password.isNotBlank() is Error -> username.isNotBlank() && password.isNotBlank() is Submitting -> false } }
  21. fun LoginState.getSubmitButtonCopy(): String { return when (this) { is Entering

    -> "Submit" is Error -> "Submit" is Submitting -> "Submitting" } }
  22. 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() } } }
  23. 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() ) }
  24. Where does the state stream come from?

  25. BuildStateStream function assembles dependencies into a stream of state

  26. 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") }
  27. 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 } }
  28. 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 } }
  29. typealias LoginExampleStateReducer = (LoginState) -> LoginState

  30. 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 } }
  31. 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 } }
  32. 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) } }
  33. 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) } }
  34. 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) } }
  35. 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 } }
  36. 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 } }
  37. .scan is an aggregation operator

  38. Starting with an initial state, we apply each reducer and

    transition from state to state
  39. 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 } }
  40. 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 } }
  41. 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 } }
  42. Intents are user actions

  43. sealed class LoginIntent { data class UpdateCredentials( val username: String,

    val password: String ) : LoginIntent() data class SubmitLoginIntent( val username: String, val password: String ): LoginIntent() }
  44. Intents mutate state through reducer streams

  45. 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 ) } }
  46. 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 } }
  47. 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 } }
  48. 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 } }
  49. 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 } }
  50. 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 ) } }
  51. 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 ) } }
  52. None
  53. 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) }
  54. 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) }
  55. 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) }
  56. 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) }
  57. 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) }
  58. 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) }
  59. 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) }
  60. fun Router.buildGoToLoggedInScreenCompletable(userId: String): Completable { return { replaceTopController(RouterTransaction.with(LoggedInController(userId))) } .toCompletable()

    .subscribeOn(AndroidSchedulers.mainThread()) }
  61. fun Router.buildGoToLoggedInScreenCompletable(userId: String): Completable { return { replaceTopController(RouterTransaction.with(LoggedInController(userId))) } .toCompletable()

    .subscribeOn(AndroidSchedulers.mainThread()) }
  62. https://staltz.com/unidirectional-user-interface-architectures.html Andre Staltz

  63. 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
  64. Thank You Twitter: @YousufHaque Github: Yousuf-Haque