for displaying data/updating UI state Presenter has calls for responding to UI events and acting upon them interface MvpView interface MvpPresenter<View: MvpView> { fun attach(view : View) fun detach() } abstract class MvpPresenterBase<View: MvpView> : MvpPresenter<View> { var view: View? = null override fun attach(view: View) { this.view = view } override fun detach() { this.view = null } }
the View and the Presenter and the "contract" between them. This can also include things like constants that are shared between Presenter and View. We're building a simple "increment the counter" app today, so we don't have much, just a few small items. interface IncrementActivityContract { interface IncrementActivityView : MvpView { fun setCountView(countString : String) } interface IncrementActivityPresenter : MvpPresenter<IncrementActivityView> { fun onIncrementClicked() } }
storing our state, the count of the incrementer. In onIncrementClicked callback, we increment count and update the View accordingly You'll note that view is nullable in onIncrementClicked, if there's a click without a view, we'll update our state but need to update the View later In our attach callback, we restore the view state accordingly, alternatively we could queue events class IncrementActivityPresenterImpl : MvpPresenterBase<View>(), Presenter { private var count = 0 override fun attach(view: View) { super.attach(view) view.setCountView(getCountString()) } override fun onIncrementClicked() { count++ view?.setCountView(getCountString()) } private fun getCountString() = count.toString() }
View interface. In our onCreate, we create our Presenter and initialize our view In onStart and onStop, we attach and detach our View to and from our Presenter Finally, we implement the View call for setting the count information on the view class IncrementActivityMvp : AppCompatActivity(), View { private lateinit var presenter: Presenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initPresenter() setContentView(R.layout.activity_increment) increment.setOnClickListener { presenter.onIncrementClicked() } } override fun onStart() { super.onStart() presenter.attach(this) } override fun onStop() { presenter.detach() super.onStop() } override fun setCountView(countString: String) { counter.text = countString } }
if we did not then go back and verify our logic Testing our Presenter,, we create a new instance, we attach a mocked View and then verify that the state changes correctly with each Presenter call class MvpTest { @Test fun `test increment`() { val presenterImpl = IncrementActivityPresenterImpl() val view = mock<View> {} presenterImpl.attach(view) presenterImpl.onIncrementClicked() verify(view).setCountView("0") // initial state verify(view).setCountView("1") // updated state } }
when our button is clicked. Let's update our MVP example to use RxJava and see where that lands us Initially, not much has changed here with our Contract, still the same interactions interface IncrementActivityRxContract { interface IncrementActivityRxView : MvpView { fun setCountView(countString : String) } interface IncrementActivityRxPresenter : MvpPresenter<IncrementActivityRxView> { fun onIncrementClicked() } }
roughly the same callbacks as before We've utilized RxBinding for some nicety observing click events from the view instead of adding a listener, though neither were particularly burdensome to begin with class IncrementActivityRxMvp : AppCompatActivity(), ViewRx { private lateinit var presenter: PresenterRx override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initPresenter() setContentView(R.layout.activity_increment) increment.clicks() .subscribe { presenter.onIncrementClicked() } } override fun onStart() { super.onStart() presenter.attach(this) } override fun onStop() { presenter.detach() super.onStop() } override fun setCountView(countString: String) { counter.text = countString } }
In this case, we'll now use the BehaviorRelay reactive type to store the state of our counter This allows us to observe changes in its value and update the UI accordingly, as we're doing in attach() Our onIncrementClick now just needs to update the value stored in the field and listening parties will be notified! class IncrementActivityRxPresenterImpl : MvpPresenterBaseRx<ViewRx>(), PresenterRx { private var count = BehaviorRelay.createDefault(0) override fun attach(view: ViewRx) { super.attach(view) getCountString() .autoDispose() .subscribe { view.setCountView(it) } } override fun onIncrementClicked() { count.take(1) .observeOn(Schedulers.io()) .map { it.plus(1) } .observeOn(AndroidSchedulers.mainThread()) .subscribe(count) } private fun getCountString() = count.map { it.toString() } }
an interesting place, but for our purposes this will suffice We replace our default Schedulers with some that we can control then proceed as we did previously The only real addition is the triggerActions() call to tell Rx to propagate events that are queued class MvpRxTest { val mainThreadScheduler = TestScheduler() val ioThreadScheduler = TestScheduler() init { RxAndroidPlugins.setInitMainThreadSchedulerHandler { mainThreadScheduler } RxJavaPlugins.setInitIoSchedulerHandler { ioThreadScheduler } } @Test fun `test increment`() { val presenterImpl = IncrementActivityRxPresenterImpl() val view = mock<ViewRx> {} presenterImpl.attach(view) presenterImpl.onIncrementClicked() ioThreadScheduler.triggerActions() mainThreadScheduler.triggerActions() verify(view).setCountView("0") verify(view).setCountView("1") } }
know how to update from disjointed calls ◦ Presenter owning state means complex view restoration • Handling situations with View availability ◦ Presenter is cluttered with handling for view not being available ▪ Restoring view in onAttach ▪ Managing Rx subscriptions ◦ Can recreate presenters for each Activity ▪ Makes tasks that run through configuration change more difficult ▪ Likely need to use persistence more often • Presenter becomes overly view-aware
changes • "Scoped" object that better aligns with the lifecycle of a "task" • Out of the box support in AppCompatActivity and support Fragments • A place to put complex data that is too ephemeral for sqlite but too big for onSaveInstanceState • Does NOT survive empty process state The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way.
ViewModel class and then ask for an instance from your scoped provider Providers are tied to a LifecycleOwner Scoped, usually, to Fragments or Activitys Can create own provider for purposes of dependency injection or other factory functions ViewModel data class Data(val someString: String?) class MyViewModel : ViewModel() { val data: BehaviorRelay<Data> = BehaviorRelay.createDefault(Data(null)) } ViewModelProviders .of(this) // this == Activity or Fragment .get(MyViewModel::class.java) // provider creates instance
classes to create first Abstract Presenter, you'll note the type params. This time we have a State object and a ViewModel that will act as a container for it To that end, there's an abstract ViewModel to use Finally, we have our base presenter which also implements our "reducer" queue, more on that in a bit interface MvmvpPresenter<State, out VM: MvmvpViewModel<State>> { val viewModel: VM } abstract class MvmvpViewModel<State>(initialState: State) : ViewModel() { val state = BehaviorRelay.createDefault(initialState)!! } abstract class MvmvpPresenterBase<State, out VM: MvmvpViewModel<State>>(override val viewModel: VM) : MvmvpPresenter<State, VM> { @SuppressLint("CheckResult") fun sendToViewModel(reducer: (State) -> State) { Observable.just(reducer) .observeOn(AndroidSchedulers.mainThread()) // ensures // mutations happen serially on main thread .zipWith(viewModel.state) .map { (reducer, state) -> reducer.invoke(state) } .subscribe(viewModel.state) } }
we don't care about the View! We create a contract for the Presenter and then the State that the Presenter will be mutating Finally, we create our ViewModel that will be the container for our State, we also pass in a default state interface IncrementActivityMvmvpContract { interface PresenterMvmvp : MvmvpPresenter<IncrementActivityMvmvpState, IncrementActivityStateViewModel> { fun onIncrementClicked() } data class IncrementActivityMvmvpState( var count: String = 0.toString() ) class IncrementActivityStateViewModel : MvmvpViewModel<IncrementActivityMvmvpState> (IncrementActivityMvmvpState()) }
to implement We accept the ViewModel that we'll be using for storing state as a parameter and then implement our Presenter interface Implementing onIncrementClicked becomes as easy as sending a mutation to the queue Since mutations can happen from multiple sources, important to process them serially class PresenterMvmvpImpl(override val viewModel: ViewModelMvmvp) : MvmvpPresenterBase<StateMvmvp, ViewModelMvmvp>(viewModel), PresenterMvmvp { override fun onIncrementClicked() { sendToViewModel { it.copy( count = it.count.toInt().plus(1).toString() )} } }
it's still straightforward Instead of implementing a View interface, we now listen for changes coming from the ViewModel class IncrementActivityRxMvmvp : RxAppCompatActivity(), ScopeProvider { private val viewModel by lazy { ViewModelProviders.of(this) .get(ViewModelMvmvp::class.java) } private lateinit var presenter: IncrementActivityMvmvpContract.PresenterMvmvp override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initPresenter() setContentView(R.layout.activity_increment) observeActions() observeState() } // … }
and observing the view interactions to pass them to the Presenter We now have a render() function that takes the updated State object and updates the Android Views accordingly private fun observeState() { viewModel.state .`as`(autoDisposable(this)) .subscribe { render(it) } } private fun render(state: StateMvmvp) { counter.text = state.count } private fun observeActions() { increment.clicks() .`as`(autoDisposable(this)) .subscribe { presenter.onIncrementClicked() } }
now, we are asserting that the State ViewModel updates the way we expect it to In this example, I used Hamkrest, a port of Hamcrest for Kotlin to make assertions @Test fun `test increment`() { val viewModel = ViewModelMvmvp() val presenterImpl = PresenterMvmvpImpl(viewModel) assert.that(viewModel.state.value.count, equalTo("0")) presenterImpl.onIncrementClicked() ioThreadScheduler.triggerActions() mainThreadScheduler.triggerActions() assert.that(viewModel.state.value.count, equalTo("1")) }
your data model to what is displayed in your view • Uses an enhanced XML layout • Eliminates the complexity of checking to see if data has changed before rendering an updated view • Simplifies the render() step of our process when using a defined State The Data Binding Library is a support library that allows you to bind UI components in your layouts to data sources in your app using a declarative format rather than programmatically
in the data field that will be generated as fields that you can assign and update later In our case, we want two fields, one for our State object that will be updated when it changes and other for our presenter so that we can trivially send events to our presenter <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" > <data> <variable name="model" type="com...IncrementActivityMvmvpState"/> <variable name="presenter" type="com...PresenterMvmvp"/> </data> <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent" > <!-- layout here --> </androidx.coordinatorlayout.widget.CoordinatorLayout> </layout>
we previously defined You'll note the syntax for entering a "data binding expression" in the xml Once there, we can perform basic logic and mapping values to the view Since we're using a data model, there's just mapping of data to view Finally, since we can also access our Presenter, we can set our onClick listener right nere <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:orientation="vertical"> <TextView android:id="@+id/counter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="@{model.count}" /> <Button android:id="@+id/increment" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="Increment!" android:onClick="@{view -> presenter.onIncrementClicked()}" /> </LinearLayout>
reference our generated "binding" and we assign it a LifecycleOwner and a reference to our Presenter Our observeState call now becomes exceedingly trivial as we just assign our State object to the model field in the data binding and we let the generated code handle updating our views accordingly override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initPresenter() binding = DataBindingUtil .setContentView(this, R.layout.activity_increment_binding) binding.setLifecycleOwner(this) binding.presenter = presenter observeState() } private fun observeState() { viewModel.state .`as`(autoDisposable(this)) .subscribe { binding.model = it } }
recommend exploring all of them! • Some may choose to use a formal implementation of these patterns • Building your own can give more flexibility ◦ Can tailor solutions to your needs ◦ Can help solve app-specific requirements you might have • One size never quite fits all, use what works best for your application! ◦ But also, don't reinvent the wheel if you don't need to