Skip to content

Instantly share code, notes, and snippets.

@aemxn
Last active July 27, 2022 06:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aemxn/f0960ed5f97253ee2629a7739af2f7b6 to your computer and use it in GitHub Desktop.
Save aemxn/f0960ed5f97253ee2629a7739af2f7b6 to your computer and use it in GitHub Desktop.
Android MVP Architecture

Android MVP Architecture

0. What is an MVP?

MVP is a Model-View-Presenter in short. It is a derivative from Model-View-Controller (MVC) software architectural pattern. The main concern about MVP structure is that it separates the view from the backend logic. Because Android doesn't care about which pattern to be used when developing, the community developer itself has to debate on which architecture to use.

It is well suited in Android because we needed it for easy testability, at least that's what I see when developing an app based on this structure. Just to let you know, this article only covers on how to implement the layering structure WITHOUT testing suite and dependency injection.

It is gonna be plain and simple to understand how it works under the hood. You can structure it any way you want but the basic idea is that view updates "decision" is done by the Presenter via View.

MVP in a nutshell

Image source: http://www.singhajit.com/mvp-in-android/

Let's take a look at the meaning of each letter that represents MVP one by one:

  1. Model

    • The data source
    • The data structure (model POJO)
    • Repository
    • Realm/Sqlite/Sharedpreference
  2. View

    • Is NOT the UI itself
    • Is an interface, well, lots of it
    • An interface which interacts between Presenter and the UI
    • Also refer to as contract for UI to update and receive data to be passed to the Presenter
  3. Presenter

    • Mediator between Model-View and the UI
    • Contains logic, means it will use Model layer to acquire data and send it back to the UI via the View
    • Ideally, does not contain Android specific framework like context
    • Communication between the UI is done by View (aka contract)

1. Why MVP?

I've worked with this structure to build an app for my company. There are several reasons why I chose to use it.

  • To learn
  • Having a team that works on a same project, it is easier to delegate tasks by layer. Eg. Person A contributes on the data layer, Person B just touch the UI (activity/fragment)
  • Easier to train new coders to start working as it is a standardized design
  • Technically, Android context is closely coupled with plain java code. So we would need its usage to be separated for easy testability. Using this, we can achieve unit testing and instrumentation test easily
  • If done correctly and by using best practices like SOLID principle, we can achieve a highly modular codebase which results in easy maintainability. Eg. changing database, changing network call provider, creating a library specifically for the core function part

2. Implementation

Though I think this implementation isn't a full MVP because it lacks test capability. But we can learn how it actually works and learn to improve upon it.

Also this design is being used in a project I've worked on before, so I will not provide any working example of Github project. This is only for my own future reference and of course for others to learn and critique.

2.1 Design implementation

Consider the following tree structure which is a modified version of avenging project which nicely separate the modules by core and ui (see top 2 parent directories below). This is not yet an MVP design implementation because we are structuring it based on modules. I think it's easier to maintain it this way for in the future if we want to make the core as a JAR library, it can be done so easily.

Other than that, we are gonna make use of several best practices in this codebase such as Builder patterns and Repository pattern for our Model DAO. One edge case I've ran into is when creating a push notification service. After some research, I found that services as such doesn't belong to any of MVP layer (See services directory below).

As for the dependencies, I'm using Realm and Shared Preference as my persistence tool and Retrofit as my networking service.

├───core ; backbone of the app
│   ├───data
│   │   ├───model ; POJO models from json response. Extends RealmObject
│   │   │   ├───Menu
│   │   │   └───Restaurant
│   │   ├───network ; networking utility
│   │   ├───preference ; shared preference utility
│   │   └───repository ; the dao
│   │       ├───commons ; common interfaces to be extended by specification
│   │       ├───impl ; implementation classes for each model repository
│   │       └───specification ; feature-centric specification
│   │           ├───Menu
│   │           └───Restaurant
│   ├───mvp ; presenter and contracts
│   │   ├───base ; abstract classes for mvp usage
│   │   ├───menu ; feature
│   │   └───restaurant ; feature
│   ├───services ; services not associated with mvp
│   │   └───push_notification
│   │       └───action
│   └───util ; utility for core module
└───ui ; activity/fragment/view/adapter
    ├───base ; base classes
    ├───home ; feature
    ├───main ; feature
    ├───menu ; feature
    ├───restaurant ; feature
    ├───util ; utility for ui module
    └───view ; custom extended views/viewgroups

core

  • Also known as the domain layer
  • Consists of only data management and transaction of data between UI and backend
  • Doesn't know the existence of where it should be displayed (fragment, activities)
  • API calling, database handling, cache management, etc. sits inside this package
  • Ideally should be context-free

ui

  • Also known as the presentation layer
  • Consists of UI such as activities, fragment and/or custom views
  • Ideally should NOT contain business logic as it only displays the data given by core module
  • Interaction with the domain layer is done by specific ViewAction implementation (see MVP section)

Chart below shows how layers are communicating with each other. More on code implementation below.

The working layers Right-click > View Image to enlarge

2.2 Code implementation

Use these base classes for View Actions and generic Repository

RemoteView:
https://gist.github.com/aemxn/056f138dbd921bd4c4a9428cdd32c5bf

BasePresenter:
https://gist.github.com/aemxn/cfa36bdf11d4b54704a8b3a05cd4a01d

Repository:
https://gist.github.com/aemxn/54b45709f6ac4dc7c2af875d4a3958e0

2.2.1 Implementing the backend

1. Creating feature Contract

/core/mvp/home/login/LoginContract.java

public interface LoginContract {

    interface ViewAction{
        void onUserLogin(@NonNull Login login);
    }

    interface LoginView extends RemoteView {
        void onLoginSuccess();
    }
}

ViewAction

Action/task to be passed to the Presenter from the UI. Such that, when user clicks a button, onUserLogin will be called. Presenter will handle the process.

LoginView

Once processing is done in the Presenter part, Presenter can call one of the interface method to make UI changes. In this example, after a login is successful, Presenter can call onLoginSuccess to tell UI it has done its work.

RemoteView

Similar to LoginView, contains other common UI changing interface methods such as showError, showEmpty etc. Put custom action in this interface.

2. Creating feature Presenter

/core/mvp/home/login/LoginPresenter.java

Pass other dependencies into the constructor.

public LoginPresenter(APIManager apiManager, PreferenceService preference,
                        INetworkManager networkManager, Repository<Login> loginRepository) {
    this.apiManager = apiManager;
    this.preference = preference;
    this.networkManager = networkManager;
    this.loginRepository = loginRepository;
}

Every Presenter class must extend a BasePresenter<View> and implement its corresponding ViewAction. Implement method as defined in the ViewAction. To make an API call, simply use apiManager object to call one of the API.

class LoginPresenter extends BasePresenter<LoginContract.LoginView> implements LoginContract.ViewAction {

    @Override
    public void onUserLogin(@NonNull Login login) {
        apiManager.loginUser(login, callback);
    }
}

Once we have extended BasePresenter and implement the correct ViewAction, we can already use the LoginView to change the UI state. isViewAttached() exists in BasePresenter. This method is to make sure we have attached a correct view instance into the Presenter. Using getView() method is how we access the View instance and do changes according to the Contract we have written earlier.

Example usage of getView():

getView().showProgress();
getView().hideProgress();
getView().showMessageLayout(false);
getView().onLoginSuccess();
getView().showEmpty();
getView().showError("An error has occurred");

For communication between the Presenter and the UI, see Bridging with the UI section below.

3. Creating feature Models

/core/data/model/Login/Login.java

/core/data/model/Login/LoginResponse.java

In this model, we define the payload required for the API. Included as well, the Builder methods generated by Intellij plugin, which I forgot which one I used.

4. Creating API endpoint integration

/core/data/network/AppAPI.java

@POST("user/login")
Call<LoginResponse> loginUser(@Body Login login);

Add a new endpoint into the interface list. In this case, Login API with user/login endpoint. Refer Retrofit docs for more info on @Body parameter.

/core/data/network/APIManager.java

public void loginUser(Login login, RemoteCallback<LoginResponse> listener) {
    mApi.loginUser(login).enqueue(listener);
}

Add a new concrete method to make use of loginUser interface we have just made earlier. Passing a response listener for OkHttp to make network call.

2.2.2 Bridging with the UI

Once everything in the backend is done, we can bridge Presenter with our UI

1. Extend BaseFragment/BaseActivity and implement corresponding *View in feature Contract

/ui/login/LoginActivity.java

public class LoginActivity extends BaseFragment implements LoginContract.LoginView {


    @Override
    public void showEmpty() {

    }

    @Override
    public void showError(String errorMessage) {

    }

    // ... other overriden methods

    @Override
    public void onLoginSuccess() {

    }

    @Override
    public void onLoginFailed(String message) {

    }

}

2. Create feature Presenter instance

For the sake of brevity and showing example for Login feature, we'll be using these common dependencies to be passed into our Presenter constructor.

Ideally, we wouldn't want to write such boilerplates each time we want to pass dependencies into a class. Dagger for dependency injections should be used in this case.

APIManager apiManager = APIManager.getInstance();
PreferenceService preferenceService = new PreferenceServiceImpl(this); 
NetworkManager networkManager = new NetworkManager(this);
DatabaseRealm databaseRealm = new DatabaseRealm();
Repository<Login> loginRepository = new LoginRepositoryImpl(databaseRealm); // not to be used because Login model doesn't extend RealmObject, but we're showing anyway

LoginPresenter presenter = new LoginPresenter(apiManager, preferenceService, networkManager, loginRepository);

3. Create feature Repository

The purpose of this class is to allow feature-specific DAO implementation to be used in our Presenter. Naming convention is used such that [FEATURE_NAME]RepositoryImpl. Repository implementation should implements Repository<?> interface.

/core/data/repository/impl/LoginRepository.java

public class LoginRepositoryImpl implements Repository<Login> {

    private DatabaseRealm databaseRealm;

    public LoginRepositoryImpl(DatabaseRealm databaseRealm) {
        this.databaseRealm = databaseRealm;
    }

    @Override
    public Login find(String guid) {
        return null;
    }

    @Override
    public List<Login> findAll() {
        return null;
    }

    // ... other overriden methods

    @Override
    public List<Login> query(Specification specification) {
        return null;
    }
}

A specification is like Data Access Object (DAO) where we specify our custom queries from Realm, other than common CRUD operations (find, findAll, add, remove etc).

  1. Create a Specification class inside core/data/repository/specification
  2. Implement any Specification interface available in core/data/repository/commons
  3. From the Specification class, we can define our own query for Realm
return realm.where(Restaurant.class)
        .equalTo(Restaurant.KEY_FAVORITE, true)
        .findAll();

For more information about Repository Pattern used in this post, check this article out.

4. Attach View instance to the Presenter

Allows Presenter to give callback commands to modify UI from within itself. Place attachView in either onCreate or onViewCreated for fragment

@Override
protected void onCreate(Bundle savedInstanceState) {


    presenter = new LoginPresenter(...);
    presenter.attachView(this);
}

Detach view instance on destroy to free up some memory

@Override
protected void onDestroy() {
    presenter.detachView(this);
    super.onDestroy();
}

5. Call any ViewAction method to be processed by the Presenter

@OnClick(R.id.login_btn)
public void onLoginClicked() {
    presenter.onUserLogin(username, password);
}

This will invoke the Presenter to process our data from the UI. A callback from Presenter must be written in order to change the state of this UI. Refer to Creating feature Presenter in Implementing the backend section for more details on how to change the state of the UI.

6. UI state change will occur in the ViewAction method

@Override
public void onLoginSuccess() {
    // do something with UI. eg. Toast.show()
}

@Override
public void onLoginFailed(String message) {
    // do something with UI. eg. Toast.show()
}

From here, you can do as much as you want with the UI. Be it showing Toasts, changing layouts, updating views etc. Since the control is done by our Presenter, it can also carry payload as well. So, front end guy would use your data from the backend to feed it to his dummy structured layout.

There are however things that should be improved in this design such as the usage of Dependency Injection pattern (we're using a lot of dependency instance), and testing suite for both unit and instrumentation.


Resources
  1. Repository pattern: https://medium.com/@krzychukosobudzki/repository-design-pattern-bc490b256006#.b8uer86v2
  2. MVP without Dagger/RxJava: https://android.jlelse.eu/avenging-android-mvp-23461aebe9b5#.qf5t6bzdj
  3. MVP and TDD: http://manabreak.eu/android/2016/10/26/mvp-tdd.html
  4. My reference list: https://hackmd.io/s/SJKO7oTCx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment