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. You May Have Heard These Buzzwords: • MVC • MVP

    • MVVM • MVI • MVU?? @AdamMc331 #AndroidSummit 2
  2. Why Not? • Not readable • Difficult to add new

    code • Difficult to change existing code • Can't write Junit tests for this @AdamMc331 #AndroidSummit 12
  3. 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
  4. Model • This is your data source • Database, remote

    server, etc • It does not care about the view @AdamMc331 #AndroidSummit 16
  5. 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
  6. Controller • Handles user inputs • Validates if necessary •

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

    Patterns @AdamMc331 #AndroidSummit 19
  8. Why Do We Have So Many Options For This Third

    Component? @AdamMc331 #AndroidSummit 22
  9. 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
  10. Model-View-Presenter • Similar to the last pattern • Moves our

    presentation logic out of the Activity class @AdamMc331 #AndroidSummit 30
  11. Why Is This Better? • UI logic is outside of

    the Activity, and now supports Junit tests • Our concerns are separated again @AdamMc331 #AndroidSummit 32
  12. 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
  13. 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
  14. Model class InMemoryTaskService : TaskListContract.Model { override fun getTasks(): List<Task>

    { return listOf( Task("Sample task 1"), Task("Sample task 2") ) } } @AdamMc331 #AndroidSummit 36
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. Model Doesn't Change (much) interface TaskRepository { fun getTasks(): List<Task>

    } class InMemoryTaskService : TaskRepository { override fun getTasks(): List<Task> { return listOf(...) } } @AdamMc331 #AndroidSummit 46
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. Since ViewModel Doesn't Reference View, We Can Leverage Android ViewModel

    To Outlast Config Changes @AdamMc331 #AndroidSummit 53
  27. 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
  28. Handle Rotation In MVP class TaskListContract { interface Presenter {

    // New: fun getState(): Bundle fun restoreState(bundle: Bundle?) } } @AdamMc331 #AndroidSummit 55
  29. 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
  30. 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
  31. Handle Rotation In MVVM class TaskListViewModel( private val repository: TaskRepository

    ) : ViewModel() { // ... } @AdamMc331 #AndroidSummit 58
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. We Achieve This With A Reducer abstract class Reducer {

    abstract fun reduce(action: Action, state: State): State } @AdamMc331 #AndroidSummit 72
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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