MV–WTF?

 MV–WTF?

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

A2ca9534b32c7dbf57b3142ad9884f15?s=128

Kevin Carpenter

April 30, 2016
Tweet

Transcript

  1. MV–WTF? Kevin Carpenter Notion AI

  2. Pull to Refresh List<Items>

  3. Entities Use Cases Controllers Gateways Presenters Devices Web DB UI

    External Interfaces Enterprise Business Rules Interface Adapters Frameworks & Drivers Application Business Rules
  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” }
  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 )
  6. M is for Model Entities Use Cases Controllers Gateways Presenters

    Devices Web DB UI External Interfaces interface Model {
 fun getItems(): Observable<List<Item>>
 }
  7. V is for View Devices Web DB UI External Interfaces

  8. WTF is for… Controllers Presenters Devices Web DB UI External

    Interfaces Presentation Models model.getItems()
  9. MV–Why?

  10. Single Responsibility Principle

  11. –“Uncle” Bob Martin “Gather together the things that change for

    the same reasons. Separate those things that change for different reasons.”
  12. Testing Unit

  13. Testing JU Unit Integration

  14. Testing JU Unit Integration UI

  15. MV-How?

  16. Separated Presentation

  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.”
  18. MVC “Supervising Controller” User Controller View Model

  19. class ItemListViewImpl :
 RecyclerView, ItemListView, OnRefreshListener {
 // lifecycle, constructor,

    etc…
 
 // OnRefreshListener implementation
 override fun onRefresh() {
 setLoadingAnimation(true)
 controller.refreshItems()
 }
 } User Controller View Model
  20. User Controller View Model class ItemListViewImpl :
 RecyclerView, ItemListView, OnRefreshListener

    {
 // lifecycle, constructor, etc…
 
 // OnRefreshListener implementation
 override fun onRefresh() {
 setLoadingAnimation(true)
 controller.refreshItems()
 }
 }
  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
  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() }
 }
 }
  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
  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
  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
  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
  27. Testing MVC class TestViewImpl(private val model: Model) : IItemListView {


    var items = emptyList<Item>() var loading = true 
 override fun notifyUpdated() {
 this.items = model.getItems() this.loading = false
 }
 }
 
 class ModelImpl(private val items: List<Item>): Model {
 override fun fetchItems(): ListItems {
 return Observable.just(items)
 }
 
 override fun getItems(): List<Items> {
 return items
 }
 }
  28. Testing MVC class TestViewImpl(private val model: Model) : IItemListView {


    var items = emptyList<Item>() var loading = true override fun notifyUpdated() {
 this.items = model.getItems() this.loading = false
 }
 }
 
 class ModelImpl(private val items: List<Item>): Model {
 override fun fetchItems(): ListItems {
 return Observable.just(items)
 }
 
 override fun getItems(): List<Items> {
 return items
 }
 }
  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)
 }
  30. MVC Takeaways User Controller View Model • Controller has too

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

    many responsibilities. • The View knows about the Model and the Controller.
  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.
  33. Are Fragments Controllers?

  34. MVP “Passive View” User Presenter View Model

  35. User Presenter View class ItemListView : RecyclerView,
 IItemListView, OnRefreshListener {


    
 override fun onRefresh() {
 presenter.onRefresh()
 }
 } Model
  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()
 }
 }
  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()
 }
 }
  38. User Presenter View Model interface IItemListView {
 fun setLoading(loading: Boolean)


    fun setItems(items: List<Item>)
 } 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 }
 }
  39. User Presenter View Model interface IItemListView {
 fun setLoading(loading: Boolean)


    fun setItems(items: List<Item>)
 } 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 }
 }
  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)
 }
  41. MVP Takeaways • The Presenter has the same responsibilities as

    before. User Presenter View Model
  42. MVP Takeaways User Presenter View Model • The Presenter has

    the same responsibilities as before. • The View no longer knows about the Model.
  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.
  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.
  45. Presentation Model

  46. MVP View stores its own state User Presenter View Model

    class ItemListView : RecyclerView,
 IItemListView, OnRefreshListener {
 
 private var items: List<Items> = emptyList()
 private var loading: Boolean = false
 
 override fun setItems(newItems: List<Items>) {
 this.items = newItems
 // update items
 }
 
 override fun setLoading(loading: Boolean) {
 this.loading = loading
 // show or hide progress bar
 }
 }
  47. MVP View stores its own state User Presenter View Model

    class ItemListView : RecyclerView,
 IItemListView, OnRefreshListener {
 
 private var items: List<Items> = emptyList()
 private var loading: Boolean = false
 
 override fun onSaveInstanceState(): Parcelable {
 // ☠☠☠☠☠☠☠☠
 }
 
 override fun setItems(newItems: List<Items>) {
 this.items = newItems
 }
 
 override fun setLoading(loading: Boolean) {
 this.loading = loading
 }
 }
  48. Presentation Model stores state User PM View Model class ItemListViewModel

    {
 private var model: Model
 
 private var loading: Boolean
 private var items: List<Item>
 
 fun onRefresh() {
 loading = true
 items = model.getItems()
 // communicate to View
 }
 }
  49. MVVM Model-View-ViewModel User ViewModel View Model

  50. User ViewModel View Model class ItemListView: RecyclerView, OnRefreshListener { override

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

    fun onRefresh() {
 viewModel.onRefresh()
 }
 }
  52. User ViewModel View class ItemListViewModel {
 private var model: Model


    
 private var loadingSubject = PublishSubject.create<Boolean private var itemsSubject = PublishSubject.create<List<Item 
 fun onRefresh() {
 loadingSubject.onNext(true)
 model.getItems()
 .subscribe(
 { items -> itemsSubject.onNext(items) }, // onNext { loadingSubject.onNext(false) }, // onError
 { loadingSubject.onNext(false) }) // onComplete
 }
 } Model
  53. User ViewModel View Model class ItemListViewModel {
 private var model:

    Model
 
 private var loadingSubject = PublishSubject.create<Boolean private var itemsSubject = PublishSubject.create<List<Item 
 fun onRefresh() {
 loadingSubject.onNext(true)
 model.getItems()
 .subscribe(
 { items -> itemsSubject.onNext(items) }, // onNext { loadingSubject.onNext(false) }, // onError
 { loadingSubject.onNext(false) }) // onComplete
 }
 }
  54. User ViewModel View class ItemListView {
 
 override fun onAttachedToWindow()

    {
 super.onAttachedToWindow()
 
 viewModel.loadingSubject
 .subscribe(loadingObserver)
 
 viewModel.itemsSubject
 .subscribe(itemsObserver)
 }
 } Model
  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<Item> ->
 adapter.items = items
 }
 } Model
  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<Item>()
 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)
 }
  57. MVVM Takeaways • The ViewModel is still the “middle man”

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

    • Clearly defined code paths User ViewModel View Model
  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
  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
  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)
 }
 
 }
  62. Data Binding <layout xmlns:android="http://schemas.android.com/apk/res/android">
 <data>
 <variable name="user" type=“devfest.example.User”/>
 </data>
 <LinearLayout>


    
 <TextView android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text=“@{user.firstName}"
 />
 
 <TextView android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text=“@{user.lastName}"
 />
 </LinearLayout>
 </layout>
  63. Now what?

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

  65. Sources • External read effects time

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

    ☝ ☝
  67. Sources • External read effects • User input • E.g.

    Pull to Refresh Refresh Events time
  68. Sinks • Write effects to external world time

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

    output time
  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
  71. Cycle • UIs are cycles Source: What if the user

    was a function? by Andre Staltz at JSConf Budapest 2015 ☝
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  84. RPM Reactive Presentation Model User Reactive Presentation Model Model View

  85. interface Model<I : Sources, O : Sinks> {
 fun setUp(sources:

    I): O
 
 fun tearDown()
 
 interface Sources
 
 interface Sinks
 } User Reactive Presentation Model Model View
  86. class Sources(
 val refreshEvents: Observable<Unit>) : Sources User Reactive Presentation

    Model Model View
  87. class Sinks(
 val items: Observable<List<Item>>,
 val loading: Observable<Boolean>) : Sinks

    User Reactive Presentation Model Model View
  88. class ItemListViewModel : Model<Sources, Sinks> {
 class Sources(
 val refreshEvents:

    Observable<Unit>) : Sources
 
 class Sinks(
 val items: Observable<List<Item>>,
 val loading: Observable<Boolean>) : Sinks
 } User Reactive Presentation Model Model View
  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<Unit>()
 
 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
  90. Clean News github.com/pardom/CleanNews

  91. class ItemListViewModel : Model<Sources, Sinks> {
 
 class Sources(
 val

    itemUrlClicks: Observable<Item>,
 val itemDetailClicks: Observable<Item>) : Model.Sources
 
 class Sinks(val items: Observable<List<Item>>) : Model.Sinks
 }

  92. class ItemDetailViewModel : Model<Sources, Sinks> {
 class Sources(
 val itemUrlClicks:

    Observable<Item>,
 val itemDetailClicks: Observable<Item>) : Model.Sources
 
 class Sinks(val items: Observable<List<Item>>) : Model.Sinks
 }
  93. class ItemDetailViewModel : Model<Sources, Sinks> {
 
 class Sources(
 val

    backClicks: Observable<Unit>,
 val shareClicks: Observable<Unit>) : Model.Sources
 
 class Sinks(
 val item: Observable<Item>,
 val children: Observable<List<Item>>) : Model.Sinks
 }
  94. What should I use?

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