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

MVWTF: Demystifying Architecture Patterns

MVWTF: Demystifying Architecture Patterns

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

Adam McNeilly

August 14, 2019
Tweet

More Decks by Adam McNeilly

Other Decks in Programming

Transcript

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

    View Slide

  2. You May Have Heard These Buzzwords:
    • MVC
    • MVP
    • MVVM
    • MVI
    • MVU??
    @AdamMc331
    #AndroidSummit 2

    View Slide

  3. Why Are There So Many?
    @AdamMc331
    #AndroidSummit 3

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. More Buzzwords!
    • Maintainability
    • Extensibility
    • Robust
    • Testable
    @AdamMc331
    #AndroidSummit 8

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  13. We Need To Break Up Our Code
    @AdamMc331
    #AndroidSummit 13

    View Slide

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

    View Slide

  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

    View Slide

  16. Model
    • This is your data source
    • Database, remote server, etc
    • It does not care about the view
    @AdamMc331
    #AndroidSummit 16

    View Slide

  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

    View Slide

  18. Controller
    • Handles user inputs
    • Validates if necessary
    • Passes input to model
    • Passes model response to view
    @AdamMc331
    #AndroidSummit 18

    View Slide

  19. The Model & View Components Are
    The Same For All Patterns
    @AdamMc331
    #AndroidSummit 19

    View Slide

  20. @AdamMc331
    #AndroidSummit 20

    View Slide

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

    View Slide

  22. Why Do We Have So Many Options
    For This Third Component?
    @AdamMc331
    #AndroidSummit 22

    View Slide

  23. Short Answer: State Management
    @AdamMc331
    #AndroidSummit 23

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  29. Model-View-Presenter
    @AdamMc331
    #AndroidSummit 29

    View Slide

  30. Model-View-Presenter
    • Similar to the last pattern
    • Moves our presentation logic out of the Activity class
    @AdamMc331
    #AndroidSummit 30

    View Slide

  31. Model-View-Presenter
    @AdamMc331
    #AndroidSummit 31

    View Slide

  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

    View Slide

  33. MVP Implementation
    @AdamMc331
    #AndroidSummit 33

    View Slide

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

    View Slide

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

    View Slide

  36. Model
    class InMemoryTaskService : TaskListContract.Model {
    override fun getTasks(): List {
    return listOf(
    Task("Sample task 1"),
    Task("Sample task 2")
    )
    }
    }
    @AdamMc331
    #AndroidSummit 36

    View Slide

  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) {
    taskAdapter.tasks = tasks
    }
    }
    @AdamMc331
    #AndroidSummit 37

    View Slide

  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) {
    taskAdapter.tasks = tasks
    }
    }
    @AdamMc331
    #AndroidSummit 38

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  42. What's Different About MVVM?
    @AdamMc331
    #AndroidSummit 42

    View Slide

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

    View Slide

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

    View Slide

  45. MVVM Implementation
    @AdamMc331
    #AndroidSummit 45

    View Slide

  46. Model Doesn't Change (much)
    interface TaskRepository {
    fun getTasks(): List
    }
    class InMemoryTaskService : TaskRepository {
    override fun getTasks(): List {
    return listOf(...)
    }
    }
    @AdamMc331
    #AndroidSummit 46

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  52. This Is Pretty Close To MVP, With
    One New Benefit
    @AdamMc331
    #AndroidSummit 52

    View Slide

  53. Since ViewModel Doesn't Reference
    View, We Can Leverage Android
    ViewModel To Outlast Config
    Changes
    @AdamMc331
    #AndroidSummit 53

    View Slide

  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

    View Slide

  55. Handle Rotation In MVP
    class TaskListContract {
    interface Presenter {
    // New:
    fun getState(): Bundle
    fun restoreState(bundle: Bundle?)
    }
    }
    @AdamMc331
    #AndroidSummit 55

    View Slide

  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

    View Slide

  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

    View Slide

  58. Handle Rotation In MVVM
    class TaskListViewModel(
    private val repository: TaskRepository
    ) : ViewModel() {
    // ...
    }
    @AdamMc331
    #AndroidSummit 58

    View Slide

  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

    View Slide

  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

    View Slide

  61. Where Does MVVM Fall Short?
    @AdamMc331
    #AndroidSummit 61

    View Slide

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

    View Slide

  63. Let's Consider A More Complicated State
    sealed class TaskListState {
    object Loading : TaskListState()
    data class Loaded(val tasks: List) : TaskListState()
    data class Error(val error: Throwable?) : TaskListState()
    }
    @AdamMc331
    #AndroidSummit 63

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  69. Model-View-Intent
    @AdamMc331
    #AndroidSummit 69

    View Slide

  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

    View Slide

  71. The First Goal Is To Make Our State
    Changes Predictable
    @AdamMc331
    #AndroidSummit 71

    View Slide

  72. We Achieve This With A Reducer
    abstract class Reducer {
    abstract fun reduce(action: Action, state: State): State
    }
    @AdamMc331
    #AndroidSummit 72

    View Slide

  73. Clearly Defined Inputs And Outputs
    class TaskListReducer : Reducer() {
    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

    View Slide

  74. We Also Want A Single Source Of
    Truth
    @AdamMc331
    #AndroidSummit 74

    View Slide

  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

    View Slide

  76. Store Implementation
    class BaseStore(
    initialState: S,
    private val reducer: Reducer
    ) {
    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

    View Slide

  77. Store Implementation
    class BaseStore(
    initialState: S,
    private val reducer: Reducer
    ) {
    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

    View Slide

  78. Store Implementation
    class BaseStore(
    initialState: S,
    private val reducer: Reducer
    ) {
    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

    View Slide

  79. Store Implementation
    class BaseStore(
    initialState: S,
    private val reducer: Reducer
    ) {
    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

    View Slide

  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

    View Slide

  81. Hook This Up To Our ViewModel/Presenter
    class TaskListViewModel(private val repository: TaskRepository) : ViewModel() {
    private val store: BaseStore = 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

    View Slide

  82. Hook This Up To Our ViewModel/Presenter
    class TaskListViewModel(private val repository: TaskRepository) : ViewModel() {
    private val store: BaseStore = 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

    View Slide

  83. Hook This Up To Our ViewModel/Presenter
    class TaskListViewModel(private val repository: TaskRepository) : ViewModel() {
    private val store: BaseStore = 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

    View Slide

  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

    View Slide

  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

    View Slide

  86. What Should I Take Away From
    This?
    @AdamMc331
    #AndroidSummit 86

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  91. What's Most Important
    • Be consistent
    @AdamMc331
    #AndroidSummit 91

    View Slide

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

    View Slide