Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

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. Entities Use Cases Controllers Gateways Presenters Devices Web DB UI

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

    Devices Web DB UI External Interfaces interface Model {
 fun getItems(): Observable<List<Item>>
 }
  5. WTF is for… Controllers Presenters Devices Web DB UI External

    Interfaces Presentation Models model.getItems()
  6. –“Uncle” Bob Martin “Gather together the things that change for

    the same reasons. Separate those things that change for different reasons.”
  7. –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.”
  8. class ItemListViewImpl :
 RecyclerView, ItemListView, OnRefreshListener {
 // lifecycle, constructor,

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

    {
 // lifecycle, constructor, etc…
 
 // OnRefreshListener implementation
 override fun onRefresh() {
 setLoadingAnimation(true)
 controller.refreshItems()
 }
 }
  10. 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
  11. User Controller View Model class Controller() {
 
 @Inject
 private

    lateinit var model: Model
 
 private var view: IItemListView? = null
 
 fun refreshItems() {
 model.updateItems()
 .subscribe { view?.notifyUpdated() }
 }
 }
  12. class Controller() {
 
 @Inject
 private lateinit var model: Model


    
 private var view: IItemListView? = null
 
 fun refreshItems() {
 model.updateItems()
 .subscribe { view?.notifyUpdated() }
 }
 } User Controller View Model
  13. 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
  14. 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
  15. 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
  16. 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
 }
 }
  17. 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
 }
 }
  18. 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)
 }
  19. MVC Takeaways User Controller View Model • Controller has too

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

    many responsibilities. • The View knows about the Model and the Controller. • Testing could be easier.
  21. User Presenter View class ItemListView : RecyclerView,
 IItemListView, OnRefreshListener {


    
 override fun onRefresh() {
 presenter.onRefresh()
 }
 } Model
  22. 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()
 }
 }
  23. 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()
 }
 }
  24. 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 }
 }
  25. 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 }
 }
  26. 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)
 }
  27. MVP Takeaways User Presenter View Model • The Presenter has

    the same responsibilities as before. • The View no longer knows about the Model.
  28. 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.
  29. 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.
  30. 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
 }
 }
  31. 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
 }
 }
  32. 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
 }
 }
  33. 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
  34. 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
 }
 }
  35. User ViewModel View class ItemListView {
 
 override fun onAttachedToWindow()

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

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

    • Clearly defined code paths • The View observes changes in the ViewModel User ViewModel View Model
  40. 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
  41. 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)
 }
 
 }
  42. 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>
  43. Sinks • Write effects to external world • Data model

    output • E.g. formatted data response, like our list of Items Item List Update Events time
  44. Cycle • UIs are cycles Source: What if the user

    was a function? by Andre Staltz at JSConf Budapest 2015 ☝
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. interface Model<I : Sources, O : Sinks> {
 fun setUp(sources:

    I): O
 
 fun tearDown()
 
 interface Sources
 
 interface Sinks
 } User Reactive Presentation Model Model View
  58. 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
  59. 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
  60. 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
 }

  61. 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
 }
  62. 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
 }