Slide 1

Slide 1 text

ThirtyInch Living next to the Activity Pascal Welsch @passsy

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

“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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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()

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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!

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

5 small steps to attach a Presenter to your Activity

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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); }

Slide 18

Slide 18 text

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(); } }

Slide 19

Slide 19 text

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(); });

Slide 20

Slide 20 text

Write the Presenter

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

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()); }); }

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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)

Slide 29

Slide 29 text

Mutating UI state is error-prone, always update everything

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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"

Slide 35

Slide 35 text

Run unit tests on the JVM

Slide 36

Slide 36 text

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);

Slide 37

Slide 37 text

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);

Slide 38

Slide 38 text

// 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()); }

Slide 39

Slide 39 text

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!

Slide 40

Slide 40 text

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