Attach your presenters to view layer

With help of Kotlin delegates

Michael Spitsin
ProAndroidDev
Published in
6 min readOct 26, 2019

--

When you want to move view logic out of view object, you create well know presentation layer, using one of MV-patterns. Particularly, for MVP you probably using presenter, where all ui-related logic is described (show/hide progress, show some error and etc.)

Your view can have several presenters, there can be one presenter in one fragment, or several presenters in one activity. And you will somehow built the base mechanism of attaching view layer to each presenter.

Let find a way to do that with Kotlin’s delegates.

How it was

Specificity of MVP pattern is that presenter tells the view, what it should do and each presenter speaks only with one view. Thus, you probably will end up with kind of BasePresenter which contains attach/detach methods and maybe any of lifecycle dependent methods like onResume/onPause or prepare/release, does not matter. Probably you will have this kind of sample base class, to not duplicate attach/detach code:

abstract class BasePresenter<V> {

private var view: V? = null

fun
attachView(view: V) {
this.view = view
}

open fun resume() {}

open fun pause() {}

fun detachView() {
view = null
}
}

You can even extend ViewModel, if you want to support orientation change out of the box, for example:

abstract class BasePresenter<V> : ViewModel() {

...

final override fun onCleared() = detachView()
}

On the view side you will have something like BaseFragment which will know the type of presenter, to which it will be attached and provide base logic of attaching and invoking lifecycle aware method. For instance:

abstract class BaseFragment<V, P : BasePresenter<V>> : Fragment() {

protected abstract val presenter: P

protected abstract val view: V

override fun onAttach(context: Context) {
super.onAttach(context)
AndroidSupportInjection.inject(this)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.attachView(this.view)
}

override fun onResume() {
super.onResume()
presenter.resume()
}

override fun onPause() {
presenter.pause()
super.onPause()
}

override fun onDestroyView() {
presenter.detachView()
super.onDestroyView()
}
}

And yes, presenter can speak with multiple views, it not so important, since for that transformation (from one view reference to list of views) you need to implement simple compositor pattern.

SampleFragment will look like that:

class SampleFragment : BaseFragment<SampleView, SamplePresenter>(), SampleView {
@Inject
override lateinit var presenter: SamplePresenter

override val view = this

...
//implementation
}

Steps we will make

How we can

Remove one fragment-to-one-presenter connection ()

Our fragment (applied to activity as well) can be attached to only one presenter. But what if we have several presenters, because, for example, one of them are used in different fragments with similar ui and same presentation logic?

In that case we need to make aforementioned approach little more flexible. And of course you should do that only if you wish and need that, since we all know, that if it is not necessary, may be you should skip that.

abstract class BaseFragment<V> : Fragment() {

private val presenters: MutableList<BasePresenter<V>> = mutableListOf()

protected abstract val view: V

override fun onAttach(context: Context) {
super.onAttach(context)
AndroidSupportInjection.inject(this)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenters.forEach { it.attachView(this.view) }
}

override fun onResume() {
super.onResume()
presenters.forEach { it.resume() }
}

override fun onPause() {
presenters.forEach { it.pause() }
super
.onPause()
}

override fun onDestroyView() {
presenters.forEach { it.detachView() }
super
.onDestroyView()
}

protected fun BasePresenter<V>.attach() = presenters.add(this)
}

Now in the inheritor we will have little bit different logic:

class SamplePresenter1 : BasePresenter<SampleView>()
class SamplePresenter2 : BasePresenter<SampleView>()

class SampleFragment : BaseFragment<SampleView>(), SampleView {
@Inject
lateinit var presenter1: SamplePresenter1

@Inject
lateinit var presenter2: SamplePresenter2

...//implementation

override fun onAttach(context: Context) {
super.onAttach(context)
presenter1.attach()
presenter2.attach()
}

...//implementation
}

Bind not only one view to presenters (↑)

Our fragment (it can be applied to activity as well) can be the only one view for all presenters, that used by it. But there are projects and team who like to have many presenters for small view components. Yes, you can exclude each view component to separate fragment, and no it is not always acceptable for different team reasons.

So we need to have possibility to bind presenter to fragment lifecycle, but give an opportunity to each presenter to attach different views they needed. For that we need just to update attach-extension we have in a BaseFragment. But first let’s introduce small internal class PresenterViewLinker that will know, how to attach specific view to its own presenter.

private class PresenterViewLinker<V>(
private val presenter: BasePresenter<V>,
private val view: V
) {
fun link() = presenter.attachView(view)
fun resume() = presenter.resume()
fun pause() = presenter.pause()
fun unlink() = presenter.detachView()
}

Now we can update BaseFragment:

abstract class BaseFragment : Fragment() {

private val linkers: MutableList<PresenterViewLinker<*>> = mutableListOf()

...//similarly handle all lifecycle methods

protected fun <V> BasePresenter<V>.attach(view: V) =
linkers.add(PresenterViewLinker(this, view))

private class PresenterViewLinker<V>(
private val presenter: BasePresenter<V>,
private val view: V
) {
fun link() = presenter.attachView(view)
fun resume() = presenter.resume()
fun pause() = presenter.pause()
fun unlink() = presenter.detachView()
}
}

Now in the inheritor we will have little bit different logic:

class SamplePresenter1 : BasePresenter<SampleView1>()
class SamplePresenter2 : BasePresenter<SampleView2>()

class SampleFragment : BaseFragment(), SampleView1 {

@Inject
lateinit var presenter1: SamplePresenter1

@Inject
lateinit var presenter2: SamplePresenter2

private val sampleView2: SampleView2 by lazy { /*something*/ }

...//implementaion

override fun onAttach(context: Context) {
super.onAttach(context)
presenter1.attach(this)
presenter2.attach(sampleView2)
}

...//implementaion
}

⓷ Make a delegate for reduce boilerplate (↑)

Now let’s make our delegate property class for attaching presenters to views and we will have a really nice effect. Let’s go :) Start with introducing PresenterDelegate which will be responsible for attaching presenter to appropriate view:

inner class PresenterDelegate<V, P : BasePresenter<V>>(
private val view: V
) : ReadWriteProperty<Any, P> {

private var presenter: P? = null

override fun
getValue(thisRef: Any, property: KProperty<*>) =
presenter ?: throw IllegalStateException("AndroidSupportInjection.inject was not called")

override fun setValue(thisRef: Any, property: KProperty<*>, value: P) {
innerAttach(value, view)
this.presenter = value
}
}

BaseFragment will be pretty the same:

abstract class BaseFragment : Fragment() {

...

protected inline fun <reified V : Any, reified P : BasePresenter<V>> V.presenter() = PresenterDelegate<V, P>(this)
protected fun <V> BasePresenter<V>.attach(view: V) = innerAttach(this, view)

protected fun <V> innerAttach(presenter: BasePresenter<V>, view: V) =
linkers.add(PresenterViewLinker(presenter, view))
...
}

As you see, know we have additional presenter method, that will actually be used for declare delegated properties. Let’s see how it looks like:

class SampleFragment : BaseFragment(), SampleView1 {

private val sampleView2: SampleView2 by lazy { /*something*/ }

@set:Inject
var presenter1: SamplePresenter1 by presenter()

@set:Inject
var presenter2: SamplePresenter2 by sampleView2.presenter()

...//implementation
}

When we call just presenter we will tell to BaseFragment “bind yourself to presenter”. if we want to bind specific view to presenter we need just to call specificView.presenter() in declaration part.

⓸ Move all base logic into separate class (↑)

Now we have everything almost done. The last part: we want to have an ability to provide this for other components. For example, for every BottomSheetDialogFragment. Since we can not extend bottom sheets from our BaseFragment we will be tempted to duplicate logic of attaching views to presenters in some kind of BaseBottomSheet. But wait for a minute. Let’s just make this logic unique.

To do that we need to define an interface PresenterContainer which will contain needed attach methods:

interface PresenterContainer {

fun <V : Any, P : BasePresenter<V>> V.presenter(): PresenterDelegate<V, P>

fun <V> BasePresenter<V>.attach(view: V)
}

In general we will move everything we wrote before into its implementation:

class PresenterLifecycleContainer : PresenterContainer, LifecycleObserver {

private val presenters: MutableList<PresenterViewLinker<*>> = mutableListOf()

override fun <V> BasePresenter<V>.attach(view: V) {
presenters.add(PresenterViewLinker(this, view))
}

fun onViewCreated() = presenters.forEach { it.link() }

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() = presenters.forEach { it.resume() }

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() = presenters.forEachReversed { it.pause() }

fun
onDestroyView() = presenters.forEachReversed { it.unlink() }

override fun
<V : Any, P : BasePresenter<V>> V.presenter() = PresenterDelegate<V, P>(this, this@PresenterLifecycleContainer)
}

Now let’s adjust BaseFragment class:

abstract class BaseFragment(
private val container: PresenterLifecycleContainer = PresenterLifecycleContainer()
) : Fragment(), PresenterContainer by container {

init {
lifecycle.addObserver(container)
}
...//attaching phase override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
container.onViewCreated()
}

override fun onDestroyView() {
container.onDestroyView()
super.onDestroyView()
}
}

That’s it. Now, if we will want to use PresenterContainer suppose inside BottomSheet, then we need just simply define:

abstract class BaseBottomSheet(
private val container: PresenterLifecycleContainer = PresenterLifecycleContainer()
) : Fragment(), PresenterContainer by container {
init {
lifecycle.addObserver(container)
}
...//attaching phase override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
container.onViewCreated()
}

override fun onDestroyView() {
container.onDestroyView()
super.onDestroyView()
}
}

And we will be able to call presenter function for property delegates, as we done for SampleFragment in previous examples.

Conclusion

Kotlin Delegated Properties are great. They help to create consice, brief and simple code that more removing all not necessary boilerplate. If you liked that approach, then do not forget to clap! Happy coding :)

--

--

Love being creative to solve some problems with an simple and elegant ways