MVWTF: Demystifying Architecture Patterns

MVWTF: Demystifying Architecture Patterns

Breaking down the differences between a number of the MV* patterns on Android.

Bc87ea9c7a0f85b8761b716a677c6694?s=128

Adam McNeilly

August 14, 2019
Tweet

Transcript

  1. MVWTF: Demystifying Architecture Patterns Adam McNeilly - @AdamMc331 @AdamMc331 #AndroidSummit

    1
  2. You May Have Heard These Buzzwords: • MVC • MVP

    • MVVM • MVI • MVU?? @AdamMc331 #AndroidSummit 2
  3. Why Are There So Many? @AdamMc331 #AndroidSummit 3

  4. What's The Difference? @AdamMc331 #AndroidSummit 4

  5. Which One Should I Use? @AdamMc331 #AndroidSummit 5

  6. Which One Should I Use? @AdamMc331 #AndroidSummit 6

  7. Why Do We Need Architecture Patterns? @AdamMc331 #AndroidSummit 7

  8. More Buzzwords! • Maintainability • Extensibility • Robust • Testable

    @AdamMc331 #AndroidSummit 8
  9. Let's Start With One Simple Truth @AdamMc331 #AndroidSummit 9

  10. You Can't Put Everything In The Activity @AdamMc331 #AndroidSummit 10

  11. Or Your Fragment1 1 Thanks Mauricio for proofreading @AdamMc331 #AndroidSummit

    11
  12. Why Not? • Not readable • Difficult to add new

    code • Difficult to change existing code • Can't write Junit tests for this @AdamMc331 #AndroidSummit 12
  13. We Need To Break Up Our Code @AdamMc331 #AndroidSummit 13

  14. Let's Explore Some Options @AdamMc331 #AndroidSummit 14

  15. Model-View-Controller • One of the earliest architecture patterns • Introduced

    in the 1970s as a way to organize code • Divides application to three parts @AdamMc331 #AndroidSummit 15
  16. Model • This is your data source • Database, remote

    server, etc • It does not care about the view @AdamMc331 #AndroidSummit 16
  17. View • This is the visual representation of information •

    Does not care where this data came from • Only responsible for displaying data • If your view has a conditional, consider refactoring @AdamMc331 #AndroidSummit 17
  18. Controller • Handles user inputs • Validates if necessary •

    Passes input to model • Passes model response to view @AdamMc331 #AndroidSummit 18
  19. The Model & View Components Are The Same For All

    Patterns @AdamMc331 #AndroidSummit 19
  20. @AdamMc331 #AndroidSummit 20

  21. Model-View-WhateverTheFYouWant @AdamMc331 #AndroidSummit 21

  22. Why Do We Have So Many Options For This Third

    Component? @AdamMc331 #AndroidSummit 22
  23. Short Answer: State Management @AdamMc331 #AndroidSummit 23

  24. Long Answer: Let's Break Them Down @AdamMc331 #AndroidSummit 24

  25. Model-View-Controller @AdamMc331 #AndroidSummit 25

  26. Why Don't We Use This For Android? @AdamMc331 #AndroidSummit 26

  27. Why Don't We Use This For Android? @AdamMc331 #AndroidSummit 27

  28. Why Don't We Use This For Android? • We can't

    write Junit tests for an Activity • We can't unit test our UI logic • We don't really have a separation of concerns here @AdamMc331 #AndroidSummit 28
  29. Model-View-Presenter @AdamMc331 #AndroidSummit 29

  30. Model-View-Presenter • Similar to the last pattern • Moves our

    presentation logic out of the Activity class @AdamMc331 #AndroidSummit 30
  31. Model-View-Presenter @AdamMc331 #AndroidSummit 31

  32. Why Is This Better? • UI logic is outside of

    the Activity, and now supports Junit tests • Our concerns are separated again @AdamMc331 #AndroidSummit 32
  33. MVP Implementation @AdamMc331 #AndroidSummit 33

  34. Contract Class class TaskListContract { interface View { fun showTasks(tasks:

    List<Task>) } interface Presenter { fun viewCreated() fun viewDestroyed() } interface Model { fun getTasks(): List<Task> } } @AdamMc331 #AndroidSummit 34
  35. Contract Class class TaskListContract { interface View { fun showTasks(tasks:

    List<Task>) } interface Presenter { fun viewCreated() fun viewDestroyed() } interface Model { fun getTasks(): List<Task> } } @AdamMc331 #AndroidSummit 35
  36. Model class InMemoryTaskService : TaskListContract.Model { override fun getTasks(): List<Task>

    { return listOf( Task("Sample task 1"), Task("Sample task 2") ) } } @AdamMc331 #AndroidSummit 36
  37. View class TaskListActivity : AppCompatActivity(), TaskListContract.View { private val taskAdapter

    = TaskAdapter() private val presenter = TaskListPresenter(this, TaskRepository()) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... presenter.viewCreated() } override fun onDestroy() { presenter.viewDestroyed() super.onDestroy() } override fun showTasks(tasks: List<Task>) { taskAdapter.tasks = tasks } } @AdamMc331 #AndroidSummit 37
  38. View class TaskListActivity : AppCompatActivity(), TaskListContract.View { private val taskAdapter

    = TaskAdapter() private val presenter = TaskListPresenter(this, TaskRepository()) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... presenter.viewCreated() } override fun onDestroy() { presenter.viewDestroyed() super.onDestroy() } override fun showTasks(tasks: List<Task>) { taskAdapter.tasks = tasks } } @AdamMc331 #AndroidSummit 38
  39. Presenter class TaskListPresenter( private var view: TaskListContract.View?, private val model:

    TaskListContract.Model ) : TaskListContract.Presenter { override fun viewCreated() { val tasks = model.getTasks() view?.showTasks(tasks) } override fun viewDestroyed() { view = null } } @AdamMc331 #AndroidSummit 39
  40. Presenter class TaskListPresenter( private var view: TaskListContract.View?, private val model:

    TaskListContract.Model ) : TaskListContract.Presenter { override fun viewCreated() { val tasks = model.getTasks() view?.showTasks(tasks) } override fun viewDestroyed() { view = null } } @AdamMc331 #AndroidSummit 40
  41. Is That Enough? • View does nothing but display data

    • Data fetching is all handled by model • Presentation of data is handled by presenter • Everything is separated, everything is testable • If you think this is good enough, use it! @AdamMc331 #AndroidSummit 41
  42. What's Different About MVVM? @AdamMc331 #AndroidSummit 42

  43. The Presenter Doesn't Need To Care About The View @AdamMc331

    #AndroidSummit 43
  44. Model-View-ViewModel @AdamMc331 #AndroidSummit 44

  45. MVVM Implementation @AdamMc331 #AndroidSummit 45

  46. Model Doesn't Change (much) interface TaskRepository { fun getTasks(): List<Task>

    } class InMemoryTaskService : TaskRepository { override fun getTasks(): List<Task> { return listOf(...) } } @AdamMc331 #AndroidSummit 46
  47. ViewModel class TaskListViewModel( private val repository: TaskRepository ) { private

    val tasks = MutableLiveData<List<Task>>() fun getTasks(): LiveData<List<Task>> = tasks init { fetchTasks() } private fun fetchTasks() { tasks.value = repository.getTasks() } } @AdamMc331 #AndroidSummit 47
  48. ViewModel class TaskListViewModel( private val repository: TaskRepository ) { private

    val tasks = MutableLiveData<List<Task>>() fun getTasks(): LiveData<List<Task>> = tasks init { fetchTasks() } private fun fetchTasks() { tasks.value = repository.getTasks() } } @AdamMc331 #AndroidSummit 48
  49. ViewModel class TaskListViewModel( private val repository: TaskRepository ) { private

    val tasks = MutableLiveData<List<Task>>() fun getTasks(): LiveData<List<Task>> = tasks init { fetchTasks() } private fun fetchTasks() { tasks.value = repository.getTasks() } } @AdamMc331 #AndroidSummit 49
  50. View class TaskListActivity : AppCompatActivity() { private val adapter =

    TaskAdapter() private val viewModel = TaskListviewModel(repository = InMemoryTaskService()) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... subscribeToViewModel() } private fun subscribeToViewModel() { viewModel.getTasks().observe(this, Observer { tasks -> adapter.tasks = tasks }) } } @AdamMc331 #AndroidSummit 50
  51. View class TaskListActivity : AppCompatActivity() { private val adapter =

    TaskAdapter() private val viewModel = TaskListviewModel(repository = InMemoryTaskService()) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... subscribeToViewModel() } private fun subscribeToViewModel() { viewModel.getTasks().observe(this, Observer { tasks -> adapter.tasks = tasks }) } } @AdamMc331 #AndroidSummit 51
  52. This Is Pretty Close To MVP, With One New Benefit

    @AdamMc331 #AndroidSummit 52
  53. Since ViewModel Doesn't Reference View, We Can Leverage Android ViewModel

    To Outlast Config Changes @AdamMc331 #AndroidSummit 53
  54. Handle Rotation In MVP 1. Update your presenter to save/restore

    state 2. Modify the view to call appropriate save/restore methods @AdamMc331 #AndroidSummit 54
  55. Handle Rotation In MVP class TaskListContract { interface Presenter {

    // New: fun getState(): Bundle fun restoreState(bundle: Bundle?) } } @AdamMc331 #AndroidSummit 55
  56. Handle Rotation In MVP class TaskListActivity : AppCompatActivity(), TaskListContract.View {

    override fun onCreate(savedInstanceState: Bundle?) { // ... presenter.restoreState(savedInstanceState) } override fun onSaveInstanceState(outState: Bundle) { outState.putAll(presenter.getState()) super.onSaveInstanceState(outState) } } @AdamMc331 #AndroidSummit 56
  57. Handle Rotation In MVVM 1. Have ViewModel class extend the

    Android ViewModel class 2. Update Activity to use ViewModelProviders 3. Since Android's ViewModel outlasts config changes, no need to save/restore state, just re-subscribe @AdamMc331 #AndroidSummit 57
  58. Handle Rotation In MVVM class TaskListViewModel( private val repository: TaskRepository

    ) : ViewModel() { // ... } @AdamMc331 #AndroidSummit 58
  59. Handle Rotation In MVVM class TaskListActivity : AppCompatActivity() { private

    lateinit var viewModel: TaskListViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... setupViewModel() } private fun setupViewModel() { viewModel = ViewModelProviders.of(this, viewModelFactory).get(TaskListViewModel::class.java) viewModel.getTasks().observe(this, Observer { tasks -> taskAdapter.tasks = tasks }) } } @AdamMc331 #AndroidSummit 59
  60. Is That Enough? • View does nothing but display data

    • Data fetching is all handled by model • ViewModel handles all UI logic • We can easily save state across config changes • Everything is separated, everything is testable • If you think this is good enough, use it! @AdamMc331 #AndroidSummit 60
  61. Where Does MVVM Fall Short? @AdamMc331 #AndroidSummit 61

  62. Let's Consider A More Complicated State @AdamMc331 #AndroidSummit 62

  63. Let's Consider A More Complicated State sealed class TaskListState {

    object Loading : TaskListState() data class Loaded(val tasks: List<Task>) : TaskListState() data class Error(val error: Throwable?) : TaskListState() } @AdamMc331 #AndroidSummit 63
  64. Let's Consider A More Complicated State class TaskListViewModel(private val repository:

    TaskRepository) : ViewModel() { init { showLoading() try { fetchTasks() } catch (e: Exception) { showError() } } // ... } @AdamMc331 #AndroidSummit 64
  65. Let's Consider A More Complicated State class TaskListViewModel(private val repository:

    TaskRepository) : ViewModel() { // ... private fun showLoading() { state.value = TaskListState.Loading } private fun fetchTasks() { val tasks = repository.getItems() state.value = TaskListState.Loaded(tasks) } private fun showError() { state.value = TaskListState.Error(Throwable("Unable to fetch tasks.")) } } @AdamMc331 #AndroidSummit 65
  66. What Are The Risks Of These Methods? private fun showLoading()

    { state.value = TaskListState.Loading } private fun fetchTasks() { val tasks = repository.getItems() state.value = TaskListState.Loaded(tasks) } private fun showError() { state.value = TaskListState.Error(Throwable("Unable to fetch tasks.")) } @AdamMc331 #AndroidSummit 66
  67. What Are The Risks Of These Methods? • Any methods

    in the class can call them • We can't guarantee they're associated with a specific action or intent • We have multiple methods manipulating our state that we have to ensure don't conflict with each other @AdamMc331 #AndroidSummit 67
  68. How Can We Mitigate This Risk? • Have one single

    source of truth for our state • Do this through a single pipeline where every action causes a specific change in the state • This makes state changes predictable, and therefore highly testable as well @AdamMc331 #AndroidSummit 68
  69. Model-View-Intent @AdamMc331 #AndroidSummit 69

  70. Model-View-Intent • Unlike the previous patterns, "Intent" isn't used to

    reference a specific kind of component, but rather the intention of doing something that we want to capture in our state. @AdamMc331 #AndroidSummit 70
  71. The First Goal Is To Make Our State Changes Predictable

    @AdamMc331 #AndroidSummit 71
  72. We Achieve This With A Reducer abstract class Reducer {

    abstract fun reduce(action: Action, state: State): State } @AdamMc331 #AndroidSummit 72
  73. Clearly Defined Inputs And Outputs class TaskListReducer : Reducer<TaskListState>() {

    override fun reduce(action: Action, state: TaskListState): TaskListState { return when (action) { is TaskListAction.TasksLoading -> TaskListState.Loading() is TaskListAction.TasksLoaded -> TaskListState.Loaded(action.tasks) is TaskListAction.TasksErrored -> TaskListState.Error() else -> state } } } @AdamMc331 #AndroidSummit 73
  74. We Also Want A Single Source Of Truth @AdamMc331 #AndroidSummit

    74
  75. We Create A State Container Called A Store • Contains

    our state and exposes it for anyone to observe • Contains our reducer instance • Dispatches actions into that reducer to modify the state @AdamMc331 #AndroidSummit 75
  76. Store Implementation class BaseStore<S : State>( initialState: S, private val

    reducer: Reducer<S> ) { private var stateListener: ((S) -> Unit)? = null private var currentState: S = initialState set(value) { field = value stateListener?.invoke(value) } fun dispatch(action: Action) { currentState = reducer.reduce(action, currentState) } fun subscribe(stateListener: ((S) -> Unit)?) { this.stateListener = stateListener } } @AdamMc331 #AndroidSummit 76
  77. Store Implementation class BaseStore<S : State>( initialState: S, private val

    reducer: Reducer<S> ) { private var stateListener: ((S) -> Unit)? = null private var currentState: S = initialState set(value) { field = value stateListener?.invoke(value) } fun dispatch(action: Action) { currentState = reducer.reduce(action, currentState) } fun subscribe(stateListener: ((S) -> Unit)?) { this.stateListener = stateListener } } @AdamMc331 #AndroidSummit 77
  78. Store Implementation class BaseStore<S : State>( initialState: S, private val

    reducer: Reducer<S> ) { private var stateListener: ((S) -> Unit)? = null private var currentState: S = initialState set(value) { field = value stateListener?.invoke(value) } fun dispatch(action: Action) { currentState = reducer.reduce(action, currentState) } fun subscribe(stateListener: ((S) -> Unit)?) { this.stateListener = stateListener } } @AdamMc331 #AndroidSummit 78
  79. Store Implementation class BaseStore<S : State>( initialState: S, private val

    reducer: Reducer<S> ) { private var stateListener: ((S) -> Unit)? = null private var currentState: S = initialState set(value) { field = value stateListener?.invoke(value) } fun dispatch(action: Action) { currentState = reducer.reduce(action, currentState) } fun subscribe(stateListener: ((S) -> Unit)?) { this.stateListener = stateListener } } @AdamMc331 #AndroidSummit 79
  80. Redux Diagram2 2 https://www.esri.com/arcgis-blog/products/3d-gis/3d-gis/react-redux-building-modern-web-apps-with-the-arcgis- js-api/ @AdamMc331 #AndroidSummit 80

  81. Hook This Up To Our ViewModel/Presenter class TaskListViewModel(private val repository:

    TaskRepository) : ViewModel() { private val store: BaseStore<TaskListState> = BaseStore( TaskListState.Loading(), TaskListReducer() ) // ... private fun fetchTasks() { store.dispatch(TaskListAction.TasksLoading) try { val tasks = repository.getTasks() store.dispatch(TaskListAction.TasksLoaded(tasks)) } catch (e: Throwable) { store.dispatch(TaskListAction.TasksErrored(e)) } } } @AdamMc331 #AndroidSummit 81
  82. Hook This Up To Our ViewModel/Presenter class TaskListViewModel(private val repository:

    TaskRepository) : ViewModel() { private val store: BaseStore<TaskListState> = BaseStore( TaskListState.Loading(), TaskListReducer() ) // ... private fun fetchTasks() { store.dispatch(TaskListAction.TasksLoading) try { val tasks = repository.getTasks() store.dispatch(TaskListAction.TasksLoaded(tasks)) } catch (e: Throwable) { store.dispatch(TaskListAction.TasksErrored(e)) } } } @AdamMc331 #AndroidSummit 82
  83. Hook This Up To Our ViewModel/Presenter class TaskListViewModel(private val repository:

    TaskRepository) : ViewModel() { private val store: BaseStore<TaskListState> = BaseStore( TaskListState.Loading(), TaskListReducer() ) // ... private fun fetchTasks() { store.dispatch(TaskListAction.TasksLoading) try { val tasks = repository.getTasks() store.dispatch(TaskListAction.TasksLoaded(tasks)) } catch (e: Throwable) { store.dispatch(TaskListAction.TasksErrored(e)) } } } @AdamMc331 #AndroidSummit 83
  84. Is That Enough? • View does nothing but display data

    • Data fetching is all handled by model • ViewModel handles UI logic • We can easily save state across config changes • Everything is separated, everything is testable • State management is clear and predictable • If you think this is good enough, use it! @AdamMc331 #AndroidSummit 84
  85. Is MVI The Best We Can Do? • State management

    is pretty solid • But, we have 22 letters that weren't covered yet @AdamMc331 #AndroidSummit 85
  86. What Should I Take Away From This? @AdamMc331 #AndroidSummit 86

  87. Model-View-Presenter • Separated concerns and allows us to unit test

    all of our code • Good for quick prototyping • Good for blog post samples because of its readability • Can handle config changes but requires a little more work • State management is unpredictable @AdamMc331 #AndroidSummit 87
  88. Model-View-ViewModel • Separated concerns and allows us to unit test

    all of our code • Even better for quick prototyping • No contract class boilerplate • Good for blog post samples because of its readability3 • Can handle config changes easily if we use Android's architecture components • State management is unpredictable 3 Depending on how you expose information @AdamMc331 #AndroidSummit 88
  89. Model-View-Intent • Can work with presenter or viewmodel • Separated

    concerns, testability come with this • Not good for quick prototyping • Can be confusing if used for sample apps due to unfamiliarity • Can handle config changes based on whether we used a presenter or a viewmodel • State management is clear and predictable @AdamMc331 #AndroidSummit 89
  90. General Suggestions • MVP can get you up and running

    quickly, but due to the boilerplate and config changes work I wouldn't recommend it. • MVVM is what I'd recommend the most. It allows for separation of concerns and unit test support without a major learning curve. • If your app handles complex user flows or states, MVI can give you more support for state management. @AdamMc331 #AndroidSummit 90
  91. What's Most Important • Be consistent @AdamMc331 #AndroidSummit 91

  92. Thank you! https://github.com/adammc331/mvwtf @AdamMc331 #AndroidSummit 92