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

MV–WTF?

 MV–WTF?

A survey of presentation architectures, focused on options for Android.

Kevin Carpenter

April 30, 2016
Tweet

More Decks by Kevin Carpenter

Other Decks in Programming

Transcript

  1. MV–WTF?
    Kevin Carpenter
    Notion AI

    View Slide

  2. Pull to Refresh
    List

    View Slide

  3. Entities
    Use Cases
    Controllers
    Gateways
    Presenters
    Devices
    Web
    DB UI
    External
    Interfaces
    Enterprise Business Rules
    Interface Adapters
    Frameworks & Drivers
    Application Business Rules

    View Slide

  4. M is for Model
    Entities
    Use Cases
    Controllers
    Gateways
    Presenters
    Devices
    Web
    DB UI
    External
    Interfaces
    GET https://hacker-news.com/item/11595667.json
    {
    "by": "jamesblonde",
    "descendants": 0,
    "id": 11595667,
    "score": 2,
    "time": 1461937655,
    "title": “Make email great again”,
    "type": "story",
    "url": “https:\/\/email.net/great”
    }

    View Slide

  5. M is for Model
    Entities
    Use Cases
    Controllers
    Gateways
    Presenters
    Devices
    Web
    DB UI
    External
    Interfaces
    {
    "id": 11595667,
    "by": "jamesblonde",
    "descendants": 0,
    "score": 2,
    "time": 1461937655,
    "title": "Successful way to stop spam email from ResearchGate",
    "type": "story",
    "url": "https:\/\/www.researchgate.net\/post"
    }
    CREATE TABLE IF NOT EXISTS ItemTable(
    id TEXT PRIMARY KEY NOT NULL,
    by TEXT NOT NULL,
    descendants INTEGER NOT NULL,
    score INTEGER NOT NULL,
    time INTEGER NOT NULL,
    title TEXT NOT NULL,
    type TEXT NOT NULL,
    url TEXT NOT NULL
    )

    View Slide

  6. M is for Model
    Entities
    Use Cases
    Controllers
    Gateways
    Presenters
    Devices
    Web
    DB UI
    External
    Interfaces
    interface Model {

    fun getItems(): Observable>

    }

    View Slide

  7. V is for View
    Devices
    Web
    DB UI
    External
    Interfaces

    View Slide

  8. WTF is for…
    Controllers
    Presenters
    Devices
    Web
    DB UI
    External
    Interfaces
    Presentation
    Models
    model.getItems()

    View Slide

  9. MV–Why?

    View Slide

  10. Single Responsibility Principle

    View Slide

  11. –“Uncle” Bob Martin
    “Gather together the things that change for the same reasons.
    Separate those things that change for different reasons.”

    View Slide

  12. Testing
    Unit

    View Slide

  13. Testing
    JU
    Unit
    Integration

    View Slide

  14. Testing
    JU
    Unit
    Integration
    UI

    View Slide

  15. MV-How?

    View Slide

  16. Separated Presentation

    View Slide

  17. –Martin Fowler
    “Ensure that any code that manipulates presentation only
    manipulates presentation, pushing all domain and data source logic
    into clearly separated areas of the program.”

    View Slide

  18. MVC
    “Supervising Controller”
    User
    Controller
    View
    Model

    View Slide

  19. class ItemListViewImpl :

    RecyclerView, ItemListView, OnRefreshListener {

    // lifecycle, constructor, etc…


    // OnRefreshListener implementation

    override fun onRefresh() {

    setLoadingAnimation(true)

    controller.refreshItems()

    }

    }
    User
    Controller
    View
    Model

    View Slide

  20. User
    Controller
    View
    Model
    class ItemListViewImpl :

    RecyclerView, ItemListView, OnRefreshListener {

    // lifecycle, constructor, etc…


    // OnRefreshListener implementation

    override fun onRefresh() {

    setLoadingAnimation(true)

    controller.refreshItems()

    }

    }

    View Slide

  21. class Controller() {


    @Inject

    private lateinit var model: Model


    private var view: IItemListView? = null


    fun refreshItems() {

    model.updateItems()

    .subscribe(

    { view?.notifyUpdated() },

    { view?.notifyError() })

    }

    }
    User
    Controller
    View
    Model

    View Slide

  22. User
    Controller
    View
    Model
    class Controller() {


    @Inject

    private lateinit var model: Model


    private var view: IItemListView? = null


    fun refreshItems() {

    model.updateItems()

    .subscribe { view?.notifyUpdated() }

    }

    }

    View Slide

  23. class Controller() {


    @Inject

    private lateinit var model: Model


    private var view: IItemListView? = null


    fun refreshItems() {

    model.updateItems()

    .subscribe { view?.notifyUpdated() }

    }

    }
    User
    Controller
    View
    Model

    View Slide

  24. class ItemListViewImpl :

    RecyclerView, ItemListView, OnRefreshListener {

    // lifecycle, constructor, etc…


    // OnRefreshListener implementation

    override fun onRefresh() {/*...*/}


    fun notifyUpdated() {

    val items = model.getItems()

    adapter.items = items

    setLoadingAnimation(false)

    }

    }
    User
    Controller
    View
    Model

    View Slide

  25. class ItemListViewImpl :

    RecyclerView, ItemListView, OnRefreshListener {

    // lifecycle, constructor, etc…


    // OnRefreshListener implementation

    override fun onRefresh() {/*...*/}


    fun notifyUpdated() {

    val items = model.getItems()

    adapter.items = items

    setLoadingAnimation(false)

    }

    }
    User
    Controller
    View
    Model

    View Slide

  26. class ItemListViewImpl :

    RecyclerView, ItemListView, OnRefreshListener {

    // lifecycle, constructor, etc…


    // OnRefreshListener implementation

    override fun onRefresh() {/*...*/}


    fun notifyUpdated() {

    val items = model.getItems()

    adapter.items = items

    setLoadingAnimation(false)

    }

    }
    User
    Controller
    View
    Model

    View Slide

  27. Testing MVC
    class TestViewImpl(private val model: Model) : IItemListView {

    var items = emptyList()
    var loading = true

    override fun notifyUpdated() {

    this.items = model.getItems()
    this.loading = false

    }

    }


    class ModelImpl(private val items: List): Model {

    override fun fetchItems(): ListItems {

    return Observable.just(items)

    }


    override fun getItems(): List {

    return items

    }

    }

    View Slide

  28. Testing MVC
    class TestViewImpl(private val model: Model) : IItemListView {

    var items = emptyList()
    var loading = true
    override fun notifyUpdated() {

    this.items = model.getItems()
    this.loading = false

    }

    }


    class ModelImpl(private val items: List): Model {

    override fun fetchItems(): ListItems {

    return Observable.just(items)

    }


    override fun getItems(): List {

    return items

    }

    }

    View Slide

  29. Testing MVC
    @Test

    fun test_getItemsError_notifiesError() {

    val items = ItemListFactory.makeList()

    val model = ModelImpl(items)

    val view = TestViewImpl()

    val controller = ListController(model, view)


    controller.refreshItems() // assume blocking

    assertTrue(view.items = items)

    assertFalse(view.loading)

    }

    View Slide

  30. MVC Takeaways
    User
    Controller
    View
    Model
    • Controller has too many
    responsibilities.

    View Slide

  31. MVC Takeaways
    User
    Controller
    View
    Model
    • Controller has too many
    responsibilities.
    • The View knows about the Model and
    the Controller.

    View Slide

  32. MVC Takeaways
    User
    Controller
    View
    Model
    • Controller has too many
    responsibilities.
    • The View knows about the Model and
    the Controller.
    • Testing could be easier.

    View Slide

  33. Are Fragments Controllers?

    View Slide

  34. MVP
    “Passive View”
    User
    Presenter
    View
    Model

    View Slide

  35. User
    Presenter
    View
    class ItemListView : RecyclerView,

    IItemListView, OnRefreshListener {


    override fun onRefresh() {

    presenter.onRefresh()

    }

    }
    Model

    View Slide

  36. Model
    User
    Presenter
    View
    class ItemListView : RecyclerView,

    IItemListView, OnRefreshListener {


    override fun onAttachedToWindow() {

    super.onAttachedToWindow()

    presenter.attach(this)

    }


    override fun onDetachedFromWindow() {

    presenter.detach()

    super.onDetachedFromWindow()

    }


    override fun onRefresh() {

    presenter.onRefresh()

    }

    }

    View Slide

  37. Model
    User
    Presenter
    View
    class ItemListView : RecyclerView,

    IItemListView, OnRefreshListener {


    override fun onAttachedToWindow() {

    super.onAttachedToWindow()

    presenter.attach(this)

    }


    override fun onDetachedFromWindow() {

    presenter.detach()

    super.onDetachedFromWindow()

    }


    override fun onRefresh() {

    presenter.onRefresh()

    }

    }

    View Slide

  38. User
    Presenter
    View
    Model
    interface IItemListView {

    fun setLoading(loading: Boolean)

    fun setItems(items: List)

    }
    class ItemListViewPresenter {

    fun attach(view: IItemListView) {

    this.view = view

    }


    fun detach() {

    this.view = null

    }


    fun onRefresh() {

    view?.setLoading(true)

    model.getItems()

    .subscribe(

    { items -> view?.setItems(items) },//onNext

    { view?.setLoading(false) }, //onError

    { view?.setLoading(false) })//onComplete
    }

    }

    View Slide

  39. User
    Presenter
    View
    Model
    interface IItemListView {

    fun setLoading(loading: Boolean)

    fun setItems(items: List)

    }
    class ItemListViewPresenter {

    fun attach(view: IItemListView) {

    this.view = view

    }


    fun detach() {

    this.view = null

    }


    fun onRefresh() {

    view?.setLoading(true)

    model.getItems()

    .subscribe(

    { items -> view?.setItems(items) },//onNext

    { view?.setLoading(false) }, //onError

    { view?.setLoading(false) })//onComplete
    }

    }

    View Slide

  40. Testing MVP
    @Test

    fun test_refreshItems_setsViewItems() {

    val items = ItemListFactory.makeList()

    val model = mock(Model::class.java)

    mockWhen(model.getItems()).thenReturn(Observable.just(items))


    val view = mock(IItemListView::class.java)

    val presenter = ItemListViewPresenter(model, view)


    presenter.refreshItems() // assume blocking

    verify(view).setItems(items)

    verify(view).setLoading(false)

    }

    View Slide

  41. MVP Takeaways
    • The Presenter has the same
    responsibilities as before.
    User
    Presenter
    View
    Model

    View Slide

  42. MVP Takeaways
    User
    Presenter
    View
    Model
    • The Presenter has the same
    responsibilities as before.
    • The View no longer knows about the
    Model.

    View Slide

  43. MVP Takeaways
    User
    Presenter
    View
    Model
    • Presenter has the same responsibilities
    as before.
    • View no longer knows about the Model.
    • Presenter is in full control of what is
    rendered in View.

    View Slide

  44. MVP Takeaways
    User
    Presenter
    View
    Model
    • Presenter has the same responsibilities
    as before.
    • View no longer knows about the Model.
    • Presenter is in full control of what is
    rendered in View.
    • Testing is still tough. We need to
    thoroughly mock IItemListView to unit
    test.

    View Slide

  45. Presentation Model

    View Slide

  46. MVP View stores its own state
    User
    Presenter
    View
    Model
    class ItemListView : RecyclerView,

    IItemListView, OnRefreshListener {


    private var items: List = emptyList()

    private var loading: Boolean = false


    override fun setItems(newItems: List) {

    this.items = newItems

    // update items

    }


    override fun setLoading(loading: Boolean) {

    this.loading = loading

    // show or hide progress bar

    }

    }

    View Slide

  47. MVP View stores its own state
    User
    Presenter
    View
    Model
    class ItemListView : RecyclerView,

    IItemListView, OnRefreshListener {


    private var items: List = emptyList()

    private var loading: Boolean = false


    override fun onSaveInstanceState(): Parcelable {

    // ☠☠☠☠☠☠☠☠

    }


    override fun setItems(newItems: List) {

    this.items = newItems

    }


    override fun setLoading(loading: Boolean) {

    this.loading = loading

    }

    }

    View Slide

  48. Presentation Model stores state
    User
    PM
    View
    Model
    class ItemListViewModel {

    private var model: Model


    private var loading: Boolean

    private var items: List


    fun onRefresh() {

    loading = true

    items = model.getItems()

    // communicate to View

    }

    }

    View Slide

  49. MVVM
    Model-View-ViewModel
    User
    ViewModel
    View
    Model

    View Slide

  50. User
    ViewModel
    View
    Model
    class ItemListView: RecyclerView, OnRefreshListener {
    override fun onRefresh() {

    viewModel.onRefresh()

    }

    }

    View Slide

  51. Model
    User
    ViewModel
    View
    class ItemListView: RecyclerView, OnRefreshListener {
    override fun onRefresh() {

    viewModel.onRefresh()

    }

    }

    View Slide

  52. User
    ViewModel
    View
    class ItemListViewModel {

    private var model: Model


    private var loadingSubject = PublishSubject.createprivate var itemsSubject = PublishSubject.create

    fun onRefresh() {

    loadingSubject.onNext(true)

    model.getItems()

    .subscribe(

    { items -> itemsSubject.onNext(items) }, // onNext
    { loadingSubject.onNext(false) }, // onError

    { loadingSubject.onNext(false) }) // onComplete

    }

    }
    Model

    View Slide

  53. User
    ViewModel
    View
    Model
    class ItemListViewModel {

    private var model: Model


    private var loadingSubject = PublishSubject.createprivate var itemsSubject = PublishSubject.create

    fun onRefresh() {

    loadingSubject.onNext(true)

    model.getItems()

    .subscribe(

    { items -> itemsSubject.onNext(items) }, // onNext
    { loadingSubject.onNext(false) }, // onError

    { loadingSubject.onNext(false) }) // onComplete

    }

    }

    View Slide

  54. User
    ViewModel
    View
    class ItemListView {


    override fun onAttachedToWindow() {

    super.onAttachedToWindow()


    viewModel.loadingSubject

    .subscribe(loadingObserver)


    viewModel.itemsSubject

    .subscribe(itemsObserver)

    }

    }
    Model

    View Slide

  55. User
    ViewModel
    View
    class ItemListView {


    private val loadingObserver = { loading: Boolean ->

    progressBar.visibility = if (loading) {

    View.VISIBLE

    }

    else {

    View.GONE

    }

    }


    private val itemsObserver = { items: List ->

    adapter.items = items

    }

    }
    Model

    View Slide

  56. Testing MVVM
    @Test

    fun test_getItems_updatesItems() {

    val items = ItemListFactory.makeList()

    val model = mock(Model::class.java)

    mockWhen(model.getItems()).thenReturn(Observable.just(items))

    val viewModel = ItemListViewModel(model)


    var updatedItems = emptyList()

    val itemsSubscriber = { newItems -> items = newItems }

    var loading = true

    val loadingSubscriber = { loadingUpdate -> loading = loadingUpdate}


    viewModel.itemsSubject.subscribe(itemsSubscriber)

    viewModel.loadingSubject.subscribe(loadingSubscriber)


    viewModel.refreshItems() // assume blocking

    assertEquals(items, updatedItems)

    assertFalse(loading)

    }

    View Slide

  57. MVVM Takeaways
    • The ViewModel is still the “middle man”
    User
    ViewModel
    View
    Model

    View Slide

  58. MVVM Takeaways
    • The ViewModel is still the “middle man”
    • Clearly defined code paths
    User
    ViewModel
    View
    Model

    View Slide

  59. MVVM Takeaways
    • The ViewModel is still the “middle man”
    • Clearly defined code paths
    • The View observes changes in the
    ViewModel
    User
    ViewModel
    View
    Model

    View Slide

  60. MVVM Takeaways
    • The ViewModel is still the “middle man”
    • Clearly defined code paths
    • The View observes changes in the
    ViewModel
    • Unit testing is improved!
    User
    ViewModel
    View
    Model

    View Slide

  61. Data Binding
    class User(

    var firstName: String,

    var lastName: String)
    class UserView {

    private val userBinding


    constructor(/*...*/) {

    val inflater = LayoutInflater.from(context)

    userBinding = UserViewBinding.inflate(inflater)

    }


    fun updateUser(newUser: User) {

    userBinding.update(newUser)

    }


    }

    View Slide

  62. Data Binding






    android:layout_height="wrap_content"

    android:text=“@{user.firstName}"

    />


    android:layout_height="wrap_content"

    android:text=“@{user.lastName}"

    />



    View Slide

  63. Now what?

    View Slide

  64. Cycle.js
    http://cycle.js.org/

    View Slide

  65. Sources
    • External read effects
    time

    View Slide

  66. Sources
    • External read effects
    • User input
    time
    ☝ ☝ ☝

    View Slide

  67. Sources
    • External read effects
    • User input
    • E.g. Pull to Refresh
    Refresh Events
    time

    View Slide

  68. Sinks
    • Write effects to external world
    time

    View Slide

  69. Sinks
    • Write effects to external world
    • Data model output
    time

    View Slide

  70. Sinks
    • Write effects to external world
    • Data model output
    • E.g. formatted data response, like our
    list of Items
    Item List Update Events
    time

    View Slide

  71. Cycle
    • UIs are cycles
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015


    View Slide

  72. Cycle
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    • UIs are cycles
    • UIs are functions
    Pull to Refresh
    List Updates
    ViewModel: f(refresh) = list updates

    View Slide

  73. Cycle
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    • UIs are cycles
    • UIs are functions
    Pull to Refresh
    List Updates
    ViewModel: f(refresh) = list updates

    View Slide

  74. Cycle
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    • UIs are cycles
    • UIs are functions
    Pull to Refresh
    List Updates
    ViewModel: f(refresh) = list updates

    View Slide

  75. Cycle
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    • UIs are cycles
    • UIs are functions
    Pull to Refresh
    List Updates
    ViewModel: f(refresh) = list updates

    View Slide

  76. Cycle
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    • UIs are cycles
    • UIs are functions
    • UIs are asynchronous
    Pull to Refresh
    List Updates
    ViewModel: f(refresh) = list updates

    View Slide

  77. Cycle
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    • UIs are cycles
    • UIs are functions
    • UIs are asynchronous
    Pull to Refresh
    List Updates
    ViewModel: f(refresh) = list updates

    View Slide

  78. Cycle
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    • UIs are cycles
    • UIs are functions
    • UIs are asynchronous
    Pull to Refresh
    List Updates
    ViewModel: f(refresh) = list updates

    View Slide

  79. Cycle
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    • UIs are cycles
    • UIs are functions
    • UIs are asynchronous
    Pull to Refresh
    List Updates
    ViewModel: f(refresh) = list updates

    View Slide

  80. Cycle
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    • UIs are cycles
    • UIs are functions
    • UIs are asynchronous
    Pull to Refresh
    List Updates
    ViewModel: f(refresh) = list updates

    View Slide

  81. Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    Pull to Refresh
    List Updates
    ViewModel: f(refresh) = list updates
    • UIs are cycles
    • UIs are functions
    • UIs are asynchronous
    Cycle

    View Slide

  82. Cycle
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    • UIs are cycles
    • UIs are functions
    • UIs are asynchronous
    • UIs are symmetric
    ViewModel: f(refresh) = list updates



    View Slide

  83. Cycle
    • UIs are cycles
    • UIs are functions
    • UIs are asynchronous
    • UIs are symmetric
    • The user is a function
    Source: What if the user was a function? by Andre Staltz at JSConf Budapest 2015
    ViewModel: f(refresh) = list updates



    View Slide

  84. RPM
    Reactive Presentation Model
    User
    Reactive
    Presentation
    Model
    Model
    View

    View Slide

  85. interface Model {

    fun setUp(sources: I): O


    fun tearDown()


    interface Sources


    interface Sinks

    }
    User
    Reactive
    Presentation
    Model
    Model
    View

    View Slide

  86. class Sources(

    val refreshEvents: Observable) : Sources
    User
    Reactive
    Presentation
    Model
    Model
    View

    View Slide

  87. class Sinks(

    val items: Observable>,

    val loading: Observable) : Sinks
    User
    Reactive
    Presentation
    Model
    Model
    View

    View Slide

  88. class ItemListViewModel : Model {

    class Sources(

    val refreshEvents: Observable) : Sources


    class Sinks(

    val items: Observable>,

    val loading: Observable) : Sinks

    }
    User
    Reactive
    Presentation
    Model
    Model
    View

    View Slide

  89. class ItemListView : RecyclerView, OnRefreshListener {


    @Inject

    private lateinit var model: ItemListViewModel


    private val progressBar //...

    private val adapter = ItemListViewAdapter()

    private val subscriptions = CompositeSubscription()

    private val refreshSubject = PublishSubject.create()


    override fun onAttachedToWindow() {

    super.onAttachedToWindow()

    val sinks = model.setUp(Sources(refreshSubject.asObservable()))

    sinks.loading.subscribe(loadingObserver).addToSubscriptions()

    sinks.items.subscribe(itemsObserver).addTo(subscriptions)

    }

    }
    User
    Reactive
    Presentation
    Model
    Model
    View

    View Slide

  90. Clean News
    github.com/pardom/CleanNews

    View Slide

  91. class ItemListViewModel : Model {


    class Sources(

    val itemUrlClicks: Observable,

    val itemDetailClicks: Observable) : Model.Sources


    class Sinks(val items: Observable>) : Model.Sinks

    }


    View Slide

  92. class ItemDetailViewModel : Model {

    class Sources(

    val itemUrlClicks: Observable,

    val itemDetailClicks: Observable) : Model.Sources


    class Sinks(val items: Observable>) : Model.Sinks

    }

    View Slide

  93. class ItemDetailViewModel : Model {


    class Sources(

    val backClicks: Observable,

    val shareClicks: Observable) : Model.Sources


    class Sinks(

    val item: Observable,

    val children: Observable>) : Model.Sinks

    }

    View Slide

  94. What should I use?

    View Slide

  95. Questions?
    @kevcarpenter | www.kevcar.me/mvwtf

    View Slide