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

Model-View-Intent for Android

Model-View-Intent for Android

Video: https://www.youtube.com/watch?v=64rQ9GKphTg

Development on Android inevitably involves asynchronous sources, which includes the network, the framework or even the user. This easily leads to a complex architecture and a state that is difficult to manage. Are you comfortable with your users rotating the screen while waiting for a network response? What will happen when multiple requests hit the network at the same time?

The Model-View-Intent architecture, borrowed from the web, is all about streams: listening to asynchronous sources, processing the information, and displaying the result. In this architecture, everything is a stream. However, Android is a multi-threaded platform and this introduces additional complexity that we have to take into account when applying the MVI pattern. Unidirectional data flow allows one to focus on a unique source of truth and by embracing immutability, there is no need to worry about data being mutated by other parts of the code. By abstracting away the common pitfalls of state management in Android, this architecture allows the developer to focus on the logic of the app, making it easy to write and easy to maintain.

In this talk, we’ll look at how, with Kotlin and RxJava, this reactive architecture can help the developer write a complex UI with concrete examples, including parallel network requests, config changes, SnackBars and more.

Benoît Quenaudon

September 26, 2017
Tweet

More Decks by Benoît Quenaudon

Other Decks in Programming

Transcript

  1. Model-View-Intent
    for Android
    Benoît Quenaudon @oldergod

    View full-size slide

  2. “What if the user was a function?” by Andre Staltz

    View full-size slide

  3. “What if the user was a function?” by Andre Staltz

    View full-size slide

  4. intent(user())

    View full-size slide

  5. model(intent(user()))

    View full-size slide

  6. view(model(intent(user())))

    View full-size slide

  7. user(view(model(intent(user()))))

    View full-size slide

  8. view(model(intent()))

    View full-size slide

  9. sealed class TasksIntent {
    }@

    View full-size slide

  10. sealed class TasksIntent {
    object InitialIntent : TasksIntent()
    }@

    View full-size slide

  11. sealed class TasksIntent {
    object InitialIntent : TasksIntent()
    object RefreshIntent : TasksIntent()
    }@

    View full-size slide

  12. sealed class TasksIntent {
    object InitialIntent : TasksIntent()
    object RefreshIntent : TasksIntent()
    data class ActivateTaskIntent(val task: Task) : TasksIntent()
    data class CompleteTaskIntent(val task: Task) : TasksIntent()
    }@

    View full-size slide

  13. sealed class TasksIntent {
    object InitialIntent : TasksIntent()
    object RefreshIntent : TasksIntent()
    data class ActivateTaskIntent(val task: Task) : TasksIntent()
    data class CompleteTaskIntent(val task: Task) : TasksIntent()
    object ClearCompletedTasksIntent : TasksIntent()
    }@

    View full-size slide

  14. sealed class TasksIntent {
    object InitialIntent : TasksIntent()
    object RefreshIntent : TasksIntent()
    data class ActivateTaskIntent(val task: Task) : TasksIntent()
    data class CompleteTaskIntent(val task: Task) : TasksIntent()
    object ClearCompletedTasksIntent : TasksIntent()
    data class ChangeFilterIntent(val filterType: TasksFilterType) : TasksIntent()
    }@

    View full-size slide

  15. sealed class TasksIntent {
    object InitialIntent : TasksIntent()
    object RefreshIntent : TasksIntent()
    data class ActivateTaskIntent(val task: Task) : TasksIntent()
    data class CompleteTaskIntent(val task: Task) : TasksIntent()
    object ClearCompletedTasksIntent : TasksIntent()
    data class ChangeFilterIntent(val filterType: TasksFilterType) : TasksIntent()
    }@

    View full-size slide

  16. class TasksFragment
    fun intents(): Observable {
    }@
    }@

    View full-size slide

  17. class TasksFragment
    fun intents(): Observable {
    return initialIntent()
    }@a
    private fun initialIntent(): Observable {
    return Observable.just(InitialIntent)
    }
    }@

    View full-size slide

  18. class TasksFragment
    fun_intents():_Observable {
    return Observable.merge(initialIntent(),
    refreshIntent())
    }@a
    private fun refreshIntent(): Observable {
    return RxSwipeRefreshLayout.refreshes(swipeRefreshLayout)
    .map { RefreshIntent }
    }@
    }@

    View full-size slide

  19. class TasksFragment
    fun_intents():_Observable {
    return Observable.merge(initialIntent(),
    refreshIntent(),
    completeTaskIntent(),
    activateTaskIntent(),
    clearCompletedTaskIntent(),
    changeFilterIntent())
    }@
    }@

    View full-size slide

  20. USER
    INTENTS
    Intent

    View full-size slide

  21. intents // Observable

    View full-size slide

  22. intents // Observable
    .map { intent -> actionFromIntent(intent) } // Observable

    View full-size slide

  23. fun actionFromIntent(intent: TasksIntent): TasksAction =
    when (intent) {
    is InitialIntent ->
    is RefreshIntent ->
    is ActivateTaskIntent ->
    is CompleteTaskIntent ->
    is ClearCompletedTasksIntent ->
    is ChangeFilterIntent ->
    }@

    View full-size slide

  24. fun actionFromIntent(intent: TasksIntent): TasksAction =
    when (intent) {
    is InitialIntent ->
    is RefreshIntent ->
    is ActivateTaskIntent ->
    is CompleteTaskIntent ->
    is ClearCompletedTasksIntent ->
    is ChangeFilterIntent ->
    }@

    View full-size slide

  25. fun actionFromIntent(intent: TasksIntent): TasksAction =
    when (intent) {
    is InitialIntent ->
    is RefreshIntent ->
    is ActivateTaskIntent ->
    is CompleteTaskIntent ->
    is ClearCompletedTasksIntent ->
    is ChangeFilterIntent ->
    }@

    View full-size slide

  26. fun actionFromIntent(intent: TasksIntent): TasksAction =
    when (intent) {
    is InitialIntent -> LoadAndFilterTasksAction(TasksFilterType.ALL_TASKS)
    is RefreshIntent ->
    is ActivateTaskIntent ->
    is CompleteTaskIntent ->
    is ClearCompletedTasksIntent ->
    is ChangeFilterIntent ->
    }@

    View full-size slide

  27. fun actionFromIntent(intent: TasksIntent): TasksAction =
    when (intent) {
    is InitialIntent -> LoadAndFilterTasksAction(TasksFilterType.ALL_TASKS)
    is RefreshIntent -> LoadTasksAction
    is ActivateTaskIntent ->
    is CompleteTaskIntent ->
    is ClearCompletedTasksIntent ->
    is ChangeFilterIntent ->
    }@

    View full-size slide

  28. fun actionFromIntent(intent: TasksIntent): TasksAction =
    when (intent) {
    is InitialIntent -> LoadAndFilterTasksAction(TasksFilterType.ALL_TASKS)
    is RefreshIntent -> LoadTasksAction
    is ActivateTaskIntent -> ActivateTaskAction(intent.task)
    is CompleteTaskIntent ->
    is ClearCompletedTasksIntent ->
    is ChangeFilterIntent ->
    }@

    View full-size slide

  29. fun actionFromIntent(intent: TasksIntent): TasksAction =
    when (intent) {
    is InitialIntent -> LoadAndFilterTasksAction(TasksFilterType.ALL_TASKS)
    is RefreshIntent -> LoadTasksAction
    is ActivateTaskIntent -> ActivateTaskAction(intent.task)
    is CompleteTaskIntent -> CompleteTaskAction(intent.task)
    is ClearCompletedTasksIntent ->
    is ChangeFilterIntent ->
    }@

    View full-size slide

  30. fun actionFromIntent(intent: TasksIntent): TasksAction =
    when (intent) {
    is InitialIntent -> LoadAndFilterTasksAction(TasksFilterType.ALL_TASKS)
    is RefreshIntent -> LoadTasksAction
    is ActivateTaskIntent -> ActivateTaskAction(intent.task)
    is CompleteTaskIntent -> CompleteTaskAction(intent.task)
    is ClearCompletedTasksIntent -> ClearCompletedTasksAction
    is ChangeFilterIntent ->
    }@

    View full-size slide

  31. fun actionFromIntent(intent: TasksIntent): TasksAction =
    when (intent) {
    is InitialIntent -> LoadAndFilterTasksAction(TasksFilterType.ALL_TASKS)
    is RefreshIntent -> LoadTasksAction
    is ActivateTaskIntent -> ActivateTaskAction(intent.task)
    is CompleteTaskIntent -> CompleteTaskAction(intent.task)
    is ClearCompletedTasksIntent -> ClearCompletedTasksAction
    is ChangeFilterIntent -> LoadAndFilterTasksAction(intent.filterType)
    }@

    View full-size slide

  32. fun actionFromIntent(intent: TasksIntent): TasksAction =
    when (intent) {
    is InitialIntent -> LoadAndFilterTasksAction(TasksFilterType.ALL_TASKS)
    is RefreshIntent -> LoadTasksAction
    is ActivateTaskIntent -> ActivateTaskAction(intent.task)
    is CompleteTaskIntent -> CompleteTaskAction(intent.task)
    is ClearCompletedTasksIntent -> ClearCompletedTasksAction
    is ChangeFilterIntent -> LoadAndFilterTasksAction(intent.filterType)
    }@

    View full-size slide

  33. sealed class TasksAction {
    data class LoadAndFilterTasksAction(val filterType: TasksFilterType) : TasksAction()
    object LoadTasksAction : TasksAction()
    data class ActivateTaskAction(val task: Task) : TasksAction()
    data class CompleteTaskAction(val task: Task) : TasksAction()
    object ClearCompletedTasksAction : TasksAction()
    }@

    View full-size slide

  34. USER
    INTENTS
    Intent
    Action
    INTENT
    INTERPRETOR

    View full-size slide

  35. Action
    Load and Filter Tasks Logic
    Clear Completed Task Logic
    Complete Task Logic
    Activate Task Logic
    Load Tasks Logic
    Result

    View full-size slide

  36. var actionProcessor: ObservableTransformer =
    ObservableTransformer { actions: Observable ->
    }c

    View full-size slide

  37. var actionProcessor: ObservableTransformer =
    ObservableTransformer { actions: Observable ->
    }c

    View full-size slide

  38. var actionProcessor: ObservableTransformer =
    ObservableTransformer { actions: Observable ->
    actions.publish { shared ->
    Observable.merge()a
    }b
    }c

    View full-size slide

  39. var actionProcessor: ObservableTransformer =
    ObservableTransformer { actions: Observable ->
    actions.publish { shared ->
    Observable.merge(
    shared.ofType(LoadTasksAction::class.java).compose(loadTasksProcessor)
    )a
    }b
    }c

    View full-size slide

  40. var actionProcessor: ObservableTransformer =
    ObservableTransformer { actions: Observable ->
    actions.publish { shared ->
    Observable.merge(
    shared.ofType(LoadTasksAction::class.java).compose(loadTasksProcessor),
    shared.ofType(LoadAndFilterTasksAction::class.java).compose(loadAndFilterTasksProcessor)
    )a
    }b
    }c

    View full-size slide

  41. var actionProcessor: ObservableTransformer =
    ObservableTransformer { actions: Observable ->
    actions.publish { shared ->
    Observable.merge(
    shared.ofType(LoadTasksAction::class.java).compose(loadTasksProcessor),
    shared.ofType(LoadAndFilterTasksAction::class.java).compose(loadAndFilterTasksProcessor),
    shared.ofType(ActivateTaskAction::class.java).compose(activateTaskProcessor)
    )a
    }b
    }c

    View full-size slide

  42. var actionProcessor: ObservableTransformer =
    ObservableTransformer { actions: Observable ->
    actions.publish { shared ->
    Observable.merge(
    shared.ofType(LoadTasksAction::class.java).compose(loadTasksProcessor),
    shared.ofType(LoadAndFilterTasksAction::class.java).compose(loadAndFilterTasksProcessor),
    shared.ofType(ActivateTaskAction::class.java).compose(activateTaskProcessor),
    shared.ofType(ClearCompletedTasksAction::class.java).compose(clearCompletedTasksProcessor)
    )a
    }b
    }c

    View full-size slide

  43. var actionProcessor: ObservableTransformer =
    ObservableTransformer { actions: Observable ->
    actions.publish { shared ->
    Observable.merge(
    shared.ofType(LoadTasksAction::class.java).compose(loadTasksProcessor),
    shared.ofType(LoadAndFilterTasksAction::class.java).compose(loadAndFilterTasksProcessor),
    shared.ofType(ActivateTaskAction::class.java).compose(activateTaskProcessor),
    shared.ofType(ClearCompletedTasksAction::class.java).compose(clearCompletedTasksProcessor),
    shared.ofType(CompleteTaskAction::class.java).compose(completeTaskProcessor)
    )a
    }b
    }c

    View full-size slide

  44. var actionProcessor: ObservableTransformer =
    ObservableTransformer { actions: Observable ->
    actions.publish { shared ->
    Observable.merge(
    shared.ofType(LoadTasksAction::class.java).compose(loadTasksProcessor),
    shared.ofType(LoadAndFilterTasksAction::class.java).compose(loadAndFilterTasksProcessor),
    shared.ofType(ActivateTaskAction::class.java).compose(activateTaskProcessor),
    shared.ofType(ClearCompletedTasksAction::class.java).compose(clearCompletedTasksProcessor),
    shared.ofType(CompleteTaskAction::class.java).compose(completeTaskProcessor)
    )a
    }b
    }c

    View full-size slide

  45. val loadTasksProcessor =
    ObservableTransformer { actions: Observable ->
    actions.switchMap {
    tasksRepository.getTasks() // Observable>
    }@
    }@

    View full-size slide

  46. val loadTasksProcessor =
    ObservableTransformer { actions: Observable ->
    actions.switchMap {
    tasksRepository.getTasks() // Observable>
    .startWith(LoadTasksResult.InFlight)
    }@
    }@

    View full-size slide

  47. val loadTasksProcessor =
    ObservableTransformer { actions: Observable ->
    actions.switchMap {
    tasksRepository.getTasks() // Observable>
    .startWith(LoadTasksResult.InFlight)
    .map { tasks -> LoadTasksResult.Success(tasks) }
    }@
    }@

    View full-size slide

  48. val loadTasksProcessor =
    ObservableTransformer { actions: Observable ->
    actions.switchMap {
    tasksRepository.getTasks() // Observable>
    .startWith(LoadTasksResult.InFlight)
    .map { tasks -> LoadTasksResult.Success(tasks) }
    .onErrorReturn { t -> loadTasksResult.Failure(t) }
    }@
    }@

    View full-size slide

  49. val loadTasksProcessor =
    ObservableTransformer { actions: Observable ->
    actions.switchMap {
    tasksRepository.getTasks() // Observable>
    .startWith(LoadTasksResult.InFlight)
    .map { tasks -> LoadTasksResult.Success(tasks) }
    .onErrorReturn { t -> loadTasksResult.Failure(t) }
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    }@
    }@

    View full-size slide

  50. val loadTasksProcessor =
    ObservableTransformer { actions: Observable ->
    actions.switchMap {
    tasksRepository.getTasks() // Observable>
    .startWith(LoadTasksResult.InFlight)
    .map { tasks -> LoadTasksResult.Success(tasks) }
    .onErrorReturn { t -> loadTasksResult.Failure(t) }
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    }@
    }@

    View full-size slide

  51. val loadTasksProcessor =
    ObservableTransformer { actions: Observable ->
    actions.switchMap {
    tasksRepository.getTasks() // Observable>
    .map { tasks -> LoadTasksResult.Success(tasks) }
    .onErrorReturn { t -> loadTasksResult.Failure(t) }
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .startWith(LoadTasksResult.InFlight)
    }@
    }@

    View full-size slide

  52. var actionProcessor: ObservableTransformer =
    ObservableTransformer { actions: Observable ->
    actions.publish { shared ->
    Observable.merge(
    shared.ofType(LoadTasksAction::class.java).compose(loadTasksProcessor),
    shared.ofType(LoadAndFilterTasksAction::class.java).compose(loadAndFilterTasksProcessor),
    shared.ofType(ActivateTaskAction::class.java).compose(activateTaskProcessor),
    shared.ofType(ClearCompletedTasksAction::class.java).compose(clearCompletedTasksProcessor),
    shared.ofType(CompleteTaskAction::class.java).compose(completeTaskProcessor)
    )a
    }b
    }c

    View full-size slide

  53. intents // Observable
    .map { intent -> actionFromIntent(intent) } // Observable
    .compose(actionProcessor) // Observable

    View full-size slide

  54. USER
    INTENTS
    Intent
    Result Action
    INTENT
    INTERPRETOR
    PROCESSOR
    REPOSITORY

    View full-size slide

  55. What do we need to render anything?

    View full-size slide

  56. data class TasksViewState(z
    )@

    View full-size slide

  57. data class TasksViewState(z
    val isLoading: Boolean
    )@

    View full-size slide

  58. data class TasksViewState(z
    val isLoading: Boolean,
    val tasksFilterType: TasksFilterType
    )@

    View full-size slide

  59. data class TasksViewState(z
    val isLoading: Boolean,
    val tasksFilterType: TasksFilterType,
    val tasks: List
    )@

    View full-size slide

  60. data class TasksViewState(z
    val isLoading: Boolean,
    val tasksFilterType: TasksFilterType,
    val tasks: List,
    val error: Throwable?
    )@

    View full-size slide

  61. data class TasksViewState(z
    val isLoading: Boolean,
    val tasksFilterType: TasksFilterType,
    val tasks: List,
    val error: Throwable?,
    val taskComplete: Boolean,
    val taskActivated: Boolean,
    val completedTasksCleared: Boolean
    )@

    View full-size slide

  62. data class TasksViewState(z
    val isLoading: Boolean,
    val tasksFilterType: TasksFilterType,
    val tasks: List,
    val error: Throwable?,
    val taskComplete: Boolean,
    val taskActivated: Boolean,
    val completedTasksCleared: Boolean
    )@

    View full-size slide

  63. data class TasksViewState(z
    val isLoading: Boolean,
    val tasksFilterType: TasksFilterType,
    val tasks: List,
    val error: Throwable?,
    val taskComplete: Boolean,
    val taskActivated: Boolean,
    val completedTasksCleared: Boolean
    )@{
    companion object Factory {
    fun default() = TasksViewState(
    isLoading = false,
    tasksFilterType = ALL_TASKS,
    tasks = emptyList(),
    error = null,
    taskComplete = false,
    taskActivated = false,
    completedTasksCleared = false)
    }
    }

    View full-size slide

  64. New
    State
    Result
    Default
    State

    View full-size slide

  65. New
    State
    REDUCER
    Result
    Default
    State

    View full-size slide

  66. New
    State
    REDUCER
    Result
    Previous
    State

    View full-size slide

  67. intents // Observable
    .map { intent -> actionFromIntent(intent) } // Observable
    .compose(actionProcessor) // Observable
    .scan(TasksViewState.default(), reducer) // Observable

    View full-size slide

  68. val reducer = BiFunction
    { previousState: TasksViewState, result: TasksResult ->
    }e

    View full-size slide

  69. val reducer = BiFunction
    { previousState: TasksViewState, result: TasksResult ->
    }e

    View full-size slide

  70. val reducer = BiFunction
    { previousState: TasksViewState, result: TasksResult ->
    when (result)_{
    is LoadTasksResult -> /***/
    is LoadAndFilterTasksResult -> /***/
    is CompleteTaskResult -> /***/
    is ActivateTaskResult -> /***/
    is ClearCompletedTasksResult -> /***/
    }d
    }e

    View full-size slide

  71. val reducer = BiFunction
    { previousState: TasksViewState, result: TasksResult ->
    when (result)_{
    is LoadTasksResult -> {
    when (result) {
    is LoadTasksResult.InFlight -> /***/
    is LoadTasksResult.Failure -> /***/
    is LoadTasksResult.Success -> /***/
    }b
    }c
    is LoadAndFilterTasksResult -> /***/
    is CompleteTaskResult -> /***/
    is ActivateTaskResult -> /***/
    is ClearCompletedTasksResult -> /***/
    }d
    }e

    View full-size slide

  72. val reducer = BiFunction
    { previousState: TasksViewState, result: TasksResult ->
    when (result)_{
    is LoadTasksResult -> {
    when (result) {
    is LoadTasksResult.InFlight -> previousState.copy(isLoading = true)
    is LoadTasksResult.Failure -> /***/
    is LoadTasksResult.Success -> /***/
    }b
    }c
    is LoadAndFilterTasksResult -> /***/
    is CompleteTaskResult -> /***/
    is ActivateTaskResult -> /***/
    is ClearCompletedTasksResult -> /***/
    }d
    }e

    View full-size slide

  73. val reducer = BiFunction
    { previousState: TasksViewState, result: TasksResult ->
    when (result)_{
    is LoadTasksResult -> {
    when (result) {
    is LoadTasksResult.InFlight -> previousState.copy(isLoading = true)
    is LoadTasksResult.Failure -> previousState.copy(isLoading = false,
    error = result.error)
    is LoadTasksResult.Success -> /***/
    }b
    }c
    is LoadAndFilterTasksResult -> /***/
    is CompleteTaskResult -> /***/
    is ActivateTaskResult -> /***/
    is ClearCompletedTasksResult -> /***/
    }d
    }e

    View full-size slide

  74. val reducer = BiFunction
    { previousState: TasksViewState, result: TasksResult ->
    when (result)_{
    is LoadTasksResult -> {
    when (result) {
    is LoadTasksResult.InFlight -> previousState.copy(isLoading = true)
    is LoadTasksResult.Failure -> previousState.copy(isLoading = false,
    error = result.error)
    is LoadTasksResult.Success -> {
    previousState.copy(isLoading = false,
    tasks = result.tasks)
    }a
    }b
    }c
    is LoadAndFilterTasksResult -> /***/
    is CompleteTaskResult -> /***/
    is ActivateTaskResult -> /***/
    is ClearCompletedTasksResult -> /***/
    }d
    }e

    View full-size slide

  75. val reducer = BiFunction
    { previousState: TasksViewState, result: TasksResult ->
    when (result)_{
    is LoadTasksResult -> {
    when (result) {
    is LoadTasksResult.InFlight -> previousState.copy(isLoading = true)
    is LoadTasksResult.Failure -> previousState.copy(isLoading = false,
    error = result.error)
    is LoadTasksResult.Success -> {
    previousState.copy(isLoading = false,
    tasks = result.tasks)
    }a
    }b
    }c
    is LoadAndFilterTasksResult -> /***/
    is CompleteTaskResult -> /***/
    is ActivateTaskResult -> /***/
    is ClearCompletedTasksResult -> /***/
    }d
    }e

    View full-size slide

  76. USER
    INTENTS
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  77. intents // Observable
    .map { intent -> actionFromIntent(intent) } // Observable
    .compose(actionProcessor) // Observable
    .scan(TasksViewState.default(), reducer) // Observable

    View full-size slide

  78. intents // Observable
    .map { intent -> actionFromIntent(intent) } // Observable
    .compose(actionProcessor) // Observable
    .scan(TasksViewState.default(), reducer) // Observable
    .subscribe { state -> render(state) }

    View full-size slide

  79. fun render(state: TasksViewState) {
    }u

    View full-size slide

  80. fun render(state: TasksViewState) {
    swipeRefreshLayout.isRefreshing = state.isLoading
    }u

    View full-size slide

  81. fun render(state: TasksViewState) {
    swipeRefreshLayout.isRefreshing = state.isLoading
    if (state.error != null) {
    showLoadingTasksError()
    return
    }a
    }u

    View full-size slide

  82. fun render(state: TasksViewState) {
    swipeRefreshLayout.isRefreshing = state.isLoading
    if (state.error != null) {
    showLoadingTasksError()
    return
    }a
    if (state.taskActivated) {
    showMessage(getString(R.string.task_marked_active))
    }b
    if (state.taskComplete) {
    showMessage(getString(R.string.task_marked_complete))
    }c
    if (state.completedTasksCleared) {
    showMessage(getString(R.string.completed_tasks_cleared))
    }d
    }u

    View full-size slide

  83. if (state.taskActivated) {
    showMessage(getString(R.string.task_marked_active))
    }b
    if (state.taskComplete) {
    showMessage(getString(R.string.task_marked_complete))
    }c
    if (state.completedTasksCleared) {
    showMessage(getString(R.string.completed_tasks_cleared))
    }d
    if (state.tasks.isEmpty()) {
    when (state.tasksFilterType) {z
    ACTIVE_TASKS -> showNoActiveTasks()
    COMPLETED_TASKS -> showNoCompletedTasks()
    ALL_TASKS -> showNoTasks()
    }e
    }_
    }u

    View full-size slide

  84. showMessage(getString(R.string.completed_tasks_cleared))
    }d
    if (state.tasks.isEmpty()) {
    when (state.tasksFilterType) {z
    ACTIVE_TASKS -> showNoActiveTasks()
    COMPLETED_TASKS -> showNoCompletedTasks()
    ALL_TASKS -> showNoTasks()
    }e
    }_else {
    listAdapter.replaceData(state.tasks)
    tasksView.visibility = View.VISIBLE
    noTasksView.visibility = View.GONE
    when (state.tasksFilterType) {
    ACTIVE_TASKS -> showActiveFilterLabel()
    COMPLETED_TASKS -> showCompletedFilterLabel()
    ALL_TASKS -> showAllFilterLabel()
    }g
    }h
    }u

    View full-size slide

  85. fun render(state: TasksViewState) {
    swipeRefreshLayout.isRefreshing = state.isLoading
    if (state.error != null) {
    showLoadingTasksError()
    return
    }a
    if (state.taskActivated) {
    showMessage(getString(R.string.task_marked_active))
    }b
    if (state.taskComplete) {
    showMessage(getString(R.string.task_marked_complete))
    }c
    if (state.completedTasksCleared) {
    showMessage(getString(R.string.completed_tasks_cleared))
    }d
    if (state.tasks.isEmpty()) {
    when (state.tasksFilterType) {
    ACTIVE_TASKS -> showNoActiveTasks()
    COMPLETED_TASKS -> showNoCompletedTasks()
    ALL_TASKS -> showNoTasks()
    }e
    }_else {
    listAdapter.replaceData(state.tasks)
    tasksView.visibility = View.VISIBLE
    noTasksView.visibility = View.GONE
    when (state.tasksFilterType) {
    ACTIVE_TASKS -> showActiveFilterLabel()
    COMPLETED_TASKS -> showCompletedFilterLabel()
    ALL_TASKS -> showAllFilterLabel()
    }g
    }h
    }u

    View full-size slide

  86. USER
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  87. effects Yo Dawg, I heard you like side
    Yo Dawg, I heard you like side effects

    View full-size slide

  88. USER
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  89. USER
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  90. USER
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  91. USER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    REPOSITORY
    INTENTS
    RENDER
    PROCESSOR

    View full-size slide

  92. USER
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY
    Unidirectional data flow

    View full-size slide

  93. USER
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  94. USER
    USER INTERFACE INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  95. USER
    USER INTERFACE
    VIEW MODEL
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  96. interface TasksUi {
    fun render(state: TasksViewState)
    fun intents(): Observable
    }@
    interface TasksViewModel {
    fun processIntents(intents: Observable)
    fun states(): Observable
    }@

    View full-size slide

  97. interface TasksUi {
    fun render(state: TasksViewState)
    fun intents(): Observable
    }@
    interface TasksViewModel {
    fun processIntents(intents: Observable)
    fun states(): Observable
    }@

    View full-size slide

  98. interface TasksUi {
    fun render(state: TasksViewState)
    fun intents(): Observable
    }@
    interface TasksViewModel {
    fun processIntents(intents: Observable)
    fun states(): Observable
    }@

    View full-size slide

  99. User: initialIntent()
    We: render()
    User: activateTask(1)
    User: activateTask(2)
    User: refresh()
    Mum:
    Android: you.onStop()

    View full-size slide

  100. User: initialIntent()
    We: render()
    User: activateTask(1)
    User: activateTask(2)
    User: refresh()
    Mum:
    Android: you.onStop()

    View full-size slide

  101. User: initialIntent()
    We: render()
    User: activateTask(1)
    User: activateTask(2)
    User: refresh()
    Mum:
    Android: you.onStop()

    View full-size slide

  102. User: initialIntent()
    We: render()
    User: activateTask(1)
    User: activateTask(2)
    User: refresh()
    Mum:
    Android: we.onStop()

    View full-size slide

  103. USER
    USER INTERFACE
    VIEW MODEL
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY
    C
    B
    A

    View full-size slide

  104. INTENTS
    RENDER
    VIEW MODEL
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY
    B
    C
    A
    USER
    USER INTERFACE

    View full-size slide

  105. INTENTS
    RENDER
    VIEW MODEL
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY
    USER
    USER INTERFACE
    B
    C
    A

    View full-size slide

  106. USER
    USER INTERFACE
    VIEW MODEL
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY
    A

    View full-size slide

  107. USER
    USER INTERFACE
    VIEW MODEL
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY
    A

    View full-size slide

  108. Config change everywhere

    View full-size slide

  109. USER INTERFACE
    VIEW MODEL
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  110. USER INTERFACE
    VIEW MODEL
    SOURCE
    SUBSCRIBER

    View full-size slide

  111. USER INTERFACE
    VIEW MODEL
    SOURCE
    SUBSCRIBER

    View full-size slide

  112. USER INTERFACE
    VIEW MODEL
    SOURCE
    SUBSCRIBER
    SUBJECT

    View full-size slide

  113. USER INTERFACE
    VIEW MODEL
    SOURCE
    SUBSCRIBER
    SUBJECT

    View full-size slide

  114. USER INTERFACE
    VIEW MODEL
    SOURCE
    SUBSCRIBER
    SUBJECT

    View full-size slide

  115. USER INTERFACE
    VIEW MODEL
    SOURCE
    SUBSCRIBER
    SUBJECT
    SUBJECT

    View full-size slide

  116. USER INTERFACE
    VIEW MODEL
    SOURCE
    SUBSCRIBER
    SUBJECT SUBJECT

    View full-size slide

  117. VIEW MODEL
    SUBJECT SUBJECT

    View full-size slide

  118. USER INTERFACE
    VIEW MODEL
    SOURCE
    SUBSCRIBER
    SUBJECT SUBJECT

    View full-size slide

  119. class TasksViewModel : ViewModel() {
    fun processIntents(intents: Observable) {
    }b
    fun states(): Observable {
    }c
    }e

    View full-size slide

  120. class TasksViewModel : ViewModel() {
    val intentsSubject: PublishSubject = PublishSubject.create()
    fun processIntents(intents: Observable) {
    intents.subscribe(intentsSubject)
    }b
    fun states(): Observable {
    }c
    }e

    View full-size slide

  121. class TasksViewModel : ViewModel() {
    val intentsSubject: PublishSubject = PublishSubject.create()
    val statesSubject: PublishSubject = PublishSubject.create()
    fun processIntents(intents: Observable) {
    intents.subscribe(intentsSubject)
    }b
    fun states(): Observable {
    return statesSubject
    }c
    }e

    View full-size slide

  122. class TasksViewModel : ViewModel() {
    val intentsSubject: PublishSubject = PublishSubject.create()
    val statesSubject: PublishSubject = PublishSubject.create()
    init {
    intentsSubject
    .map { intent -> actionFromIntent(intent) }
    .compose(actionProcessor)
    .scan(TasksViewState.default(), reducer)
    }a
    fun processIntents(intents: Observable) {
    intents.subscribe(intentsSubject)
    }b
    fun states(): Observable {
    return statesSubject
    }c
    }e

    View full-size slide

  123. class TasksViewModel : ViewModel() {
    val intentsSubject: PublishSubject = PublishSubject.create()
    val statesSubject: PublishSubject = PublishSubject.create()
    init {
    intentsSubject
    .map { intent -> actionFromIntent(intent) }
    .compose(actionProcessor)
    .scan(TasksViewState.default(), reducer)
    .subscribe(statesSubject)
    }a
    fun processIntents(intents: Observable) {
    intents.subscribe(intentsSubject)
    }b
    fun states(): Observable {
    return statesSubject
    }c
    }e

    View full-size slide

  124. class TasksViewModel : ViewModel() {
    val intentsSubject: PublishSubject = PublishSubject.create()
    val statesSubject: PublishSubject = PublishSubject.create()
    init {
    intentsSubject
    .map { intent -> actionFromIntent(intent) }
    .compose(actionProcessor)
    .scan(TasksViewState.default(), reducer)
    .subscribe(statesSubject)
    }a
    fun processIntents(intents: Observable) {
    intents.subscribe(intentsSubject)
    }b
    fun states(): Observable {
    return statesSubject
    }c
    }e

    View full-size slide

  125. class TasksFragment : Fragment() {
    fun intents(): Observable { /***/ }
    fun render(state: TasksViewState) { /***/ }
    }d

    View full-size slide

  126. class TasksFragment : Fragment() {
    private val viewModel: TasksViewModel by lazy(NONE) {
    ViewModelProviders.of(this).get(TasksViewModel::class.java)
    }a
    fun intents(): Observable { /***/ }
    fun render(state: TasksViewState) { /***/ }
    }d

    View full-size slide

  127. class TasksFragment : Fragment() {
    private val viewModel: TasksViewModel by lazy(NONE) {
    ViewModelProviders.of(this).get(TasksViewModel::class.java)
    }a
    fun intents(): Observable { /***/ }
    fun render(state: TasksViewState) { /***/ }
    override fun onStart() {
    viewModel.states().subscribe(this::render)
    }b
    }d

    View full-size slide

  128. class TasksFragment : Fragment() {
    private val viewModel: TasksViewModel by lazy(NONE) {
    ViewModelProviders.of(this).get(TasksViewModel::class.java)
    }a
    private val disposables = CompositeDisposable()a
    fun intents(): Observable { /***/ }
    fun render(state: TasksViewState) { /***/ }
    override fun onStart() {
    disposables.add(viewModel.states().subscribe(this::render))
    }b
    }d

    View full-size slide

  129. class TasksFragment : Fragment() {
    private val viewModel: TasksViewModel by lazy(NONE) {
    ViewModelProviders.of(this).get(TasksViewModel::class.java)
    }a
    private val disposables = CompositeDisposable()
    fun intents(): Observable { /***/ }
    fun render(state: TasksViewState) { /***/ }
    override fun onStart() {
    disposables.add(viewModel.states().subscribe(this::render))
    viewModel.processIntents(intents())
    }b
    }d

    View full-size slide

  130. class TasksFragment : Fragment() {
    private val viewModel: TasksViewModel by lazy(NONE) {
    ViewModelProviders.of(this).get(TasksViewModel::class.java)
    }a
    private val disposables = CompositeDisposable()
    fun intents(): Observable { /***/ }
    fun render(state: TasksViewState) { /***/ }
    override fun onStart() {
    disposables.add(viewModel.states().subscribe(this::render))
    viewModel.processIntents(intents())
    }b
    override fun onStop() {
    disposables.dispose()
    super.onStop()
    }c
    }d

    View full-size slide

  131. class TasksFragment : Fragment() {
    private val viewModel: TasksViewModel by lazy(NONE) {
    ViewModelProviders.of(this).get(TasksViewModel::class.java)
    }a
    private val disposables = CompositeDisposable()
    fun intents(): Observable { /***/ }
    fun render(state: TasksViewState) { /***/ }
    override fun onStart() {
    disposables.add(viewModel.states().subscribe(this::render))
    viewModel.processIntents(intents())
    }b
    override fun onStop() {
    disposables.dispose()
    super.onStop()
    }c
    }d

    View full-size slide

  132. class TasksFragment : Fragment() {
    private val viewModel: TasksViewModel by lazy(NONE) {
    ViewModelProviders.of(this).get(TasksViewModel::class.java)
    }a
    private val disposables = CompositeDisposable()
    fun intents(): Observable { /***/ }
    fun render(state: TasksViewState) { /***/ }
    override fun onStart() {
    disposables.add(viewModel.states().subscribe(this::render))
    viewModel.processIntents(intents())
    }b
    override fun onStop() {
    disposables.dispose()
    super.onStop()
    }c
    }d

    View full-size slide

  133. class TasksFragment
    fun intents(): Observable {
    return Observable.merge(initialIntent(),
    refreshIntent(),
    completeTaskIntent(),
    activateTaskIntent(),
    clearCompletedTaskIntent(),
    changeFilterIntent())
    }@
    }@

    View full-size slide

  134. class TasksFragment
    fun intents(): Observable {
    return Observable.merge(initialIntent(),
    refreshIntent(),
    completeTaskIntent(),
    activateTaskIntent(),
    clearCompletedTaskIntent(),
    changeFilterIntent())
    }@
    }@
    fun actionFromIntent(intent: TasksIntent): TasksAction =
    when (intent) {
    is InitialIntent -> LoadAndFilterTasksAction
    /***/
    }@

    View full-size slide

  135. private fun compose(): Observable {
    return intentsSubject
    .map { this.actionFromIntent(it) }
    .compose(actionProcessorHolder.actionProcessor)
    .scan(TasksViewState.idle(), reducer)
    }a

    View full-size slide

  136. private fun compose(): Observable {
    return intentsSubject
    .scan(initialIntentFilter)
    .map { this.actionFromIntent(it) }
    .compose(actionProcessorHolder.actionProcessor)
    .scan(TasksViewState.idle(), reducer)
    }a

    View full-size slide

  137. private fun compose(): Observable {
    return intentsSubject
    .scan(initialIntentFilter)
    .map { this.actionFromIntent(it) }
    .compose(actionProcessorHolder.actionProcessor)
    .scan(TasksViewState.idle(), reducer)
    }a
    private val initialIntentFilter =
    BiFunction { _: TasksIntent, newIntent: TasksIntent ->
    }c

    View full-size slide

  138. private fun compose(): Observable {
    return intentsSubject
    .scan(initialIntentFilter)
    .map { this.actionFromIntent(it) }
    .compose(actionProcessorHolder.actionProcessor)
    .scan(TasksViewState.idle(), reducer)
    }a
    private val initialIntentFilter =
    BiFunction { _: TasksIntent, newIntent: TasksIntent ->
    if (newIntent is TasksIntent.InitialIntent) {
    TasksIntent.GetLastState
    }_
    }c

    View full-size slide

  139. private fun compose(): Observable {
    return intentsSubject
    .scan(initialIntentFilter)
    .map { this.actionFromIntent(it) }
    .compose(actionProcessorHolder.actionProcessor)
    .scan(TasksViewState.idle(), reducer)
    }a
    private val initialIntentFilter =
    BiFunction { _: TasksIntent, newIntent: TasksIntent ->
    if (newIntent is TasksIntent.InitialIntent) {
    TasksIntent.GetLastState
    }_else {
    newIntent
    }b
    }c

    View full-size slide

  140. private fun compose(): Observable {
    return intentsSubject
    .scan(initialIntentFilter)
    .map { this.actionFromIntent(it) }
    .compose(actionProcessorHolder.actionProcessor)
    .scan(TasksViewState.idle(), reducer)
    }a
    private val initialIntentFilter =
    BiFunction { _: TasksIntent, newIntent: TasksIntent ->
    if (newIntent is TasksIntent.InitialIntent) {
    TasksIntent.GetLastState
    }_else {
    newIntent
    }b
    }c

    View full-size slide

  141. Do you even test?

    View full-size slide

  142. USER INTERFACE
    VIEW MODEL
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  143. USER INTERFACE
    VIEW MODEL
    INTENTS
    RENDER
    Intent
    Result Action
    State
    TEST OBSERVER
    SUBJECT
    PROCESSOR
    REPOSITORY

    View full-size slide

  144. USER INTERFACE
    VIEW MODEL
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  145. USER INTERFACE
    VIEW MODEL
    SUBJECT
    TEST OBSERVER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  146. USER INTERFACE
    VIEW MODEL
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  147. USER INTERFACE
    VIEW MODEL
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  148. USER INTERFACE
    VIEW MODEL
    INTENTS
    RENDER
    Intent
    Result Action
    State
    INTENT
    INTERPRETOR
    REDUCER
    PROCESSOR
    REPOSITORY

    View full-size slide

  149. Action
    Load and Filter Tasks Logic
    Clear Completed Task Logic
    Complete Task Logic
    Activate Task Logic
    Load Tasks Logic
    Result PROCESSOR

    View full-size slide

  150. Load and Filter Tasks Logic
    Clear Completed Task Logic
    Complete Task Logic
    Activate Task Logic
    Load Tasks Logic SUBJECT
    TEST OBSERVER
    SUBJECT
    SUBJECT
    SUBJECT
    SUBJECT
    TEST OBSERVER
    TEST OBSERVER
    TEST OBSERVER
    TEST OBSERVER

    View full-size slide

  151. Fin
    Benoît Quenaudon @oldergod

    View full-size slide