Android MVP Architecture

Ken Baldauf

Software Engineer @ Originate

What's MVP?

  • Model-View-Presenter
    • M: Data layer
    • V: UI
    • P: Middle-man

Why MVP?

  • Separation of concerns
    • Decouple code
  • Easier to test
    • Improve unit testability
  • Cleaner code

Android MVP

Presenter/View Interfaces

/**
 * Interface for presenters that can be used with DemoViews
 */
public interface DemoPresenter<V extends DemoView> {

    /**
     * Method used to attach a view to the presenter
     */
    void attachView(V view);
    /**
     * Method used to detach the previously attached view from the presenter
     */
    void detachView();
}
/**
 * Interface for views that can be attached to DemoPresenters
 */
public interface DemoView {

}

BasePresenter

/**
 * Base implementation of the DemoPresenter
 */
public class BasePresenter<V extends DemoView> implements DemoPresenter<V> {

    private V view;

    @Override
    public void attachView(V view) {
        // store the given DemoView in a private member variable
        this.view = view;
    }

    @Override
    public void detachView() {
        // clear the stored DemoView
        view = null;
    }
}

BasePresenter

/**
 * Provides subclasses of BasePresenter access to the attached DemoView
 */
protected V getView() {
    return view;
}

/**
 * Informs subclasses of BasePresenter whether or not a DemoView is attached
 */
protected boolean isViewAttached() {
    return view != null;
}
/**
 * Base implementation of the DemoPresenter
 */
public class BasePresenter<V extends DemoView> implements DemoPresenter<V> {

    private V view;

    @Override
    public void attachView(V view) {
        // store the given DemoView in a private member variable
        this.view = view;
    }

    @Override
    public void detachView() {
        // clear the stored DemoView
        view = null;
    }

    /**
     * Provides subclasses of BasePresenter access to the attached DemoView
     */
    protected V getView() {
        return view;
    }

    /**
     * Informs subclasses of BasePresenter whether or not a DemoView is attached
     */
    protected boolean isViewAttached() {
        return view != null;
    }
}

Next Steps

  • View interface
    • Activity
    • Fragment
    • Custom View
  • Android-free presenter

Example Models

public class Course {
    public String name;
    public List<Student> students;
}

public class Student {
    public String id; // unique student id
    public String name;
}

View Interface

/**
 * Interface exposed to CoursePresenters for updating the course screen's UI
 */
public interface CourseView extends DemoView { }
/**
 * Informs the view that the loading spinner should be hidden
 */
void hideLoadingSpinner();
/**
 * Informs the view that the network error message should be displayed
 */
void showErrorMessage();

View Interface

/**
 * Informs the view of the student that has been found
 */
void studentFound(String studentName);
/**
 * Informs the view that no student has been found with the given id number
 */
void studentNotFound(String number);
/**
 * Informs the view that the title should be updated to the given title
 */
void updateViewTitle(String title);
/**
 * Interface exposed to CoursePresenters for updating the course screen's UI
 */
public interface CourseView extends DemoView {
    /**
     * Informs the view that the loading spinner should be hidden
     */
    void hideLoadingSpinner();
    /**
     * Informs the view that the network error message should be displayed
     */
    void showErrorMessage();
    /**
     * Informs the view of the student that has been found
     */
    void studentFound(String studentName);
    /**
     * Informs the view that no student has been found with the given id number
     */
    void studentNotFound(String number);
    /**
     * Informs the view that the title should be updated to the given title
     */
    void updateViewTitle(String title);
}

View Implementation

public class CourseActivity extends Activity implements CourseView    

    private CoursePresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        presenter = new CoursePresenter;
        presenter.attachView(this);
    }

    @Override
    protected void onDestroy() {
        presenter.detachView();
        super.onDestroy();
    }

    @OnClick(R.id.submit_button)
    void onClick() {
        presenter.findStudent(editTextView.getText().toString());
    }

    /* Implementation of CourseView methods */
}

View Presenter

public class CoursePresenter extends BasePresenter<CourseView> {

    private List<Student> students;

    @Override
    public void attachView() {
        super.attachView()
        DataManager.loadCourse(new CallBack() {
                    @Override
                    public void onSuccess(List<Student> result) {
                        if (isViewAttached()) {
                            students = result;
                            getView().hideLoadingSpinner();
                        }
                    }
                    @Override
                    public void onError(Throwable error) {
                        if (isViewAttached()) {
                            getView().showErrorMessage();
                        }
                    }
                });
    }
}

View Presenter

    @Override
    public void detachView() {
        roster = null;
        super.detachView();
    }

    /**
     * Passes the student's name back to the attached view
     * @param the string representation of the matching student's name
     */
    private void studentFound(String name) {
        if (isViewAttached()) {
            getView().studentFound(name);
        }
    }

    /**
     * Passes the id number that wasn't found back to the attached view
     * @param the string representation of the searched id number
     */
    private void studentNotFound(String number) {
        if (isViewAttached()) {
            getView().studentNotFound(number);
        }
    }

View Presenter

    /**
     * Checks to see if the the course contains a student 
     * with the matching id number
     * and passes the result back to the attached view
     *
     * @param the string representation of the id number to lookup
     */
    public void findStudent(String number) {
        if (! TextUtils.isEmpty(number)) {
            for (Student student : students) {
                if (number.equals(student.id)) {
                    studentFound(student.name);
                    return;
                }
            }
        }
        studentNotFound(number);
    }

Initial Conclusions

  • Android SDK dependencies isolated within the view
    • Easy to unit test
  • Presenter handles all business logic
  • View remains UI centric
    • Updates UI at the request of the presenter
    • Forwards user interaction to the presenter

Mockito Quick Overview

  • Create mock objects
  • Mock and verify object interactions
  • Test functionality in isolation
// mock creation
DataManager mockedDataManager = 
    mock(DataManager.class);
// stub a method
List<Student> students = 
    new ArrayList<>();
when(mockDataManager.loadCourse())
    .thenReturn(Callback.success(students));

// verify a method was called
verify(mockDataManager).loadCourse();

Attach/Detach Test

    @Before
    public void setup() {
        DataManager mockDataManager = mock(DataManager.class);
        presenter = new RosterPresenter(mockDataManager);
        mockView = mock(CourseView.class);
    }    

    @Test
    public void testDetachView() {
        assertFalse(presenter.isViewAttached());
        assertNull(presenter.getView());

        presenter.attachView(mockView);

        assertTrue(presenter.isViewAttached());
        assertNotNull(presenter.getView());

        presenter.detachView();

        assertFalse(presenter.isViewAttached());
        assertNull(presenter.getView());
    }

Network Result Test

    @Before
    public void setup() {
        DataManager mockDataManager = mock(DataManager.class);
        presenter = new RosterPresenter(mockDataManager);
        mockView = mock(CourseView.class);
    }    

    @Test
    public void testLoadCourseSuccess() {
        when(mockDataManager.loadCourse())
            .thenReturn(Callback.success(new ArrayList()));
        presenter.attachView(mockView);
        verify(mockDataManager).loadCourse();
        verify(mockView).hideLoadingSpinner();
    }

    @Test
    public void testLoadCourseFailure() {
        when(mockDataManager.getPlayers(anyString()))
            .thenReturn(Callback.error(new Throwable()));
        presenter.attachView(mockView);
        verify(mockDataManager).loadCourse();
        verify(mockView).showErrorMessage();
    }

Final Conclusions

  • Breaks the app up into easy to understand components
  • MVP + Mockito allow us to test functionality in isolation
  • Manually managing presenter lifecycle can be tricky

Questions?

MVP

By Kenneth Baldauf

MVP

Introduction to Model-View-Presenter architecture in Android

  • 910