Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Model View Presenter: The Life-Changing Magic of Tidying Up Your Presentation Layer

Model View Presenter: The Life-Changing Magic of Tidying Up Your Presentation Layer

Yun Cheng

July 20, 2018
Tweet

More Decks by Yun Cheng

Other Decks in Programming

Transcript

  1. Model View Presenter The Life-Changing Magic of Tidying Up Your

    Presentation Layer Yun Cheng ASICS Digital
  2. • Problems to Solve ◦ Separation of Concerns ◦ Unit

    Testing • What is Model View Presenter? • Steps to refactor to MVP • Pros/Cons Overview
  3. It is a common mistake to write all your code

    in an Activity or a Fragment. Any code that does not handle a UI or operating system interaction should not be in these classes. Keeping them as lean as possible will allow you to avoid many lifecycle related problems. --Android Developers Guide https://developer.android.com/jetpack/docs/guide Separation of Concerns
  4. Why should we unit test? • Fast to run •

    No Robolectric or Espresso • Foundation for working UI logic Why is unit testing hard in Android? • Android life cycle • Lots of dependencies Unit Testing
  5. public class DateUtil { DateUtil() { // Constructor } public

    int getPreviousCalendarDay(int day) { return ((day + 5) % 7) + 1; } } Normal Java Code
  6. Android Unit Testing public class TripListActivity extends Activity { @Override

    public void onCreate(Bundle savedInstanceState) { super.onCreate(); methodIWantToTest(); } @Override protected void onResume() {} @Override protected void onDestroy() {} protected void methodIWantToTest() {} }
  7. How did we solve these problems? • No separation of

    concern • Not testable Trip List Activity
  8. Trip List Activity (BEFORE) • onCreate • onResume • onDestroy

    • onPause • onRefresh • onLowMemory • onSaveInstanceState • onActivityResult • setTripProgressObservable • triggerActivityPushSync • reloadTrips • syncDone • syncDoneWithError • LoadTripsTask • createAdapter • onTripsLoaded • setUpSwipeRefreshLayout • setLayout • showHideViewsOnSync • trackAnActivity • unregisterForBroadcasts • registerForBroadcasts • connectionError • getViewEventName • getTripCount • findIndexByTripUuid • startSyncTimer • completeSyncTimer
  9. Trip List Activity (BEFORE) • onCreate • onResume • onDestroy

    • onPause • onRefresh • onLowMemory • onSaveInstanceState • onActivityResult • setTripProgressObservable • triggerActivityPushSync • reloadTrips • syncDone • syncDoneWithError • LoadTripsTask • createAdapter • onTripsLoaded • setUpSwipeRefreshLayout • setLayout • showHideViewsOnSync • trackAnActivity • unregisterForBroadcasts • registerForBroadcasts • connectionError • getViewEventName • getTripCount • findIndexByTripUuid • startSyncTimer • completeSyncTimer Lifecycle methods Networking methods View methods Broadcast methods Utility methods DB methods
  10. Model View Presenter Activity Lifecycle methods Model The data View

    Views Presenter Presentation logic ---------- Lifecycle
  11. POJOS extends Activity/Fragment Model View Presenter Lifecycle Lifecycle methods Navigation

    Model The data View Views View logic Presenter Presentation logic
  12. Androidy Model View Presenter Lifecycle Lifecycle methods Navigation Model The

    data View Views View logic Presenter Presentation logic Not Androidy
  13. Model View Presenter Lifecycle Lifecycle methods Model The data View

    Views Presenter Presentation logic IView IPresenter ILifecycle
  14. Contract Model View Presenter Lifecycle Lifecycle methods Model The data

    View Views Presenter Presentation logic IView IPresenter ILifecycle
  15. Step 1: Split up the activity into model, view, presenter,

    lifecycle Step 2: Connect all the components Step 3: Create the presenter using a factory Step 4: Write unit tests How to Refactor to MVP
  16. Presenter POJO presentation logic Step 1: Split up the activity

    View View View logic recyclerView tripHistoryAdapter Lifecycle Lifecycle methods Navigation Model tripList
  17. Runkeeper Architecture Managers -TripManager -FriendsManager -CoachingManager -etc. WebClient Database Lifecycle

    Lifecycle methods Model The data View Views Presenter Presentati on logic APP ARCHITECTURE SCREEN ARCHITECTURE Business logic
  18. Split up the Activity • TripListActivity • TripListModel • TripListView

    • TripListPresenter • TripListContract ◦ IActivity ◦ IModel ◦ IView ◦ IPresenter cmd+alt+b Goto Implementation Works on methods to find where they are implemented/overriden.
  19. Activity @Override public void onActivityResult(int requestCode, int resultCode, Intent data)

    { final boolean deleted = data.getBooleanExtra(TRIP_DELETED, false); ... presenter.handleActivityResult(requestCode, resultCode, index, deleted); } Activity-Presenter Relationship Presenter public void handleActivityResult(int requestCode, int resultCode, boolean index, boolean deleted) { ... triggerActivityPushSync(); }
  20. Presenter public void triggerActivityPushSync() { view.updateUIForSyncInProgress(); ... } Presenter-View Relationship

    View public void updateUIForSyncInProgress() { loadingAnimation.setVisibility(VISIBLE); swipeRefreshLayout.setVisibility(GONE); ... }
  21. Trip List Activity (AFTER) • onCreate • createPresenter • onResume

    • onDestroy • onPause • onLowMemory • onSaveInstanceState • onActivityResult • getViewEventName • showConnectionErrorToast • trackAnActivity • createMapFactory • Lines of code ◦ 747 --> 132 • Number of methods ◦ 27 --> 12
  22. Because our presenter is just a POJO, we can easily

    unit test our logic Key Takeaway Presenter POJO presentation logic ✓T
  23. Step 2: Connect the components Lifecycle Lifecycle methods Model The

    data View Views Presenter Presentation logic
  24. Lifecycle to Presenter Activity protected TripListContract.IPresenter presenter; @Override protected void

    onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); presenter = createPresenter(); } @Override public TripListContract.IActivity createPresenter() { //Create presenter here }
  25. View to Presenter View protected TripListContract.IPresenter presenter; @Override public void

    bindPresenter(TripListContract.IPresenter presenter) { this.presenter = presenter; }
  26. Presenter Presenter protected TripListPresenter(TripListContract.IView view, TripListContract.ILifecycle activity, TripListContract.IModel model, RKWebClient

    webClient, TripManager tripManager) { this.view = view; this.activity = activity; this.model = model; this.webClient = webClient; this.tripManager = tripManager; view.bindPresenter(this); } Inject all these dependencies
  27. Step 3: Create the presenter using a factory (optional) Activity

    @Override public TripListContract.IActivity createPresenter() { //Create presenter here }
  28. Dependency: any kind of object that our presenter class needs

    to do its job Dependency Injection Presenter public Observable<ArrayList<Trip>> getReloadTripsObservable() { TripManager tripManager = TripManager.openManager(context); return Observable.fromCallable(() -> { ... return tripManager.getLatestTrips(); } }
  29. Remove tight coupling by injecting dependencies into the presenter’s constructor

    Dependency Injection Presenter public Observable<ArrayList<Trip>> getReloadTripsObservable() { TripManager tripManager = TripManager.openManager(context); return Observable.fromCallable(() -> { ... return tripManager.getLatestTrips(); } } Tightly coupled dependency
  30. public class TripListPresenterFactory { @Override public TripListContract.IPresenter create(Context context, TripListContract.ILifecycle

    activity) { } } Presenter Factory Create all dependencies Return a new presenter, injecting in the dependencies
  31. public class TripListPresenterFactory { @Override public TripListContract.IPresenter create(Context context, Intent

    intent, Bundle savedInstanceState, TripListContract.ILifecycle activity) { LayoutInflater layoutInflater = LayoutInflater.from(context); RKWebClient webClient = new RKWebClient(context); TripManager tripManager = TripManager.openTripManager(context); TripListContract.IModel model = new TripListModel(); TripHistoryAdapter adapter = new TripHistoryAdapter(model); TripListContract.IView view = new TripListView(layoutInflater, context); view.attachNewAdapter(adapter); return new TripListPresenter(view, activity, model, webClient, tripManager); } } Presenter Factory Create all dependencies Return a new presenter, injecting in the dependencies }
  32. @Mock private TripListView mockView; @Mock private TripListActivity mockActivity; @Mock private

    TripListModel mockModel; @Mock private RKWebClient mockWebClient; @Mock private TripManager mockTripManager; @Mock private TripHistoryAdapter mockAdapter; Mock Dependencies
  33. @Before public void setUp() { ... presenter = new TripListPresenter(mockView,

    mockActivity, mockModel, mockWebClient, mockTripManager); } Inject Dependencies
  34. Example: Delete a Trip Activity @Override public void onActivityResult(int requestCode,

    int resultCode, Intent data) { final boolean deleted = data.getBooleanExtra(TRIP_DELETED, false); ... presenter.handleActivityResult(requestCode, resultCode, index, deleted); } Presenter public void handleActivityResult(int requestCode, int resultCode, boolean index, boolean deleted) { ... if (deleted) { handleTripDeletedFromTripList(index); } }
  35. @Test public void onActivityResultDeleted() throws Exception { int tripCountBefore =

    model.getTripCount(); //Set up the test so that tripDeleted is true boolean tripDeleted = true; //Call the method of interest presenter.handleActivityResult(requestCode, resultCode, index, tripDeleted); //Assert that list is 1 trip fewer int tripCountAfter = model.getTripCount(); assertEquals(tripCountAfter, tripCountBefore - 1); } Write Tests
  36. @Test public void onActivityResultDeleted() throws Exception { int tripCountBefore =

    model.getTripCount(); //Set up the test so that tripDeleted is true boolean tripDeleted = true; //Call the method of interest presenter.handleActivityResult(requestCode, resultCode, index, tripDeleted); //Assert that list is 1 trip fewer int tripCountAfter = model.getTripCount(); assertEquals(tripCountAfter, tripCountBefore - 1); } Write Tests
  37. @Test public void onActivityResultDeleted() throws Exception { int tripCountBefore =

    model.getTripCount(); //Set up the test so that tripDeleted is true boolean tripDeleted = true; //Call the method of interest presenter.handleActivityResult(requestCode, resultCode, index, tripDeleted); //Assert that list is 1 trip fewer int tripCountAfter = model.getTripCount(); assertEquals(tripCountAfter, tripCountBefore - 1); } Write Tests
  38. @Test public void onActivityResultDeleted() throws Exception { int tripCountBefore =

    model.getTripCount(); //Set up the test so that tripDeleted is true boolean tripDeleted = true; //Call the method of interest presenter.onActivityResult(requestCode, resultCode, mockIntent); //Assert that list is 1 trip fewer int tripCountAfter = model.getTripCount(); assertEquals(tripCountAfter, tripCountBefore - 1); } Write Tests
  39. Example: Swipe to Refresh View @Override public void onRefresh() {

    if (!presenter.getSyncInProgress()) { visibleItemPosition = 0; presenter.triggerActivityPushSync(); } } Presenter public void triggerActivityPushSync() { view.updateUIForSyncInProgress(); ... // Start syncing to server webClient.beginSync(); }
  40. @Test public void showProgressDuringSync() throws Exception { //Set up the

    test doNothing().when(mockWebClient.beginSync()) //Call the method of interest presenter.triggerActivityPushSync(); //Assert that the progress bar is showing assertTrue(mockView.inProgress()); } Write Tests
  41. @Test public void showProgressDuringSync() throws Exception { //Set up the

    test doNothing().when(mockWebClient.beginSync()) //Call the method of interest presenter.triggerActivityPushSync(); //Assert that the progress bar is showing assertTrue(mockView.inProgress()); } Write Tests
  42. @Test public void showProgressDuringSync() throws Exception { //Set up the

    test doNothing().when(mockWebClient.beginSync()) //Call the method of interest presenter.triggerActivityPushSync(); //Assert that the progress bar is showing assertTrue(mockView.inProgress()); } Write Tests
  43. Step 1: Split up the activity into model, view, presenter,

    lifecycle Step 2: Connect all the components Step 3: Create the presenter using a factory Step 4: Write unit tests How to Refactor to MVP
  44. Cons • Has many classes and interfaces • Must consider

    when the activity gets destroyed Pros and Cons Pros • Can now achieve separation of concerns • Can now test previously huge and untestable activities/fragments