Depop — MVP architecture in Android

Canato
8 min readMar 24, 2020

How our simple architecture improves our team’s ability to modularise, de-couple, maintain and test our code base.

This art is a small reference to my first Android mentor, Kobe, without him I would still building apps like a monkey. Now he is back to Taiwan. Art: https://www.linkedin.com/in/caroline-fletcher/ Android + Depop: https://www.linkedin.com/in/joe-stone-16a0a961/
  • The first part here I would explain what problems we had in our legacy code, so can be clear what motivate us to Modular Architecture (https://engineering.depop.com/navigating-between-modules-in-android-decf20555a4c) our code and use MVP;
  • In second part I will explain how we approach the MVP architecture;
  • A resume of the best features we can get from this modularise code + our MVP structure
  • And in the end a small explanation why we don't use MVVM and why make sense for us.

Legacy code and it problems

(you can jump this, I would love to jump this)

Our code base was build with target to reuse structures, so if we want to have a list in the app we would create one layout that we could use everywhere we want lists.

Basic Layout:
list_items_layout.xml

Now you need:
1. Profile page with list of items;
2. Receipt page with receipt list of receipts.

  • To open Receipt page you need to be in your profile profile > receipt
    navigation.

Packages and files:

profile/
ProfileView.kt
receipts/
ReceiptView.kt

Both would use the same layout:

class ProfileView: Fragment(R.layout.list_items_layout) {
//... code
fun showList(items: ProductItem){...}
}
class ReceiptView: Fragment(R.layout.list_items_layout) {
//... code
fun showList(items: ReceiptItems){...}
}

Very couple, right? get worst.
1. Let's change the Receipt List so we can have two tabs. But we cannot change list_items_layout cause this would change ProductItem too, so we create a IF condition.

class ProfileView: Fragment(R.layout.list_items_layout) {
companion object {
fun newInstance(): Fragment = ProfileView()
}
//... code
fun showList(items: ProductItem){...}
}
class ReceiptView: Fragment(R.layout.list_items_layout) {
companion object {
fun newInstance(): Fragment = ReceiptView()
}
//... code
fun showList(items: ReceiptItems, tabs: NumberOfTabs){...}
}

2. Open receipt list from messages, but first you need to navigate to profile so you can open it. And you want the receipts only between two users.

class ProfileView: Fragment(R.layout.list_items_layout) {
companion object {
fun newInstance(): Fragment = ProfileView(openReceipts: Boolean, otherUserId: UserID)
}
//... code
fun showList(items: ProductItem){...}
}
class ReceiptView: Fragment(R.layout.list_items_layout) {
companion object {
fun newInstance(): Fragment = ReceiptView(otherUserId: UserID)
}
//... code
fun showList(items: ReceiptItems, tabs: NumberOfTabs){...}
}

This plan of more general structures that we can reuse everywhere can easily come something like this:

public static Intent makeIntentFromLogin(@NonNull final Context context, final Uri uri) {...}public static Intent makeIntentFromIsJustRegistered(@NonNull final Context context, final Uri  uri) {...}public static Intent makeIntent(@NonNull final Context context) {...}public static Intent makeIntent(@NonNull final Context context, @IdRes final int navigationItem, final int subnavigationItem, @Nullable final String productSlug, final long productId, final Uri uri, final int flags, final boolean showRecommendations) {...}public static void startFromLogin(@NonNull final Activity activity, final Uri uri) {...}public static void start(@NonNull final Activity activity) {...}public static void start(@NonNull final Activity activity, @IdRes final int navigation)  {...}public static void start(@NonNull final Context context)  {...}public static void startProductRecommendations(@NonNull final Activity activity)  {...}public static void startToProfileOpenReceipts(@NonNull final Activity activity, @NonNull final ReceiptTab initReceiptsTab, final Long otherUserId) {...}public static void startToProfileWithInitialTab(@NonNull final Activity activity, final int initialTab) {...}
What scare me.

So we had 3 main reasons to start to modularise our code and change to MVP

  • Sensible/highly-couple Code — What means that, for each change in one place, we could affect many places on the same time.
    (layout example)
  • Hard to reuse or change — When we want use a feature that is inside another feature or give the feature a new type of parameter we had the creation of many if/else's cases
    (Call receipts from messages example)
  • Long Term One Activity Possibility — Making our code more modularised we open to a possibility of in the future only have fragments being called. Since each feature/screen is independent it can be called from any other place in the code

MVP Architecture

We split our code base in 4 different layers: View, Presenter, Core and Data.
They are very self explanatory, but the main one here is core. Core is the source of true. Core never change if the feature never change.

View Layer

  • Responsible for the UI elements;
  • Hold Android elements;
  • Dumb as possible;
  • React to user interactions.

Presenter Layer

  • Contains Model (our model is the data representative of the UI elements);
  • Mapper (from Domain/entity to our Model);
  • Deal with Threads and Concurrencies;
  • Control View life cycle;
  • If need, keep the state. Our source of true.

Core Layer

  • Domain rules / Business Logic;
  • Immutable/Stable;
  • Contains the Domain Class / Entity;
  • Have the Contract;
  • Always receive domain and return domain.

Data Layer

  • Deal with different data sources;
  • Aggregate DTOs [Data Transfer Objects];
  • Mapper (from DTO to Domain).

This is a little abstract, and don't show the importance we give to the Core, so let's build one high level structure step by step.

Here we can see our 4 layers and the Contract in green, inside the Core.
The Contract is the base of how the layers connect between then. I will explain it a little further, but by now understand it as the holder of 4 interfaces, for each main class of each layer.

As said, the graphic is high level. But now you can see our main classes in blue and how they communicate. They use the interface (contract) as point of trust to create the functions and to consume other classes.
Important to note that Presenter hold a view instance.

Here we add our Data Classes and our Mappers. Now with a more complete structure we can visualize how all arrows get inside the core layer and none point to outside, this give us a stable structure, where our layers dependes on the Core but the Core don't depende of anything. Having this make us safe about changes in the UI/View, in how we should our data (presenter) or where this data come from (data/repository).

Now we should have the connection from our Domain with the Data Source. One same repository could have one or many data sources, so maybe we could expected something like this:

But this look like a mess and no one want to take care of it.
Good point that data layer is only consuming what we call Service.
Basic the Service should contain one communication class, like API, SharedPreferences, Room, etc and one data class, what we call DTO.
Service should not hold any logic or cache, they are only meant to hold the data structure if we use in different places.

So we have it as a different package and this is very important, where we consume what they offer us:

Modules Packages Structure

With this structure many domains can use the same service. And if the service change (for example the API call have a new version) we are sure to update it in everywhere where it is used.
Ideally we should have just one API for each Domain, but the ideal not always happen =/

Just discover this year about some pigs with hair. I was amazed, so wanna share this knowledge. Thanks

Domain Contract

Part of the Core Layers (most important one), the contract have a unique function in the MVP structure. The Contract is responsible to make the rules about how the layers will consume each other and communicate.

Since the code touches all the layers, we could say that it should have a horizontal position, but it is important to have inside the core domain, so we can remember about it immutability.

internal interface ListingCopyContract {

interface View {
fun showLoading()
fun hideLoading()
fun displayProducts(model: ListingCopyModel.Valid)
fun displayNextProducts(model: ListingCopyModel.Valid)
fun displayError(errorMessage: String)
fun closeWithResult(productId: ProductId)
fun cancel()
}

interface Presenter {
fun bindView(view: View)
fun unbindView()
fun onViewCreated()
fun onScroll(lastVisibleItemPosition: Int)
fun onItemSelected(productId: ProductId)
fun onBackPressed()
}

interface Interactor {
suspend fun retrieveProductsPage(): ListingCopyDomain
suspend fun retrieveProductsNextPage(
offsetId: OffsetId
): ListingCopyDomain
}

interface Repository {
suspend fun retrievePage(userId: Long): ListingCopyDomain
suspend fun retrieveNextPage(
userId: Long,
offsetId: OffsetId
): ListingCopyDomain
}
}

On the contract we can check some nice points. By looking the contract you should be able to understand what the feature is doing and what the feature need, almost you can predict all cases for the feature.
In another hand, the contract don't know implementation details, like where the data come from or the logic used inside the Interactor.
And when we write a good contract we can avoid parameters like Booleans or strings. So we have a tested method already on the interface

Beneficies

Not gonna lie to you, implement this architecture, with this structure and this modularisation was against my instinct. So I was keen to understand the benefits and there are some really important ones.

First it maintenance, when you had some problem to fix you can easily spot where in the code it is. Because of the good structure you know where to expected each part of the code.

Second reviews, with this architecture you can first look directly the Contract and the tests to check what could be going wrong. Is easy to spot possible errors before look the implementation. Help you to "smell" something wrong.

Third tests, we decouple a lot, we unit test a lot, became easy to guarantee the right behaviour and rely on your tests.

Four the Reuse of the features became very simple, you can easily call any domain from any other domain and this make the navigation in your app fast to adapt

We are hiring! https://grnh.se/bf3fb55f3

BUT and MVVM?

Kronk

MVVM, like others architectures, is a very good solution for many cases, but for us, with the modularisation we don't see the need to use. In one hand, yeah, you need to bind the view always to the presenter and this is something we should improve, but give us another skill we enjoy more.
With MVVM, when you change the value, you expect that something will be listening/observing the data and update the UI. With MVP we know that the function is called. So it give us more power and knowledge about what is going on.

And one strong reason for us, is that the normal good implementation of MVVM rely in some library like LiveData. And we have this strong view about not rely in any library. So our structure is independent by it self.

That's it, like it? Suggestions? Another point of view?

--

--

Canato

Android Developer, RPG players, DJ and producer and ex- many stuffs, let's have a coffee