One of the user interface architectural patterns is Model View Presenter (MVP) which is widely used in Android apps to separate presentation components from data provider.
In this post, you can find information about MVP concepts and how to implement MVP in android app with an example.
MVP pattern separates model from view using presenter. With this separation, app components can be tested and modified independently (for example replacing data source), designers or developers can independently work on their respective components and components can be reused within app or across applications of an organization.
In MVP pattern, model is the data provider. View passes the user interface events to the presenter for results and displays the data received from model via presenter.
Presenter interacts with both model and view. It is a mediator between view and model. It receives the user events from view, interacts with model to get results and updates the view with the results.
In android applications, view is the activity or fragment. Model is the repository classes which provide data from local or remote data source. Presenter is a class which holds reference to view and model and interacts with them.
In Android apps, a screen or view consists of an activity or a fragment and their layouts. Layout defines UI components of user interface and activity or fragment handles UI events and populates results in UI components. So, view part of MVP is activity or fragment and its layout.
In apps, each UI event results in execution of certain behavior and return of result. The component which provides behavior and results is called model.
If an activity or a fragment directly calls repository or data source classes in response to UI events to get results, testing and modification of app will be difficult. This is where the presenter comes into picture. It separates view and model by mediating between them.
To implement MVP, the first thing that needs to be done is to decide what can be done in view and what should be delegated to presenter which intern interacts with model. Depending on your app and type organization, you can decide the degree of app logic that views in your app contains.
In MVP, view contains presenter and passes events to it. Presenter, which contains both view and model, interacts with model to get data and updates the view with results.
As shown in the diagram above, create view interface with methods that presenters calls, create presenter interface with the methods that view calls and inject the instance of presenter to view & instance of view to presenter. Inject instance of model to presenter. See below example for detailed explanation of calls between view, presenter, and model.
Let’s see how MVP can be implemented in android using donations tracking app example. The app lets user enter and save donation amount and it displays total money donated.
The read and write data events are passed to the presenter which in turn calls model to obtain the results and then updates the results to view.
One important point that needs to be mentioned before we get into the details of the example is that MVP may leak actions if not implemented properly as view holds a reference to presenter. If presenter can’t be garbage collected due to long running tasks, activity can’t be garbage collected leading to memory leak. And also, background thread started when an activity is open tries to update it with results after the activity is gone leading to app crash.
These issues can be prevented by cleaning resources in the activity or fragment lifecycle callback methods such as start, stop, pause, and resume. It is easy to clean background work if RxJava is used. The example uses RxJava and Room.
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import com.zoftino.mvpandroidexample.repository.LocalRepository;
public class DonationActivity extends AppCompatActivity implements ViewInterface{
@NonNull
private PresenterInterface presenter;
@NonNull
private TextView totalAmt;
@NonNull
private EditText newDonation;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_donation);
setUp();
}
private void setUp(){
//initialize db
LocalRepository.initiateDB(getApplication());
//set views
setViews();
//set presenter
presenter = new Presenter(this);
}
public void addNewDonation(View view){
addDonation(newDonation.getText().toString());
}
private void setViews(){
totalAmt = findViewById(R.id.d_amount);
newDonation = findViewById(R.id.add_amt);
}
@Override
public void addDonation(String donationAmt) {
presenter.addDonation(donationAmt);
}
@Override
public void setTotalDonation(double totalDonation) {
totalAmt.setText(""+totalDonation);
}
@Override
public void displaySaveMessage(String msg){
Toast.makeText(this, msg, Toast.LENGTH_LONG);
newDonation.setText("");
//refresh total donation on the screen
presenter.getTotalDonation();
}
@Override
protected void onPause() {
super.onPause();
//cleanup resources
presenter.stop();
}
@Override
protected void onResume() {
super.onResume();
//add resources
presenter.start();
}
}
import com.zoftino.mvpandroidexample.repository.Donation;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
public class Presenter implements PresenterInterface {
private CompositeDisposable compositeDisposable;
private ModelInterface model;
private ViewInterface view;
public Presenter(ViewInterface view) {
model = new DonationModel();
this.view = view;
}
@Override
public void start() {
compositeDisposable = new CompositeDisposable();
getTotalDonation();
}
@Override
public void stop() {
compositeDisposable.clear();
}
public void getTotalDonation() {
compositeDisposable.add(model.getDonation()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::setDonation));
}
private void setDonation(Donation donation) {
if (donation != null)
view.setTotalDonation(donation.getAmount());
}
//add donation
public void addDonation(String donation) {
double donationAmt;
try {
donationAmt = Double.parseDouble(donation);
} catch (NumberFormatException exception) {
sendSaveMessage("Please enter valid donation amount");
return;
}
//add observable to disposable and
//subscribe to the observable on main thread to send message to view
compositeDisposable.add(model.saveDonationAmt(donationAmt)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::sendSaveMessage));
}
//add donation status message to view
private void sendSaveMessage(String msg) {
view.displaySaveMessage(msg);
}
}
import com.zoftino.mvpandroidexample.repository.Donation;
import com.zoftino.mvpandroidexample.repository.DonationDAO;
import com.zoftino.mvpandroidexample.repository.LocalRepository;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
public class DonationModel implements ModelInterface{
private DonationDAO donationDAO;
public DonationModel(){
donationDAO = LocalRepository.getDonationDAO();
}
@Override
public Maybe<Donation> getDonation() {
return Maybe.just(1).subscribeOn(Schedulers.computation())
.flatMap(this::getDonationObj);
}
private Maybe<Donation> getDonationObj(int i) {
return donationDAO.getDonation();
}
@Override
public Donation getDonationObject(){
return donationDAO.getDonationObject();
}
@Override
public Observable<String> saveDonationAmt(double donationAmt) {
return Observable.just(donationAmt).subscribeOn(Schedulers.computation())
.flatMap(this::saveDonation);
}
private Observable<String> saveDonation(double donationAmt){
try {
Donation donation = getDonationObject();
if (donation == null) {
donation = new Donation();
donation.setId(1);
}
donation.setAmount(donation.getAmount() + donationAmt);
donationDAO.saveDonation(donation);
}catch (Exception e){
return Observable.just("Donation couldn't be saved");
}
return Observable.just("Donation saved");
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp"
tools:context=".DonationActivity">
<TextView
android:id="@+id/d_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Total Donations So Far"
android:textSize="24dp"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/d_amount"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/d_amount"
android:layout_marginTop="24dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="33333"
android:textSize="24dp"
android:textStyle="bold"
app:layout_constraintLeft_toRightOf="@+id/d_title"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/add_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Enter new donation amount"
android:textSize="24dp"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/d_title" />
<EditText
android:id="@+id/add_amt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Enter new donation"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_title" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="addNewDonation"
style="@style/Widget.AppCompat.Button.Colored"
android:text="Add New Donation"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_amt" />
</android.support.constraint.ConstraintLayout>