Android MVP Architecture Extension with Interactors and Repositories
Evolution is the truth of nature and new developments are guided by the natural selection. This results in a lot of different species with many similarities and all adapt best in their habitat. The notion that one is better than the other is only valid in bounded conditions where one is more suited for survival. When the conditions change so is the notion. This is also true for any software design. One solution is better than the other in its own scope.
Sometime back I wrote a series of articles on the design of MVP architecture for Android. The project was very well received in the community and hundreds of companies and developers added the architecture in their applications. I have also implemented the same architecture for my Android app. With the exposure of this architecture to many use cases, some enhancements were identified to suit those particular conditions.
So, as I mentioned some enhancements were identified to adapt better to some situations. All these situations emerged for Android apps having a huge codebase with many developers working on the project simultaneously.
The limitations that were identified in those conditions were two:
- Centralization of DataManager: The DataManager in the MVP architecture is designed to delegate all responsibilities to ApiHelper, DbHelper, and PreferenceHelper. It is a good design for most of the use cases because it empowers to intercept the Helper calls and perform some pre-processing. However, for large projects, this class tends to become very large.
- Centralization of DbHelper: The DbHelper was designed to fetch data from the database using the DAO classes. This is also good for most of the projects with fewer DB queries but for large projects tend to become very large.
Potentially Centralization of DataManager problem could be solved by exposing the Helper instances to the Presenters. This will remove the DataManager delegation calls and hence will reduce the code base to few methods that require cross Helpers calls or series of calls made to a Helper. I would recommend this solution to the current MVP architecture.
Tapping an unbound potential requires taking two steps ahead after we have reached our destination.
Now I propose some fundamental changes to the existing MVP architecture to make it suitable for very large projects with many developers working on it simultaneously. First, let’s identify the challenges to such a projects.
- Large projects have many features and to manage those features, it needs to have a modularize code base. Even in a module, the best approach is to segregate the codes such that all the features are encapsulated, meaning that all the dependencies for that feature can be at most resolved in its package.
- For many developers to work on the same project and to avoid the code merge with each pull request, we need to do the features encapsulation.
Let’s see how can we extend the current MVP project to incorporate the features encapsulation.
Current Project has this structure:
- DataManager has ApiHelper, DbHelper and PreferenceHelper.
- Presenter gets DataManager singleton instance and the DataManager delegates all the Helper calls.
- View is attached with its Presenter.
Before you proceed further I would recommend you to go through the original MVP architecture articles:
Proposed change for the feature encapsulation is the decentralization of DataManager and DbHelper.
DataManager decentralization results in a structure we call it as an Interactor and DbHelper decentralization results in Repositories.
- Interactor: Each Presenter gets its own instance of an Interactor in place of a singleton DataManager. The interactor is responsible for the same responsibilities as the DataManager but it is only concerned with those data access and processing calls that are required by the feature that it is serving.
- Repository: The DbHelper is broken into Repositories. For example, all the database queries for a User table is made through an UserRepository and not in the DbHelper. UserRepository, similar to DbHelper, makes all its concerned queries through UserDao. An important difference is that the Repository is an on demand instantiation and each Interactor gets a new Repository instance in oppose to a singleton DbHelper.
Here is the link to this GitHub project:
Let’s see that architecture blueprint with these new components:
Comparing it with the original MVP architecture blueprint: Image Link
We can see that the DataManager and DbHelper are replaced by the Interactors and Repositories which are separately available for each Presenter.
Important changes in the MVP architecture:
- MvpInteractor: It is an interface that lists the data access APIs common to all application components and access method for the singleton ApiHelper and PreferencesHelper.
public interface MvpInteractor {
ApiHelper getApiHelper();
PreferencesHelper getPreferencesHelper();
void setUserAsLoggedOut();
void setAccessToken(String accessToken);
...
}
- BaseInteractor: It implements the MvpInteractor. All the subsequent Interactors extend this BaseInteractor. BaseInteractor gets the singleton ApiHelper, and PreferenceHelper.
public class BaseInteractor implements MvpInteractor {
private final PreferencesHelper mPreferencesHelper;
private final ApiHelper mApiHelper;
@Inject
public BaseInteractor(PreferencesHelper preferencesHelper, ApiHelper apiHelper) {
mPreferencesHelper = preferencesHelper;
mApiHelper = apiHelper;
}
@Override
public ApiHelper getApiHelper() {
return mApiHelper;
}
@Override
public PreferencesHelper getPreferencesHelper() {
return mPreferencesHelper;
}
@Override
public void setAccessToken(String accessToken) {
getPreferencesHelper().setAccessToken(accessToken);
getApiHelper().getApiHeader()
.getProtectedApiHeader()
.setAccessToken(accessToken);
}
...
}
- Component wise Interactors extends the BaseInteractor and implement the interface that extends the MvpInteractor that adds its own APIs in them. Example: LoginMvpInteractor extends MvpInteractor and add doServerLoginApiCall, doGoogleLoginApiCall, and doFacebookLoginApiCall. Similarly LoginInteractor extends BaseInteractor and implements LoginMvpInteractor.
Example: LoginMvpInteractor
public interface LoginMvpInteractor extends MvpInteractor {
Observable<LoginResponse> doServerLoginApiCall(
LoginRequest.ServerLoginRequest request);
Observable<LoginResponse> doGoogleLoginApiCall(
LoginRequest.GoogleLoginRequest request);
Observable<LoginResponse> doFacebookLoginApiCall(
LoginRequest.FacebookLoginRequest request);
}
Its implementation by LoginInteractor. Notice that a new instance of the UserRepository is provided to this Interactor.
public class LoginInteractor extends BaseInteractor
implements LoginMvpInteractor {
private UserRepository mUserRepository;
@Inject
public LoginInteractor(PreferencesHelper preferencesHelper, ApiHelper apiHelper, UserRepository userRepository) {
super(preferencesHelper, apiHelper);
mUserRepository = userRepository;
}
@Override
public Observable<LoginResponse> doServerLoginApiCall(
LoginRequest.ServerLoginRequest request) {
return getApiHelper().doServerLoginApiCall(request);
}
...
}
- The Presenter has the same structure as the original MVP architecture, with only difference is the DataManager is replaced by the component’s interactor. Example: LoginMvpPresenter interface. Notice the addition of Interactor along with View in the definition.
@PerActivity
public interface LoginMvpPresenter<V extends LoginMvpView,
I extends LoginMvpInteractor> extends MvpPresenter<V, I> {
void onServerLoginClick(String email, String password);
void onGoogleLoginClick();
void onFacebookLoginClick();
}
Its implementation by the LoginPresenter:
public class LoginPresenter<V extends LoginMvpView, I extends LoginMvpInteractor>
extends BasePresenter<V, I> implements LoginMvpPresenter<V, I> {
private static final String TAG = "LoginPresenter";
@Inject
public LoginPresenter(I mvpInteractor,
SchedulerProvider schedulerProvider,
CompositeDisposable compositeDisposable) {
super(mvpInteractor, schedulerProvider, compositeDisposable);
}
...
}
- Repository is created for each Db Table wrapper Object. DAO in this project is generated by the greenDAO library.
Example UserRepository:
public class UserRepository {
private final DaoSession mDaoSession;
@Inject
public UserRepository(DaoSession daoSession) {
mDaoSession = daoSession;
}
public Observable<Long> insertUser(final User user) {
return Observable.fromCallable(new Callable<Long>() {
@Override
public Long call() throws Exception {
return mDaoSession.getUserDao().insert(user);
}
});
}
...
}
The new project structure:
Compare this with the original MVP project structure: Image Link
You can observe that the login feature components are independent of other application components and has all the dependencies encapsulated. Any developer working on the login feature can work independently to other developers working on separate features.
You don’t need an atom bomb to kill an ant.
These components make the architecture more extensible and flexible but at the cost of more work. I must repeat again original MVP architecture is suited for most projects of decent complexity and this extended version is suited to very large projects with hundreds of features and many developers working on it simultaneously. We are using the original architecture very happily in our own code base of our app.
Note:
ApiHelper and PreferenceHelper are not broken into parts because the Android app dependency on them is very limited.
The link to the sample app project of MVP extended with Interactor and Repository.
Learning is a journey, let’s learn together!