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

ThirtyInch - Living next to the Activity

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

F4cff86f26a0d63a0f5d20494e955c04?s=128

Pascal Welsch

November 24, 2016
Tweet

Transcript

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

  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
  3. “What if I don’t write tests?” You should write tests!

  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
  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
  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()
  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()
  8. If you build MVP with a stateless Presenter you don’t

    need a library
  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!
  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
  11. ThirtyInch supports both types public MyStatelessPresenter() { super(new TiConfiguration.Builder() .setRetainPresenterEnabled(false)

    .build()); } Stateless Stateful (default)
  12. Sample with ViewModel github.com/passsy/thirtyinch-sample

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

  15. 1. Define View interface interface EditTaskView extends TiView { void

    setTitle(String title); void showLoadingIndicator(boolean show); }
  16. 2. Create Activity public class EditTaskActivity extends TiActivity<EditTaskPresenter, EditTaskView> implements

    EditTaskView {
  17. 3. Activity#providePresenter() public class EditTaskActivity extends TiActivity<EditTaskPresenter, EditTaskView> 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); }
  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(); } }
  19. 5. send UI input events public class EditTaskActivity extends TiActivity<EditTaskPresenter,

    EditTaskView> 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(); });
  20. Write the Presenter

  21. None
  22. Presenter constructor class EditTaskPresenter extends TiPresenter<EditTaskView> { EditTaskPresenter(@Nullable String taskId,

    @NonNull TasksDataSource tasksRepository) { mTaskId = taskId; mTasksRepository = checkNotNull(tasksRepository); mViewModel = new EditTaskViewModel(); }
  23. Presenter#onCreate() @Override protected void onCreate() { super.onCreate(); mViewModel.setLoadingTask(true); mRepositoryRequest =

    mTasksRepository.getTask(mTaskId, task -> { mViewModel.setLoadingTask(false); mViewModel.setTask(task); }); }
  24. None
  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()); }); }
  26. ViewModel properties class EditTaskViewModel { private Task mTask = null;

    private boolean mIsLoadingTask = false; private NotifyChangeListener<EditTaskViewModel> mNotifyChangeListener; private void notifyChange() { if (mNotifyChangeListener != null) { mNotifyChangeListener.onChange(this); } } ...
  27. ViewModel notifyChange() class EditTaskViewModel { ... public boolean isLoading() {

    return mIsLoadingTask; } public void setLoadingTask(final boolean loadingTask) { if (mIsLoadingTask != loadingTask) { mIsLoadingTask = loadingTask; notifyChange(); } }
  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)
  29. Mutating UI state is error-prone, always update everything

  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
  31. None
  32. Presenter#onDetachView() @Override protected void onDetachView() { super.onDetachView(); mViewModel.setOnChangeListener(null); }

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

    mRepositoryRequest.cancel(); } }
  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"
  35. Run unit tests on the JVM

  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<EditTaskView> instructor = new TiPresenterInstructor<>(presenter); // When task is requested and view is attached final AddEditTaskView view = mock(AddEditTaskView.class); instructor.attachView(view);
  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);
  38. // get fake task from repository final ArgumentCaptor<TasksDataSource.GetTaskCallback> 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()); }
  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!
  40. github.com/grandcentrix/ThirtyInch Pascal Welsch @passsy grandcentrix.net @grandcentrix