But MVC does the job!

When examining the MVC pattern as described here, it’s evident that the direct relationship between the View and Controller is problematic. Tight coupling results in challenging testing, where changes in one class necessitate updates in another, and it doesn’t scale well. In complex views, there can be multiple conditions for setting values in UI elements, depending on the current state of the UI and responses from the Model. Dealing with complex logic when testing becomes challenging… leads to anarchy.

This issue was apparent to developers, prompting some to propose better ways to implement MVC, as discussed here. In this approach, the Controller is a separate class from the Fragment or Activity. Since it’s an independent object from Android Framework instances, interfaces can be employed to mock components and facilitate testing.

The Presenter is born

The evolution of MVP, as known in the context of Android development, can be traced back to Google’s Web Toolkit ( GWT). In the mid-2000s, GWT adopted MVP as a response to the challenges faced in building modular and testable web applications.

Let’s have an interface for the View and the new Controller type, but let’s call it Presenter since it’s responsible for presenting model data to the View. A simple diagram may look like this:

classDiagram
    class Model

    class View {
        << interface >>
    }

    class Presenter {
         << interface >>
    }

    Model -- Presenter: notifies
    View -- Presenter: interacts
    Presenter -- Model: manipulates
    Presenter -- View: updates

Please note that View and Presenter are interfaces, and while they still keep a reference to each other, it’s only by the interface, not the concrete implementation. The interfaces are providing only methods relevant for particular UI interactions.

Let there be Contract!

Okay, so now there is a set of interfaces, implemented by a concrete View and Presenter, to serve for a particular UI screen. It would be nice to keep them close, so it’s clear which View talks to which Presenter, there is always a 1-to-1 relation.

classDiagram
    class ItemListContract {
         << interface >>
    }

    class Presenter {
         << interface >>
        +addItem()
        +removeItem()
    }

    class View {
         << interface >>
        +displayError()
        +hideError()
        +displayItems()
        +showLoading()
        +hideLoading()
    }

    View --* ItemListContract
    Presenter --* ItemListContract

    class ConcreteItemListFragment

    class ConcreteItemListPresenter

    ConcreteItemListFragment --|> View
    ConcreteItemListPresenter --|> Presenter

It looks nicer in Kotlin code:

interface ItemListContract {
    interface View {
        fun displayError(errorMessage: String)
        fun hideError()
        fun displayItems(items: List<Int>)
        fun showLoading()
        fun hideLoading()
    }

    interface Presenter {
        fun addItem()
        fun removeItem()
    }
}

The Activity being the View will look like this:

class MainActivity : AppCompatActivity(), ItemListContract.View {
    // creating instance of the presenter, can be also injected by DI
    private val presenter: ItemListContract.Presenter = MainViewPresenter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // giving Presenter instance of the View
        // this should happen when the View is actually ready to perform its tasks
        // because Presenter may want to display something right after getting reference to the View
        presenter.attachView(this)
        ...

The Presenter:

class MainViewPresenter : ItemListContract.Presenter {
    private lateinit var view: ItemListContract.View

    fun attachView(view: ItemListContract.View) {
        this.view = view
    }
}

No magic here, simple passing a reference to this from View to Presenter, and from now on, the Presenter should be safe to call View methods. This is why it should happen in the Activity or Fragment onCreate and not in the Presenter constructor. Otherwise, you would have to call some extra onViewReady() on the Presenter when you have your view prepared, with all elements assigned to fields with findViewById() etc.

The View and the Presenter know only each other through the Contract, so it should be nice to write Presenter tests, with mocking of the View and verifying which methods were called and passed arguments.

The boilerplate

You probably already identified the ugly part now. Each presenter has a method attachView or similar, but it’s not really part of any common interface for Presenter. It would be really great to have some sort of framework, taking care of all the boilerplate methods for View and Presenter, so Contract can focus only on relevant methods for a particular UI scenario.

The View and Presenter couple should also be detached to avoid memory leaks when the view is destroyed.

All Presenter work is done on the UI thread. It would be great to perform tasks on other threads, and cancel them when the View is destroyed since the result it’s not relevant anymore.

This can be achieved by introducing an abstract BasePresenter, which is also a CoroutineScope. The diagram starts to be really complex, for a simple presentation layer architecture:

classDiagram
    class Presenter~VIEW~ {
        << interface >>
        +attachView(VIEW view)
        +detachView()
    }

    class View {
        << interface >>
    }

    class BasePresenter~VIEW~ {
         << abstract >>
        -VIEW view
        -Job job
        +onViewAttached()
        +onViewDetached()
    }

    class CoroutineScope {
        << interface>>
    }

    Presenter <|.. BasePresenter
    CoroutineScope <|.. BasePresenter

    class ItemListContract {
        << interface >>
    }

    class ItemListView {
        << interface >>
        +displayError()
        +hideError()
        +showLoading()
        +hideLoading()
    }

    class ItemListPresenter {
        << interface >>
        +addItem()
        +removeItem()
    }

    class ConcreteItemListFragment

    ItemListContract ..* ItemListView
    ItemListContract ..* ItemListPresenter
    ItemListPresenter ..|> Presenter
    ItemListView ..|> View
    ConcreteItemListPresenter ..|> ItemListPresenter
    ConcreteItemListPresenter --|> BasePresenter
    ConcreteItemListFragment ..|> ItemListView

BasePresenter

It looks more complex than it is. Each ConcretePresenter implements Contract.Presenter for its domain capabilities and BasePresenter for common methods like attachView() or detachView(). The BasePresenter provides CoroutineContext, so running methods on the Model won’t happen on the UI thread, and jobs will be canceled when the view is detached.

Kotlin implementation:

abstract class BasePresenter<VIEW : View> : Presenter<VIEW>, CoroutineScope {
    internal lateinit var view: VIEW

    private var job: Job = SupervisorJob()

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.IO + job

    override fun attachView(view: VIEW) {
        this.view = view
        onViewAttached()
    }

    override fun detachView() {
        job.cancel()
        onViewDetached()
    }

    abstract fun onViewAttached()
    abstract fun onViewDetached()
}

Because the jobs are performed outside the UI thread, and only the UI thread can modify UI elements, the View methods implementation has to use something like this:

override fun displayLoading() {
    runOnUiThread {
        progressBar.visibility = View.VISIBLE
    }
}

View and Presenter interfaces

There are also new interfaces for generic View and Presenter.

interface Presenter<VIEW : View> {
    fun attachView(view: VIEW)
    fun detachView()
}

interface View

// Contract
interface Contract {
    interface ConcreteView : View

    interface ConcretePresenter : Presenter<ConcreteView>
}

Using generics guarantees everything is connected in the right way. The MainViewPresenter now has to inherit from both the contract presenter interface and the BasePresenter. It’s not ideal but allows keeping behavior relevant to the contract in a separate place from behavior relevant to the presenter base functionality.

class MainViewPresenter : Contract.ConcretePresenter, BasePresenter<Contract.ConcreteView>()

The ConcretePresenter and BasePresenter<Contract.ConcreteView> have the same ConcreteView view, so it all works nicely. This way, the View doesn’t have to know about the BasePresenter interface to use attachView() and detachView(). Those methods are part of the Presenter interface that ConcretePresenter inherits. BasePresenter also inherits this interface, and it provides the actual implementation for assigning the View reference and handling job cancellation. Still, it gives ConcretePresenter implementation a way to perform some actions when the view is attached with the

abstract fun onViewAttached()
abstract fun onViewDetached()

So with a relatively small cost of remembering to always inherit from BasePresenter, we get nice separation of behaviors, and our Contract can focus on UI handling, rather than technical details of threading etc.

There were no changes for the View implementation; the ConcretePresenter interface has all the relevant methods.

Presenter implementation

Let’s look into MainViewPresenter code:

override fun removeItem(item: Int) {
    launch {
        view.displayLoading()
        Model.removeItem(item)
            .onFailure { error ->
                view.hideLoading()
                displayErrorMessage(error)
            }
            .onSuccess {
                view.hideLoading()
            }
    }
}

Every method from ConcretePresenter looks similar. It’s wrapped in a launch block because the BasePresenter is a CoroutineScope, and it sets the context in its implementation:

private var job: Job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job

The Job is canceled when the view is detached. In my implementation, the Model returns a Kotlin Result, so I can handle errors nicely with the onFailure() extension method. The removeItem() method tells the View to display some loading. Some, because it’s up to the View implementation on how to do this. The Presenter has no idea if it’s a LinearProgressIndicator or CircularProgressIndicator, or maybe a Dialog with text “please wait”. Thanks to this, the whole UI can even get redesigned with very little impact on the Presenter. The Controller typically had to know much more about View details. The same goes for displayErrorMessage(). We can define what data is passed to View in the Contract, and we don’t have to rely on any Android Framework limitations here.

After an item is successfully removed, the Presenter is just hiding the loading. What about refreshing data? Well, it happens automatically:

override fun onViewAttached() {
    launch {
        Model.itemsFlow.collect { items ->
            view.displayItems(items)
        }
    }
}

The Presenter is observing the items’ flow from the Model and updating the View with each new change. The old-fashioned way of achieving this would be to get new data from the Model after removing an item and passing it to the View.

Common problems of MVP

While Model-View-Presenter (MVP) offers several advantages over MVC, it’s important to be aware of some common issues associated with this architectural pattern.

  1. Boilerplate Code:
    • Each component (Model, View, and Presenter) requires its own set of interfaces and implementations, leading to verbose code.
  2. Complexity for Simple UIs:
    • For simple user interfaces, the introduction of MVP may seem like an overhead. In cases where the UI logic is straightforward, the additional layers introduced by MVP might be considered unnecessary.
  3. Learning Curve:
    • Developers who are new to MVP may find it challenging to understand the separation of concerns and the proper communication flow between the components. This can result in a steeper learning curve compared to simpler patterns.
  4. Presenter Responsibilities:
    • Determining what should go into the Presenter and what should stay in the View or Model is not always straightforward. Deciding on the proper distribution of responsibilities can lead to ambiguity and inconsistencies across implementations.
  5. Tight Coupling:
    • While MVP aims to reduce coupling compared to MVC, there’s still a potential for tight coupling between the View and Presenter, especially if not implemented carefully. This can make it harder to replace or modify one component without affecting the other.
  6. Difficulty in Lifecycle Management:
    • Managing the lifecycle of components in MVP, especially in Android where the activity and fragment lifecycles are critical, can be challenging. Improper management may result in memory leaks or unexpected behavior.
  7. No standardization:
    • Unlike some other patterns, there is no strict standardization for MVP, leading to variations in implementations. This lack of a standard can make it challenging for developers to move between projects seamlessly.

It’s important to note that the issues mentioned above are not inherent flaws in MVP but rather considerations and trade-offs. The appropriateness of MVP depends on the specific needs of the project and the preferences and experience of the development team. Additionally, some of these issues can be mitigated with proper design practices and libraries that support MVP, such as dependency injection frameworks.

Conclusion

MVP is a step in the right direction from MVC. It’s also not that hard to actually refactor from MVC to MVP. Nowadays, we can benefit from using coroutines, flows, or Result class to avoid callback hell and easy threading. The pattern provides clear(er) separation of concerns. It’s finally easy to test code between Model and the View.

The MVP pattern version I showed in this post requires a bit of boilerplate, even for low complexity UI. But at the same time, a lot of technical issues are hidden, and developers may focus solely on the Contract.

Even now when I look at the extended MVP diagram, it looks a bit scary. There are a lot of interfaces, and it may look like an overkill. If only there was a way to achieve similar separation of concerns, testability, and ease of use… oh wait, there is MVVM :)


share on:



share on: