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:
- Isn't
View
the actual View? - I mean, you have anActivity
and you usesetContentView(...)
then withfindViewById(...)
you get an instance of the classView
which represents the actual view parsed from xml or programmatically built. - So if
View
is the View, isn't theActivity
the actual presenter? - I mean theActivity
concept itself is there to represent some logic behind the View. - Presenter should keep status: well there's a number of ways to keep status inside and
Activity
. Like, for instanceSavedInstanceState
,SharedPreferences
etc. - 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");
}
}