1

Lately my team has started considering the implementation of MVP pattern in some of our applications.

We followed the several guides and tutorials out there, basically ending up with regular PresenterInterface and ViewInterface, the latter being implemented by an Activity or a Fragment.

As for the PresenterInterface, we created an implementation of it and injected it in the Activity with dagger.

So the PresenterImplementation would keep track of status and execute all the logic behind the Activity's behaviour.

We could successfully publish some application based on this pattern and everything seems to work quite well.

Lately thought, I started questioning this way of implementing the MVP pattern as it seems to me there's an extra logic layer added in between without being mentioned:

  1. Isn't View the actual View? - I mean, you have an Activity and you use setContentView(...) then with findViewById(...) you get an instance of the class View which represents the actual view parsed from xml or programmatically built.
  2. So if View is the View, isn't the Activity the actual presenter? - I mean the Activity concept itself is there to represent some logic behind the View.
  3. Presenter should keep status: well there's a number of ways to keep status inside and Activity. Like, for instance SavedInstanceState, SharedPreferences etc.
  4. All the View listeners (e.g. OnClickListener) are the methods the View can invoke on its Presenter while a compound CustomView may be created exposing methods to set values of its Sub-Views with an interface.

With the "regular" MVP pattern we used so far, it seems to me that the Activity is there just to access its View's sub-views and "port" the Presenter's commands down to them.


So in my idea, you would have something like this:

Structure

- main
|
|- MainActivity (class)
|- MainPresenter (interface)
|- MainView (interface)
|- MainViewImpl (class)

MainView

public interface MainView {

  void setTitle(String title);
  void setBackgroundColor(int backgroundColor);

}

MainPresenter

public interface MainPresenter {

  void awkwardButtonPressed();

  void bind(MainView mainView);

  void setup();

}

MainViewImpl

public class MainViewImpl extends FrameLayout implements MainView{

  LayoutInflater inflater;
  TextView tvTitle;
  Button btnAwkward;
  MainPresenter mainPresenter;

  public MainViewImpl(Context context) {
    super(context);

    if(!(context instanceof MainPresenter))
      throw new IllegalArgumentException("I need a MainPresenter");
    else mainPresenter = (MainPresenter) context;

    inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    init();
    mainPresenter.bind(this);
    mainPresenter.setup();
  }

  public MainViewImpl(Context context, AttributeSet attrs) {
    super(context, attrs);

    if(!(context instanceof MainPresenter))
      throw new IllegalArgumentException("I need a MainPresenter");
    else mainPresenter = (MainPresenter) context;

    inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    init();
    mainPresenter.bind(this);
    mainPresenter.setup();
  }

  public MainViewImpl(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    if(!(context instanceof MainPresenter))
      throw new IllegalArgumentException("I need a MainPresenter");
    else mainPresenter = (MainPresenter) context;

    inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    init();
    mainPresenter.bind(this);
    mainPresenter.setup();
  }

  @Override
  public void setTitle(String title) {
    tvTitle.setText(title);
  }

  @Override
  public void setBackgroundColor(String backgroundColor) {
    super.setBackgroundColor(Color.parseColor(backgroundColor));
  }

  private void init(){
    inflater.inflate(R.layout.main_view_layout, this, true);
    tvTitle = (TextView) findViewById(R.id.tvTitle);
    btnAwkward = (Button) findViewById(R.id.btnAwkward);
    btnAwkward.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        mainPresenter.awkwardButtonPressed();
      }
    });
  }

}

MainActivity

public class MainActivity extends AppCompatActivity implements MainPresenter {

  private MainView mainView;
  private final Map<String, String> status = new HashMap<>();

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (savedInstanceState != null) {
      if (savedInstanceState.containsKey("title")) {
        status.put("title", savedInstanceState.getString("title"));
      }
      if (savedInstanceState.containsKey("backgroundColor")) {
        status.put("backgroundColor", savedInstanceState.getString("backgroundColor"));
      }
    } else {
      status.put("title", "My Cool MVP");
      status.put("backgroundColor", "#00AAFF");
    }

    setContentView(R.layout.activity_main);
  }

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    outState.putString("title", status.get("title"));
    outState.putString("backgroundColor", status.get("backgroundColor"));
    super.onSaveInstanceState(outState);
  }

  @Override
  public void bind(MainView mainView) {
    this.mainView = mainView;
  }

  @Override
  public void setup() {
    this.mainView.setTitle(status.get("title"));
    this.mainView.setBackgroundColor(status.get("backgroundColor"));
  }

  @Override
  public void awkwardButtonPressed() {
    status.put("backgroundColor", "#00FFAA");
    status.put("title", "That Was Awkward...");
    mainView.setBackgroundColor(status.get("backgroundColor"));
    mainView.setTitle(status.get("title"));
  }
}

Finally the xml resources for the layouts here could be:

main_activity_layout.xml

<com.sample.application.MainViewImpl
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/cvMainView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

main_view_layout.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/btnAwkward"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/awkward_button"/>

</LinearLayout>

This way the MainViewImpl is the View and the Activity is the presenter. Notice in both implementations the counterpart is always accessed through its interface, the Activity keeps the status in a map which puts in the SavedInstanceState when needed and retrieves back again when re-created.


Questions

Does this make any sense? Is this really MVP? If not, what is the actual role of the Activity class in Android MVP? What could be the flaws of the presented implementation?


In case someone is wandering about how to unit test the Presenter's logic (which now is an Activity), here is how it can be done:

MainActivityTest

public class MainActivityTest {

  MainPresenter mainPresenter;
  MainView mainView;

  @Before
  public void setUp() {
    mainPresenter = new MainActivity();
    mainView = Mockito.mock(MainView.class);
    mainPresenter.bind(mainView);
  }

  @Test
  public void testSetup() throws Exception {
    mainPresenter.setup();
    Mockito.verify(mainView).setTitle(Mockito.anyString());
    Mockito.verify(mainView).setBackgroundColor(Mockito.anyString());
  }

  @Test
  public void testAwkwardPressing() throws Exception {
    mainPresenter.awkwardButtonPressed();
    Mockito.verify(mainView).setTitle("That Was Awkward...");
    Mockito.verify(mainView).setBackgroundColor("#00FFAA");
  }
}
  • 3
    Did you just post the same code twice, with minor variations? Please don't do that. Choose one revision, and go with that. You don't need to say "EDIT:" we already know you edited. – Robert Harvey May 4 '16 at 14:24
  • Hi, sorry, I think the major repost was in MainActivity because it changed quite a bit, while for the MainViewImpl I just posted the differential. So you think I should just replace the old code with the new one and in the EDIT note explain the changes and show the test class? Thanks for the feedback. – Onheiron May 4 '16 at 14:28
  • 4
    Yes. Don't say "EDIT." We already know that. Also, for what it's worth, this post is really broad. Do you think you could make your question more specific? Focus it on the most pressing issue you are facing. "Understanding Android MVP" isn't a question; it's the title of a programming course. – Robert Harvey May 4 '16 at 14:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.