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

ThirtyInch - Living next to the Activity

Pascal Welsch
November 24, 2016

ThirtyInch - Living next to the Activity

Explains when you should use ThirtyInch and when you should use MVP at all. It also covers the different kinds of MVP, statless and stateful.
The code sample focuses on MVVM, or as I call it VPVM (view presenter viewmodel).

Project on github: https://github.com/grandcentrix/ThirtyInch
Sample on github: https://github.com/passsy/thirtyinch-sample

Pascal Welsch

November 24, 2016
Tweet

More Decks by Pascal Welsch

Other Decks in Technology

Transcript

  1. ThirtyInch
    Living next to the Activity
    Pascal Welsch
    @passsy

    View Slide

  2. Why is MVP a thing on Android?
    Moving all logic to a Presenter makes the code testable
    ○ Only self written code
    ○ Easy lifecycle, view attached/detached
    ○ Unit tests on JVM
    Activities and Fragments are hard to test
    ○ Have final methods and classes
    ○ Constructor dependency injection not possible
    ○ Requires emulator to run androidTest tests (slow)
    ○ The lifecycle is hard to mock and too complex

    View Slide

  3. “What if I don’t write tests?”
    You should write tests!

    View Slide

  4. “What if I really don’t write tests?”
    ● MVP introduces new boilerplate code
    ● There are some parts which will be more complex than before
    ● You most likely will not benefit from MVP
    Try testing with MVP, it will be easier than ever before!
    Don’t use MVP

    View Slide

  5. MVP != MVP
    Stateless
    Presenter will be created every time in
    Activity#onCreate
    Doesn’t solve the problem or network requests
    when the phone is changing orientation
    Hold state (model) in the Activity and save is
    in onSaveInstanceState()
    Stateful
    Presenter survives orientation changes
    Network requests keep running when the phone
    is changing orientation
    Hold state objects (model) in the Presenter

    View Slide

  6. Stateless Presenter
    ● Created in onCreate()
    ● View attached in constructor
    ● New Presenter object when the
    Activity recreates
    ● Move state to new Activity
    instance with
    onSaveInstanceState()

    View Slide

  7. Stateless Presenter
    ● Created in onCreate()
    ● View attached when displayed
    ● View detached when the app in
    in background
    ● New Presenter object when the
    Activity recreates
    ● Move state to new Activity
    instance with
    onSaveInstanceState()

    View Slide

  8. If you build MVP with a
    stateless Presenter
    you don’t need a library

    View Slide

  9. Stateful Presenter
    Created in onCreate() when
    savedInstanceState == null
    Destroy callback when Activity gets
    finished in onDestroy() with
    isFinishing() == true
    When the Activity gets recreated
    the Presenter does survives!

    View Slide

  10. Stateful Presenter
    The state (model) of the Activity can be saved in
    the Presenter (in memory) instead of using
    Parcelables
    Ignores all lifecycle events except for the availability of
    the View
    Network requests can run during a orientation change
    and update the model, not the view
    Implementing a stateful Presenter works best with a
    ViewModel and a passive view (VPVM)
    Living next
    to the
    Activity

    View Slide

  11. ThirtyInch supports both types
    public MyStatelessPresenter() {
    super(new TiConfiguration.Builder()
    .setRetainPresenterEnabled(false)
    .build());
    }
    Stateless Stateful (default)

    View Slide

  12. Sample with
    ViewModel
    github.com/passsy/thirtyinch-sample

    View Slide

  13. View Slide

  14. 5 small steps to
    attach a Presenter to
    your Activity

    View Slide

  15. 1. Define View interface
    interface EditTaskView extends TiView {
    void setTitle(String title);
    void showLoadingIndicator(boolean show);
    }

    View Slide

  16. 2. Create Activity
    public class EditTaskActivity
    extends TiActivity
    implements EditTaskView {

    View Slide

  17. 3. Activity#providePresenter()
    public class EditTaskActivity
    extends TiActivity
    implements EditTaskView {
    @NonNull @Override
    public AddEditTaskPresenter providePresenter() {
    final String taskId =
    getIntent().getStringExtra(EditTaskActivity.ARGUMENT_EDIT_TASK_ID);
    final TasksRepository tasksRepository = Injection.provideTasksRepository(this);
    return new EditTaskPresenter(taskId, tasksRepository);
    }

    View Slide

  18. 4. Activity implements EditTaskView
    @Override public void setTitle(String title) {
    mTitle.setText(title);
    }
    @Override public void showLoadingIndicator(final boolean show) {
    if (show) {
    mLoadingIndicator.show();
    } else {
    mLoadingIndicator.hide();
    }
    }

    View Slide

  19. 5. send UI input events
    public class EditTaskActivity
    extends TiActivity
    implements EditTaskView {
    @Override protected void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.addtask_act);
    mFab = (FloatingActionButton) findViewById(R.id.fab_edit_task_done);
    mFab.setOnClickListener(view -> {
    getPresenter().saveTask();
    });

    View Slide

  20. Write the Presenter

    View Slide

  21. View Slide

  22. Presenter constructor
    class EditTaskPresenter extends TiPresenter {
    EditTaskPresenter(@Nullable String taskId,
    @NonNull TasksDataSource tasksRepository) {
    mTaskId = taskId;
    mTasksRepository = checkNotNull(tasksRepository);
    mViewModel = new EditTaskViewModel();
    }

    View Slide

  23. Presenter#onCreate()
    @Override
    protected void onCreate() {
    super.onCreate();
    mViewModel.setLoadingTask(true);
    mRepositoryRequest = mTasksRepository.getTask(mTaskId, task -> {
    mViewModel.setLoadingTask(false);
    mViewModel.setTask(task);
    });
    }

    View Slide

  24. View Slide

  25. Presenter#onAttachView(view)
    @Override
    protected void onAttachView(@NonNull final EditTaskView view) {
    super.onAttachView(view);
    mViewModel.setOnChangeListener(model -> {
    view.setTitle(model.getTitle());
    view.showLoadingIndicator(model.isLoading());
    });
    }

    View Slide

  26. ViewModel properties
    class EditTaskViewModel {
    private Task mTask = null;
    private boolean mIsLoadingTask = false;
    private NotifyChangeListener mNotifyChangeListener;
    private void notifyChange() {
    if (mNotifyChangeListener != null) {
    mNotifyChangeListener.onChange(this);
    }
    }
    ...

    View Slide

  27. ViewModel notifyChange()
    class EditTaskViewModel {
    ...
    public boolean isLoading() {
    return mIsLoadingTask;
    }
    public void setLoadingTask(final boolean loadingTask) {
    if (mIsLoadingTask != loadingTask) {
    mIsLoadingTask = loadingTask;
    notifyChange();
    }
    }

    View Slide

  28. ViewModel is
    the truth
    Update the ViewModel (not view) when
    - data changes
    - receiving UI events
    Update the View when
    - the View attaches
    - The ViewModel changes
    This prevents NPEs when View == null
    (detached)
    Always update everything (React way)

    View Slide

  29. Mutating UI state
    is error-prone,
    always update
    everything

    View Slide

  30. DistinctUntilChanged
    mViewModel.setOnChangeListener(model -> {
    view.setTitle(model.getTitle());
    view.showLoadingIndicator(model.isLoading());
    });
    interface EditTaskView extends TiView {
    @DistinctUntilChanged
    void showLoadingIndicator(boolean show);
    }
    @DistinctUntilChanged
    Prevents calling methods with
    the same arguments over and
    over again

    View Slide

  31. View Slide

  32. Presenter#onDetachView()
    @Override
    protected void onDetachView() {
    super.onDetachView();
    mViewModel.setOnChangeListener(null);
    }

    View Slide

  33. Presenter#onDetachView()
    @Override
    protected void onDestroy() {
    super.onDestroy();
    if (!mRepositoryRequest.isCanceled()) {
    mRepositoryRequest.cancel();
    }
    }

    View Slide

  34. Modules
    def tiVersion = '0.8.0-rc1'
    compile "net.grandcentrix.thirtyinch:thirtyinch:$tiVersion"
    // rx extension
    compile "net.grandcentrix.thirtyinch:thirtyinch-rx:$tiVersion"
    // test extension
    testCompile "net.grandcentrix.thirtyinch:thirtyinch-test:$tiVersion"

    View Slide

  35. Run unit tests
    on the JVM

    View Slide

  36. Testing on JVM
    @Test
    public void populateTask_callsRepoAndUpdatesView() {
    // Get a reference to the class under test
    final EditTaskPresenter presenter =
    new EditTaskPresenter("1", mTasksRepository);
    final TiPresenterInstructor instructor =
    new TiPresenterInstructor<>(presenter);
    // When task is requested and view is attached
    final AddEditTaskView view = mock(AddEditTaskView.class);
    instructor.attachView(view);

    View Slide

  37. Testing
    // When task is requested and view is attached
    final AddEditTaskView view = mock(AddEditTaskView.class);
    instructor.attachView(view);
    verify(view).setTitle("");
    verify(view).showLoadingIndicator(true);

    View Slide

  38. // get fake task from repository
    final ArgumentCaptor captor =
    ArgumentCaptor.forClass(TasksDataSource.GetTaskCallback.class);
    verify(mTasksRepository).getTask(eq("1"), captor.capture());
    // Simulate callback
    final Task testTask = new Task("TITLE", "DESCRIPTION");
    captor.getValue().onTaskLoaded(testTask);
    // Then the task repository is queried and the view updated
    verify(view).showLoadingIndicator(false);
    verify(view, atLeastOnce()).setTitle(testTask.getTitle());
    }

    View Slide

  39. Why you should use ThirtyInch
    ● most likely better tested than your homemade one;
    Stateless or stateful
    ● Grown and stabilized over the last 1.5 years in
    multiple projects
    ● Gives you a stateful Presenter for Activities and
    Fragments which survives configuration changes
    Try it now!

    View Slide

  40. github.com/grandcentrix/ThirtyInch
    Pascal Welsch
    @passsy
    grandcentrix.net
    @grandcentrix

    View Slide