Robopupu.MVP API

Robopupu library uses Model-View-Presenter (MVP) architectural design pattern to support unit testing and separation of concerns for presentation logic of an Android application and its features. Good discussion about using MVP in the development of Android applications can be found from Antonio Leiva’s blog. The key classes of the Robopupu.MVP API are depicted in the following class diagram. Classes with white background represent API classes and classes with light blue background are classes written for an example implementation  (see the source codes for the Robopupu sample application).

mvp_key_classes

View

The View is implemented as a passive interface that displays data (from the model), executes animations (if any), and routes user interaction events to the Presenter to act upon that data and to perform other actions. A View is typically implemented as a Fragment or as an Activity. The former is recommended. Robopupu library provides a set of abstract base classes for implementations of the View interface

The View interface is defined as follows:


package com.robopupu.api.mvp;

public interface View {
    ViewState getState();
    String getViewTag();
}

The View interface defines the following methods:

  • getState() : Each View implementation keeps track of its lifecycle state using an instance of ViewState.
  • getViewTag() : Returns a tag that is used for identifying a View instance, for instance, when adding it to back stack. The tag is also used in MVP API’s dependency management. Dependency management is needed, for instance, when an Activity or Fragment is recreated because of, for instance, a configuration change. The default implementation of this method returns the class name of the View.

Presenter

A Presenter receives the user interaction events from its attached View and acts upon them. An  implementation of Presenter contains the logic for obtaining from Model(s) and formatting the data to be displayed in the View. One such model could be a presentation model, a ListModel implementation for a RecyclerView. Presenters should not have any references to Android APIs so that they can be unit tested as POJOs.

The Presenter interface is defined as follows:


package com.robopupu.api.mvp;
import com.robopupu.api.util.Params;

public interface Presenter extends ViewListener {
    View getView();
    void setParams(Params params);
    void finish();
}

By default, a concrete implementation of Presenter implements also the ViewListener interface so it receives the lifecycle events from an attached Fragment or Activity. An abstract base class AbstractPresenter provides default implementation for these events.

The Presenter interface defines the following methods:

  • getView(): Returns the View attached to the Presenter instance.
  • setParams(Params): Method can be used to provide parameters for a Presenter instance. Params is an extended HashMap with utility methods.
  • finish(): Invoked to finish a Presenter. The finished Presenter will notify its listeners about its finished state.

Model

The Model is just an object that provides data to be displayed in a View. The MVP API does not set any specific requirements for implementing Model objects, but provides an interfaces Model and Model.Listener, class ModelEvent, enum ModelEventType, and an abstract base class AbstractModel for implementing them.

Implementing Presenters

Robopupu MVP API provides couple of abstract base classes for implementing Presenters: AbstractPresenter and AbstractFeaturePresenter. The latter is intended to be used with Robopupu.Feature API. These abstract base classes utilises the two other APIs of Robopupu:

  1. Robopupu.Dependency API: AbstractPresenter implements Scopeable interface which means that a DependencyScope can be assigned to it. This in turn means that the assigned DependencyScope can be used to resolve dependencies for the Presenter implementation.
  2. Robopupu.Plugin API: AbstractPresenter implements PluginComponent interface which means that a concrete implementations of Presenter are plugged to a PluginBus and it can communicate with the View and other components via PluginBus.

Let’s use the About feature of the Robopupu sample application to provide an example how to implement a presenter. The About feature has few screens. One of those the main screen of the feature, the About screen. It is implemented using AboutPresenter, AboutPresenterImpl, AboutView, and AboutFragment classes. The classes are depicted in the UML class diagram on the top of this page.

The AboutPresenter interface is pretty simple. It is declared to define a plugin interface using the @PlugInterface annotation. This is because Robopupu architecture implements features, presenters, and views as plugin components that communicate via a plugin bus. AboutPresenter is extended from the FeaturePresenter interface, because it is part of a feature implementation. AboutPresenter declares three event methods, one for each clickable item defined in the layout of AboutFragment class. Each method declaration is annotated with the @OnClick annotation to declare a code generated binding from the clickable item to the implementation of the annotated method. This binding with annotation is described in detail later on this page.


package com.robopupu.feature.about.presenter;
import com.robopupu.api.feature.FeaturePresenter;
import com.robopupu.api.mvp.OnClick;
import com.robopupu.api.plugin.PlugInterface;

@PlugInterface
public interface AboutPresenter extends FeaturePresenter {
    @OnClick void onViewLicenseClick();
    @OnClick void onViewOssLicensesClick();
    @OnClick void onViewSourcesClick();
}

The interface declared above is implemented with class AboutPresenterImpl which extends class AbstractFeaturePresenter. See the walktrough comments written in blue.


package com.robopupu.feature.about.presenter;
...
@Plugin // This annotation declares AboutPresenterImpl to implement a plugin
// The following annotation is from the Dependency API. It declares that this
// class provides a dependency for type AboutPresenter.
@Provides(AboutPresenter.class)
// Parametrised type AboutView declares that this presenter is attached to
// View of type AboutView
public class AboutPresenterImpl extends AbstractFeaturePresenter<AboutView>
        implements AboutPresenter {

    // Plug annotation are used to inject dependencies as plugins from 
    // the plugin bus
    @Plug AppManager mAppManager;
    // A listener for this AboutPresenter. This is actually the implementation of
    // AboutFeature interface.
    @Plug AboutPresenterListener mListener;
    // We use a plug field to attach this AboutPresenterImpl to an implementation
    // of AboutView i.e. to AboutFragment.
    @Plug AboutView mView;

    @Override
    // This overriding method is required by the MVP framework.
    // The base classes utilise it to access a reference to the plugged AboutView
    public AboutView getViewPlug() {
        return mView;
    }

    @Override
    // This method is invoked by the PluginBus when the instance of
    // AboutPresenterImpl is plugged to plugin bus
    public void onPlugged(final PluginBus bus) {
        super.onPlugged(bus);
        // We utilise this plugged event to plug and show the AboutView.
        // When AboutView is instantiated (using the Dependency API)
        // the instance is plugged to plugin bus and a reference is injected
        // to @Plug annotated field AboutView mView
        plug(AboutView.class);
    }

    @Override
    // This method is invoked when the attached view (i.e. AboutFragment)
    // is resumed. 
    public void onViewResume(final View view) {
        super.onViewResume(view);
        mView.setVersionText(mAppManager.getAppVersionName());
    }

    @Override
    // This method is bound as an event handler to a clickable
    // item in the layout of AboutFragment. The binding is 
    // code generated from @OnClick annotation given in 
    // the AboutPresenter interface
    public void onViewLicenseClick() {
        mListener.onShowLicenseInfo();
    }

    @Override
    public void onViewOssLicensesClick() {
        mListener.onShowOssLicensesInfo();
    }

    @Override
    public void onViewSourcesClick() {
        mListener.onOpenSourcesWebPage();
    }
}

Implementing View

As an example how to implement a View. Let’s  take look at how the About screen is implemented. First we define interface AboutView which extends View:


package com.robopupu.feature.about.view;
import com.robopupu.api.mvp.View;
import com.robopupu.api.plugin.PlugInterface;

@PlugInterface
public interface AboutView extends View {
    void setVersionText(String text);
}

Since Views are implemented as components plugged to PluginBus (see Plugin API) we declare the AboutView to define a plugin interface by annotating it with @PlugInterface. Next we implement AboutView interface by extending FeatureFragment which is one of the base class provided by MVP API for implementing Fragment based Views for Features. If you are using Android Compatibility Library is used to implement your UI, then FeatureCompatFragment should be used instead.

See the walktrough comments written in blue.


package com.robopupu.feature.about.view;
...
// We need to annotate AboutFragment with @Plugin for the annotation processor of 
// Plugin API.
@Plugin
public class AboutFragment extends FeatureFragment<AboutPresenter> 
    implements AboutView {

    // AboutPresenter is attached to AboutFragment via a plug field. See Plugin API
    // documentation for more information
    @Plug AboutPresenter mPresenter;

    // Binding is a utility class for binding widgets to a Presenter. This binding 
    // is for a TextView in the layout of AboutFragment
    private Binding mVersionTextBinding;

    // AboutFragment is described to provide dependency AboutView using this annotation
    @Provides(AboutView.class)
    public AboutFragment() {
    }
    
    // Each View implementation has to implement this framework method
    @Override
    public AboutPresenter getPresenter() {
        return mPresenter;
    }

    @Override
    public void setVersionText(final String text) {
        mVersionTextBinding.setText(text);
    }

    @Override
    public View onCreateView(final LayoutInflater inflater, 
        final ViewGroup container, final Bundle inState) {
        return inflater.inflate(R.layout.fragment_about, container, false);
    }

    // A framework method for creating bindings between a View and attached Presenter
    @Override
    protected void onCreateBindings() {
        super.onCreateBindings();
        mVersionTextBinding = bind(R.id.text_view_version);
    }
}

Binding View Components to Presenter

In Robopupu architecture View and Presenter objects communicate with each other using the inter-component communication provided by Plugin API. That was shown in code examples above.  AboutFragment has the plug field mView and AboutPresenterImpl plug field mPresenter. Those fields will be injected with dependencies when AboutFragment and AboutPresenterImpl are plugged to a plug bus.

Since all user interactions are to be dispatched from a AboutFragment, which is a View,  to AboutPresenterImpl, which is a Presenter, we need to bind the widgets, such as Buttons and TextViews, for sending their events to AboutPresenterImpl. This can be done by the usual way setting event listeners.

Robopupu provides a couple of additional ways to establish those bindings:

  1. Bindings API
  2. Code generation based on binding annotations: @OnClick, @OnChecked, and @OnTextChanged annotations.
Bindings API

An example of using Bindings API was shown in implementation of AboutFragment which created an instance of Binding using method ViewFragment# bind(@IdRes int layoutIdRes). Class Binding has, for instance, methods click() and textChanged(String) which can be overridden to delegate View#OnClickListener  and TextWatcher events to the attached Presenter implementation.

Binding Annotations

Binding annotation@OnClick, @OnChecked, and @OnTextChanged can be used to declare methods in a Presenter interface for automatically binding them to layout widgets that have corresponding tag attributes in layout XML.  AboutPresenter interface has the following @OnClick annotations:


@PlugInterface
public interface AboutPresenter extends FeaturePresenter {
    @OnClick void onViewLicenseClick(); // Tag: ViewLicense
    @OnClick void onViewOssLicensesClick(); // Tag: ViewOssLicense
    @OnClick void onViewSourcesClick(); // Tag: ViewSources
}

The layout XML fragment_about.xml contains widgets that have tag attributes for binding them to corresponding event handler methods declared in AboutPresenter interface:


<TextView
    android:id="@+id/text_view_license"
    android:tag="ViewLicense" // -> onViewLicenseClick()
    ... />
<TextView
    android:id="@+id/text_view_oss_licenses"
    android:tag="ViewOssLicenses" // -> onViewOssLicensesClick()
    ... />
<TextView
    android:id="@+id/text_view_sources"
    android:tag="ViewSources" // -> onViewSourcesClick()
    ... />

Automatic binding of widgets defined in a layout XML is based on the following naming conventions for the Presenter methods that are annotated with binding annotations:

  • @OnChecked: on[Tag]Checked
  • @OnClick: on[Tag]Click
  • @OnTextChanged: on[Tag]TextChanged

On Communication between Views and Presenters

In Robopupu MVP implementation, the communication between a Presenter and its attached View happens via PluginBus as described in Robopupu PluginBus API. One of the benefits of using PluginBus-based communication is that we do not need to perform a null check before calling a View’s method from a Presenter. The reason for this is simple: A Presenter does not have a direct object reference to its attached View, but to a PlugInvoker object that delegates the method invocations to View – if it exists. For instance, AboutPresenterImpl calls method AboutView#setVersionText(String) via a code generated AboutView_PlugInvoker object:


package com.robopupu.feature.about.view;

...

public class AboutView_PlugInvoker extends ViewPlugInvoker<AboutView> 
    implements AboutView {
    @Override
    public void setVersionText(final String text) {
        if (mPlugins.size() > 0) {
           mPlugins.get(0).setVersionText(text);
        }
    }
}

As can be seen from the method AboutView_PlugInvoker#setVersionText(String) calls to method AboutFragment#setVersionText(String) takes place only if an instance of AboutFragment is plugged as a plugin into to AboutView_PlugInvoker.

However, if a method which returns a value has to be called for a View, then null check has to be performed. This is because the invoked method has to return a value – and a PlugInvoker would return a default value which would cause logic errors. To check whether a Presenter has an attached View, a convenience method AbstractPresenter#hasAttachedView() can be used.

Dealing with Configuration Changes

Some Android device configurations can change during runtime: Typical examples of  such configuration changes are screen orientation or current language as discussed in Android developer documents. When such a change occurs, Android restarts the running foreground Activity and re-creates its current Fragment(s).  In case of MVP, this means that on configuration change, View objects are recreated while their attached Presenter objects should remain in the memory.

Robopupu.MVP API utilises PluginBus to handle View re-creation in configuration changes. Views and Presenters communicate via PluginBus and they do not have direct object references to each other. A re-created View instance is automatically plugged to PluginBus and the bindings between the View and it’s attached Presenter are re-established automaticallyThe magic happens in method ViewFragment#resolvePresenter():


protected T_Presenter resolvePresenter() {
    T_Presenter presenter = getPresenter();

    if (presenter == null) {
        if (PluginBus.isPlugin(getClass())) {
            PluginBus.plug(this);
            presenter = getPresenter();
        }
    }
    return presenter;
}