MODEL VIEW PRESENTER (MVP) IN ANDROID, PART 2

In the last article we talked about Model View Presenter (MVP) concepts, and its advantages concerning Android development. In the second part of this series we’ll get the hands dirty and implement our own version of MVP, using canonical form without any libraries from outside Android SDK/Java.

We’ll develop a simple code, but it could look a little bit complex, due to the amount of objects involved. Although, once you get the handle of it, you’ll see how this kind of pattern could help you in your projects. If you prefer to learn directly from code reading, you could check out the final project.

Planning Model View Presenter (MVP)

[expand title=”click to visualize MVP main concepts in Android” tag=”h5″]

Presenter
The Presenter is responsible to act as the middle man between View and Model. It retrieves data from the Model and returns it formatted to the View. But unlike the typical MVC, it also decides what happens when you interact with the View.
View
The View, usually implemented by an Activity, will contain a reference to the presenter. The only thing that the view will do is to call a method from the Presenter every time there is an interface action.
Model
In an application with a good layered architecture, this model would only be the gateway to the domain layer or business logic. See it as the provider of the data we want to display in the view.
These excellent definitions above were extracted from Antonio Leiva’s article.

[/expand]

Our greatest objective with MVP pattern is to increase the separation of concerns of our project. Therefore, we need to ensure the isolation between the layer Model, View and Presenter. In this context, View and Model cannot communicate directly, hence the Presenter intermediates all relations among the layers.

Model View Presenter action diagram

Let’s imagine an extremely simple application, that allows the user to take note on a journal. Basically, the user inserts notes while the system saves and exhibit the data. If we trace the insert note action, considering that the app was developed using MVP pattern, we’ll get the following diagram:

  1. User clicks on “insert note”.View sends note toPresenter getPresenter.newNote(textNote)
  2. Presenter creates a new Note, using the given String sent and invokes on Model the method responsible to insert the data on DB getModel.insertNote(note, this)
  3. Model inserts the Note on DB and informs Presenter about the success/error using the callback sent callback.onSuccess()
  4. Presenter processes the result and require View to exhibit a Toast success message getView.showToast(msg)

This mapping gives us a better idea for our classes planning. The communication process defined above could be different: with direct object access, using interfaces or maybe with some kind of EventBus. However, since our implementation respects the canonical form and we aim to increase the isolation of concerns, we’ll use only plain and simple interfaces.

Model View Presenter class diagram

Let’s use our action diagram to construct a MVP Class Diagram. We’ll change a little bit our concept, switching from callback to interface, to send results back to Presenter from Model. I believe this path is more efficient, but some could argue that using callbacks the isolation of concerns would increase.

  1. Presenter implements interface PresenterOps
  2. View receives reference from PresenterOps to access Presenter
  3. Model implements interface ModelOps
  4. Presenter receives reference from ModelOps to access Model
  5. Presenter implements RequiredPresenterOps
  6. Model receives reference from RequiredPresenterOps to access Presenter
  7. View implements RequiredViewOps
  8. Presenter receives reference from RequiredViewOps to access View

Implementing Model View Presenter on Android

Without further ado, let’s get to work! We’ll start defining the app operations. In the name of a better organization, we’ll use an “umbrella” class, containing all interfaces responsible to manage the communication between the layers.

Note: Since MVP implementation is complex enough, no function outside the pattern scope will be developed. I suppose that the readers have a more advanced understanding of the Android SDK, therefore such things shouldn’t be a concern.

Interface MainMVP

/*
* Aggregates all communication operations between MVP pattern layer:
* Model, View and Presenter
*/
public interface MainMVP {
    /**
* View mandatory methods. Available to Presenter
* Presenter -> View
*/
interface RequiredViewOps {
void showToast(String msg);
void showAlert(String msg);
// any other ops
}
    /**
* Operations offered from Presenter to View
* View -> Presenter
*/
interface PresenterOps{
void onConfigurationChanged(RequiredViewOps view);
void onDestroy(boolean isChangingConfig);
void novaNota(String textoNota);
void deletaNota(Nota nota);
// any other ops to be called from View
}
    /**
* Operations offered from Presenter to Model
* Model -> Presenter
*/
interface RequiredPresenterOps {
void onNotaInserida(Nota novaNota);
void onNotaRemovida(Nota notaRemovida);
void onError(String errorMsg);
// Any other returning operation Model -> Presenter
}
    /**
* Model operations offered to Presenter
* Presenter -> Model
*/
interface ModelOps {
void insereNota(Nota nota);
void removeNota(Nota nota);
void onDestroy();
// Any other data operation
}
}

MainPresenter Class

public class MainPresenter
implements MainMVP.RequiredPresenterOps, MainMVP.PresenterOps {
    // Layer View reference
private WeakReference<MainMVP.RequiredViewOps> mView;
// Layer Model reference
private MainMVP.ModelOps mModel;
    // Configuration change state
private boolean mIsChangingConfig;
    public MainPresenter(MainMVP.RequiredViewOps mView) {
this.mView = new WeakReference<>(mView);
this.mModel = new MainModel(this);
}
    /**
* Sent from Activity after a configuration changes
* @param view View reference
*/
@Override
public void onConfigurationChanged(MainMVP.RequiredViewOps view) {
mView = new WeakReference<>(view);
}
    /**
* Receives {@link MainActivity#onDestroy()} event
* @param isChangingConfig Config change state
*/
@Override
public void onDestroy(boolean isChangingConfig) {
mView = null;
mIsChangingConfig = isChangingConfig;
if ( !isChangingConfig ) {
mModel.onDestroy();
}
}
    /**
* Called by user interaction from {@link MainActivity}
* creates a new Note
*/
@Override
public void newNote(String noteText) {
Note note = new Note();
note.setText(textoNota);
note.setDate(getDate());
mModel.insertNote(note);
}
    /**
* Called from {@link MainActivity},
* Removes a Note
*/
@Override
public void removeNote(Note note) {
mModel.removeNote(note);
}
    /**
* Called from {@link MainModel}
* when a Note is inserted successfully
*/
@Override
public void onNoteInsert(Note newNote) {
mView.get().showToast("New register added at " + newNote.getDate());
}
    /**
* Receives call from {@link MainModel}
* when Note is removed
*/
@Override
public void onNoteRemoved(Note noteRemoved) {
mView.get().showToast("Note removed);
}
    /**
* receive errors
*/
@Override
public void onError(String errorMsg) {
mView.get().showAlert(errorMsg);
}
}

MainModel Class

public class MainModel
implements MainMVP.ModelOps {
    // Presenter reference
private MainMVP.RequiredPresenterOps mPresenter;
    public MainModel(MainMVP.RequiredPresenterOps mPresenter) {
this.mPresenter = mPresenter;
}
    /**
* Sent from {@link MainPresenter#onDestroy(boolean)}
* Should stop/kill operations that could be running
* and aren't needed anymore
*/
@Override
public void onDestroy() {
// destroying actions
}
    // Insert Note in DB
@Override
public void insertNote(Note note) {
// data business logic
// ...
mPresenter.onNoteInserted(note);
}
    // Removes Note from DB
@Override
public void removeNote(Note note) {
// data business logic
// ...
mPresenter.onNoteRemoved(note);
}
}

Dealing with Android particularities

In our MVP vision, the layer View is responsible to create layer Presenter, who is encharged to instantiate the Model. Considering that an Activity receives the View role, we need to consider some Android specifics, specially the lifecycle, that destroys and creates activities and its objects at any given moment.

That said, we’ll need to add a fourth element, the StateMaintainer, responsible to maintain the Presenter and Model state during lifecycle changes. A retained fragment will be the base to the Object and a simplified version of MVP on the Activity’s lifecycle would look like this:

  1. Activity creates a Presenter instance, saving a PresenterOps reference. The Presenter is saved in the StateMaintainer
  2. Presenter receives RequiredViewOps during its creation and instantiates a new Model
  3. Model receives RequiredPresenterOps
  4. When Activity is being destroyed, it informs to Presenter about it
  5. Presenter processes the information, takes the necessary measures and passes the info to Model
  6. Activity recovers the Presenter from StateMaintainer, sends RequiredViewOps and informs about its active state.

StateMaintainer Class

This implementation of the StateMaintainer can be used to save any object state.

public class StateMaintainer {
protected final String TAG = getClass().getSimpleName();
    private final String mStateMaintenerTag;
private final WeakReference<FragmentManager> mFragmentManager;
private StateMngFragment mStateMaintainerFrag;
    /**
* Constructor
* @param fragmentManager FragmentManager reference
* @param stateMaintainerTAG the TAG used to insert the state maintainer fragment
*/
public StateMaintainer(FragmentManager fragmentManager, String stateMaintainerTAG) {
mFragmentManager = new WeakReference<>(fragmentManager);
mStateMaintenerTag = stateMaintainerTAG;
}
    /**
* Create the state maintainer fragment
* @return true: the frag was created for the first time
* false: recovering the object
*/
public boolean firstTimeIn() {
try {
// Recovering the reference
mStateMaintainerFrag = (StateMngFragment)
mFragmentManager.get().findFragmentByTag(mStateMaintenerTag);
            // Creating a new RetainedFragment
if (mStateMaintainerFrag == null) {
Log.d(TAG, "Creating a new RetainedFragment " + mStateMaintenerTag);
mStateMaintainerFrag = new StateMngFragment();
mFragmentManager.get().beginTransaction()
.add(mStateMaintainerFrag, mStateMaintenerTag).commit();
return true;
} else {
Log.d(TAG, "Returns a existent retained fragment existente " + mStateMaintenerTag);
return false;
}
} catch (NullPointerException e) {
Log.w(TAG, "Error firstTimeIn()");
return false;
}
}

    /**
* Insert Object to be preserved during configuration change
* @param key Object's TAG reference
* @param obj Object to maintain
*/
public void put(String key, Object obj) {
mStateMaintainerFrag.put(key, obj);
}
    /**
* Insert Object to be preserved during configuration change
* Uses the Object's class name as a TAG reference
* Should only be used one time by type class
* @param obj Object to maintain
*/
public void put(Object obj) {
put(obj.getClass().getName(), obj);
}

    /**
* Recovers saved object
* @param key TAG reference
* @param <T> Class type
* @return Objects
*/
@SuppressWarnings("unchecked")
public <T> T get(String key) {
return mStateMaintainerFrag.get(key);
    }
    /**
* Verify the object existence
* @param key Obj TAG
*/
public boolean hasKey(String key) {
return mStateMaintainerFrag.get(key) != null;
}

    /**
* Save and manages objects that show be preserved
* during configuration changes.
*/
public static class StateMngFragment extends Fragment {
private HashMap<String, Object> mData = new HashMap<>();
        @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Grants that the frag will be preserved
setRetainInstance(true);
}
        /**
* Insert objects
* @param key reference TAG
* @param obj Object to save
*/
public void put(String key, Object obj) {
mData.put(key, obj);
}
        /**
* Insert obj using class name as TAG
* @param object obj to save
*/
public void put(Object object) {
put(object.getClass().getName(), object);
}
        /**
* Recover obj
* @param key reference TAG
* @param <T> Class
* @return Obj saved
*/
@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) mData.get(key);
}
}
}

MainActivity Activity (View layer)

public class MainActivity extends AppCompatActivity
implements MainMVP.RequiredViewOps {
    protected final String TAG = getClass().getSimpleName();
    // Responsible to maintain the Objects state
// during changing configuration
private final StateMaintainer mStateMaintainer =
new StateMaintainer( this.getFragmentManager(), TAG );
    // Presenter operations
private MainMVP.PresenterOps mPresenter;
    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startMVPOps();
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
}

    /**
* Initialize and restart the Presenter.
* This method should be called after {@link Activity#onCreate(Bundle)}
*/
public void startMVPOps() {
try {
if ( mStateMaintainer.firstTimeIn() ) {
Log.d(TAG, "onCreate() called for the first time");
initialize(this);
} else {
Log.d(TAG, "onCreate() called more than once");
reinitialize(this);
}
} catch ( InstantiationException | IllegalAccessException e ) {
Log.d(TAG, "onCreate() " + e );
throw new RuntimeException( e );
}
}

    /**
* Initialize relevant MVP Objects.
* Creates a Presenter instance, saves the presenter in {@link StateMaintainer}
*/
private void initialize( MainMVP.RequiredViewOps view )
throws InstantiationException, IllegalAccessException{
mPresenter = new MainPresenter(view);
mStateMaintainer.put(MainMVP.PresenterOps.class.getSimpleName(), mPresenter);
}
    /**
* Recovers Presenter and informs Presenter that occurred a config change.
* If Presenter has been lost, recreates a instance
*/
private void reinitialize( MainMVP.RequiredViewOps view)
throws InstantiationException, IllegalAccessException {
mPresenter = mStateMaintainer.get( MainMVP.PresenterOps.class.getSimpleName() );
        if ( mPresenter == null ) {
Log.w(TAG, "recreating Presenter");
initialize( view );
} else {
mPresenter.onConfigurationChanged( view );
}
}

    // Show AlertDialog
@Override
public void showAlert(String msg) {
// show alert Box
}
    // Show Toast
@Override
public void showToast(String msg) {
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show;
}
}

Check the full project on GitHub

On the next article

This post was a little too big, I know, sorry about that. But I really hope that it helped somebody out there. In the next article of the series, we’ll discuss how to use the final framework, that has some abstraction to facilitate the MVP implementation, and we’ll also discuss some hiccups that could get on the way of the Model View Presenter adoption in Android.

See you soon!