Disclaimer: This post is quite old and better API's are out there now. Please read up on Android architecture view models.
This post was originally written for Mindorks publication on Medium. This post will be split into two parts. The first part explains the need, the job of each component of MVP and then the actual code to make your project organized in Model-View-Presenter(MVP) pattern. While the second part will focus on Loader API, on how to use it to save your presenters during rotation.
The example project for this post is at this link.
The only-mvp branch of the example project deals with MVP only. The master branch shows an example of MVP and Loader API. We will cover the master branch in the second post.
Let’s understand the need for MVP
We all started with making small projects and as time went on, the complexity of these projects started to increase, bringing in chaos.
“The Only Thing That Is Constant Is Change -”― Heraclitus
And the time to make these changes or new features in our chaotic complex project kept increasing. Also the amount of code which is deleted or shifted or is written new to make these changes kept increasing. And how can we forget about the bugs? Complexity increase brings all kinds of bugs and we first have to locate the bug before we can squash it. But we can’t locate it in that messy code, can we?
We slowly realize how the mixing of different parts of code led to time being wasted on stupid bugs and how hard it made making changes or adding new things making our project unmaintainable. We slowly also realize our mess has been handling all kind of stuff regarding our view, transformation logic, navigation logic and business logic. We can’t even test any logic or view as all of it is too interdependent.
We realize that what we don’t have is separation of concern and it is causing us many kind of problem. Separation of concern means that the concern of how data is shown to the user, how it is fetched and what transformation we applied on it should all be separate so we easily change any of them without worrying about affecting the other one.
This is the need of Model-View-Presenter (MVP) pattern —Separation of concern. Now that we know we need it, we need to understand the job of each component.
Job of Model
This component’s job is to implement the business logic and handling of data. Handling of data refers to the fetching of data( Either from local cache, or from external storage or from server in a network call) and then saving it in memory or cache or external storage.
Let’s take an example to understand it : You have a button in your app and it loads some recipes from the server but only for premium accounts. Now when a user clicks on it, the network request reaches the models which is implementing the business logic, so it first checks with the user data stored if the user has a premium account or not. If the account is not premium, the request is denied and an upgrade dialog is shown. Otherwise, the data is fetched from the server and is saved in the database to be shown instantly the next time. Here is some pseudo code for it for better understanding:
class Model {
UserModelHelper userModel;
RecipeModelHelper recipeModel;
NetworkManager networkManager;
/*
Make the constructor here and initialize the model helpers
like UserModel and other managers
*/
/* Checks if the account is premium or not. Throws an
exception if not, otherwise proceeds with network call.
The business logic is to only give this option to
the premium account holders. This type of logic goes
in the model*/
List<Recipe> onLoadRecipes() {
if (userModel.isAccountPremium()) {
// Fetches the recipes
Result result = networkManager.getRecipes();
// Saves the recipes data
recipeModel.saveRecipes(result.getRecipes());
// Send the result back to the presenter.
return result.getRecipes();
} else {
/* We can show an upgrade dialog when we catch
this */
throw new UpgradeAccountException();
}
}
}
Job of View
This component only job is related to the views that are being displayed. It doesn’t know how they are fetched or even if they are coming from the cache or from a network call. Activity, Fragment etc. components come under this.
Continuing the example, you have a recipe screen where you show the list of recipes or an upgrade dialog. Now this view will have this kind of pseudo code:
class RecipeView {
// Lifecycle and other instantiation events.
/*
updateUI function is called with RecipeData object
holding the list to be shown
*/
void updateUI(RecipeData data) {
if (data.getError()) {
// show upgrade dialog.
}
else {
recipeListAdapter.set(data.getRecipesList());
}
}
}
Job of Presenter
This component is the handler of logic and decides when, what and how to display data. It is responsible for getting the UI events, asking the model to make necessary changes, applying transformation and caching results before sending the data back to the view.
In the example we have seen, the user wanted to fetch the recipes and the presenter asked the model to fetch the recipes. This is the first role of presenter — handle UI events and ask model for data. When those recipes results came back, it cached them, and then shipped it to the view. This is the second role of presenter — cache and send data to be displayed to the View. Maybe the user now applied a filter to the recipes. We have already fetched the whole list of recipes. Do we fetch the filtered recipes again? No, we will simply apply a transformation filter on the cached recipes. This is the third role of presenter — transform data if needed. Here is some pseudo code for it :
class RecipePresenter {
Model model;
View view;
List<Recipe> list;
RecipeData data;
// Make the constructor here and initialize the model and view
/* First and second role of presenter- asking model for data
on button click. Caching the recipes in 'data' variable and
then sending the results back */
void loadRecipes() {
try {
list = model.onLoadRecipes();
data.setRecipesList(list);
} catch (UpgradeAccountException exception) {
data.setError(true);
}
view.updateUI(data);
}
// Third role of presenter - transform data
void loadFilterRecipe() {
List<Recipe> filterList = list.someFilter();
data.setRecipesList(filterList);
view.updateUI(data);
}
}
Implementation of MVP
Now that you understand what is the job of each MVP component, lets dive into how we actually implement it in code.
Suppose we have a Activity acting as our view. First we make the presenter object in our activity onCreate(). To connect the presenter and the view, we use an interface. The interface defines all the functions that presenter can call on the view and they will all be somehow to update the UI. The interface is as follows:
interface MyViewInterface {
/* MyState class is a class which contains all the data needed
to display the UI of the said activity. */
void updateUI(MyState state);
}
We implement it in our activity so that presenter can send UI updates to us. We say that we attach the view in onResume() to the presenter, i.e. we pass an interface implementation asking the presenter to give us UI updates on this. And in onPause(), we set it to null as we can’t display UI updates anymore, hence we don’t need UI updates from presenter. The code for the view (in this case an activity is as follows) :
public class MyActivity extends AppCompatActivity implements MyViewInterface {
private MyPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
presenter = new MyPresenter(DataManager.getDataManager());
}
@Override
protected void onStart() {
super.onStart();
// instantiates views and sets on click on them.
initView();
}
@Override
protected void onPause() {
// Detach the view as UI isn't going to be visible
presenter.detachView();
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
// Attach the view interface as we want UI updated again.
presenter.attachView(this);
}
private void initView() {
// initialize views and set on click and other stuff here
/* the following line isn't syntactically correct but I
just want to give an idea */
submitButton.setOnClickListener(presenter.saveName(name));
}
@Override
public void updateUI(MyState state) {
/* Update the UI using the MyState object*/
}
}
Presenter will have attachView() and detachView() functiong alongside some other functions to handle UI events from the view. The presenter is as follows:
class MyPresenter {
private MyViewInterface viewInterface;
private DataManager dataManager;
private MyState state;
MyPresenter(DataManager dataManager) {
this.dataManager = dataManager;
/* Initiates state with a default value for the initial UI
of this view */
state = new MyState("This is the template data");
}
/**
* Called when the UI became visible in onResume, thus we
save the interface object and update the UI with the cached
state */
void attachView(MyViewInterface viewInterface) {
this.viewInterface = viewInterface;
viewInterface.updateUI(state);
}
/**
* Called when the UI became invisible, thus we set the
interface object to null as we can't dispatch more UI
updates but we keep our state object up to date with
the model changes and will update the UI when it attaches
again. */
void detachView() {
this.viewInterface = null;
}
/* Button click UI event from submitButton in view to save
data in the model which we do using dataManager */
void saveName(String name) {
dataManager.saveName(name);
state.setNameSaved(name);
viewInterface.updateUI(state);
}
}
And DataManager is the class which acts as the interface for every presenter to interact with the model. In my opinion it should be kept a singleton. You can use the lazy instantiation method to instantiate it with application context in the application class.
//MyApplication.java
//onCreate()
DataManager.init(this.getApplicationContext());
To access it we can then use the public static method in DataManager called getDataManager() which returns the singleton reference. The Model pseudo code that we saw when understanding the job of Model component will actually be present in DataManager class. It also delegates the tasks to other managers, for example: network requests for model are delegated to NetworkManager, SharedPreference changes to SharedPreferencesHelper etc. It is also responsible for checking that if we have cache data, then update the UI with it while proceeding with network call, and when the network call returns, update the database and cache , and update the UI again. The sample code for the data manager :
public class DataManager {
private static DataManager dataManager;
private final Context context;
private SharedPreferencesHelper sharedPreferencesHelper;
private DataManager(Context context) {
this.context = context;
/* Create your delegation managers here like
NetworkManager, SharedPreferencesHelper etc. */
sharedPreferencesHelper = new SharedPreferencesHelper();
}
public static void init(Context context) {
dataManager = new DataManager(context);
}
public static DataManager getDataManager(){
return dataManager;
}
/**
* Change the name in the appropriate model. It can be
shared preferences, or SQL db or Realm db or making a
network call (Dispatched via Network Manager) to save the
name in server. Here I'm using a shared preference as an
example */
public void saveName(String name) {
sharedPreferencesHelper.saveName(context, name);
}
}
Please check out the example project for full code implementation. The next part will cover Loader API and rotation problem.