Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Moxy. Как правильно пользоваться? / Юрий Шмаков (Arello Mobile)

1,693 views

Published on

РИТ++ 2017, AppsConf
Зал Касабланка, 6 июня, 16:00

Тезисы:
http://appsconf.ru/2017/abstracts/2704.html

В последнее время паттерн MVP будоражит Android-комьюнити. Уже есть несколько довольно приличных библиотек, которые помогают использовать этот подход. Но с ними вам придётся писать много boilerplate-кода. Поэтому я хочу познакомить вас с Moxy. Покажу, как использовать её компоненты для решения задач, которые будут вставать перед вами, когда вы решите использовать паттерн MVP. И расскажу, как устроены эти компоненты, и почему именно так, чтобы вы не боялись использовать Moxy из-за потенциальных подводных камней.

Published in: Engineering
  • Be the first to comment

Moxy. Как правильно пользоваться? / Юрий Шмаков (Arello Mobile)

  1. 1. 1
  2. 2. Определить Presenter Определить интерфейсView РеализоватьView Выделить бизнес-логику в Model 2
  3. 3. Автоматическое сохранение отображения при пересозданииView Никакого boilerplate-кода Отсутствие ненужных флажков, id, switch-case, e.t.c. 3
  4. 4. ViewImpl Presenter ViewState ViewCommands 4
  5. 5. ViewImpl Presenter ViewState ViewCommands Command 5
  6. 6. ViewImpl Presenter ViewState ViewCommands CommandCommand 6
  7. 7. ViewImpl Presenter ViewState ViewCommands Command Command 7
  8. 8. ViewImpl Presenter ViewState ViewCommands Command Command 8
  9. 9. ViewImpl Presenter ViewState ViewCommands Command 9
  10. 10. ViewImpl Presenter ViewState ViewCommands CommandCommand 10
  11. 11. Приложение запускается Проходит 1 секунда Отображается сообщение 11
  12. 12. public interface HelloWorldView extends MvpView { void showMessage(int message); } 12
  13. 13. @InjectViewState public class HelloWorldPresenter extends MvpPresenter<HelloWorldView> { public HelloWorldPresenter() { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... voids) { sleepSecond(); return null; } @Override protected void onPostExecute(Void aVoid) { getViewState().showMessage(R.string.hello_world); } }.execute(); ... 13
  14. 14. public class MainActivity extends MvpAppCompatActivity implements HelloWorldView { @InjectPresenter HelloWorldPresenter mHelloWorldPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override public void showMessage(int message) { TextView messageTextView = new TextView(this); messageTextView.setText(message); ((ViewGroup) findViewById(R.id.activity_main)).addView(messageTextView); } } 14
  15. 15. 15
  16. 16. Приложение запускается Проходит 5 секунд На экране отображается количество оставшихся секунд Отображается сообщение 16
  17. 17. @InjectViewState public class HelloWorldPresenter extends MvpPresenter<HelloWorldView> { ... @Override protected void onPreExecute() { getViewState().showTimer(); } @Override protected Void doInBackground(Void... voids) { for (int i = 5; i > 0; i--) { publishProgress(i); sleepSecond(); } return null; } @Override protected void onProgressUpdate(Integer... values) { getViewState().setTimer(values[0]); } @Override protected void onPostExecute(Void aVoid) { getViewState().hideTimer(); getViewState().showMessage(R.string.hello_world); } ... 17
  18. 18. public interface HelloWorldView extends MvpView { void showTimer(); void hideTimer(); void setTimer(int seconds); void showMessage(int message); } 18
  19. 19. public class MainActivity extends MvpAppCompatActivity implements HelloWorldView { private TextView mTimerTextView; @Override public void showTimer() { mTimerTextView.setVisibility(View.VISIBLE); } @Override public void hideTimer() { mTimerTextView.setVisibility(View.GONE); } @Override public void setTimer(int seconds) { mTimerTextView.setText(getString(R.string.timer, seconds)); } 19
  20. 20. 20
  21. 21. Приложение запускается Проходит 2 секунды На экране отображается количество оставшихся секунд Отображается сообщение в виде AlertDialog 21
  22. 22. public class MainActivity extends MvpAppCompatActivity implements HelloWorldView { private AlertDialog mMessageDialog; @Override public void showMessage(int message) { mMessageDialog = new AlertDialog.Builder(this) .setTitle(R.string.app_name).setMessage(message) .setOnDismissListener(dialog -> mHelloWorldPresenter.onDismissMessage()) .show(); } @Override public void hideMessage() { if (mMessageDialog != null) { mMessageDialog.dismiss(); } } 22
  23. 23. public class MainActivity extends MvpAppCompatActivity implements HelloWorldView { private AlertDialog mMessageDialog; @Override public void showMessage(int message) { mMessageDialog = new AlertDialog.Builder(this) .setTitle(R.string.app_name).setMessage(message) .setOnDismissListener(dialog -> mHelloWorldPresenter.onDismissMessage()) .show(); } @Override public void hideMessage() { if (mMessageDialog != null) { mMessageDialog.dismiss(); } } 23
  24. 24. 06-02 04:59:00.178 14192-14192/com.arellomobile.mvp.sample.appsconf E/WindowManager: android.view.WindowLeaked: Activity com.arellomobile.mvp.sample. MainActivity has leaked window DecorView@c59996b[] that was originally added here at android.view.ViewRootImpl.<init>(ViewRootImpl.java:418) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:331) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93) at android.app.Dialog.show(Dialog.java:322) at android.support.v7.app.AlertDialog$Builder.show(AlertDialog.java:956) ... 24
  25. 25. @Override protected void onDestroy() { super.onDestroy(); if (mMessageDialog != null) { mMessageDialog.dismiss(); } } 25
  26. 26. @Override protected void onDestroy() { super.onDestroy(); if (mMessageDialog != null) { mMessageDialog.dismiss(); } } 26
  27. 27. @Override protected void onDestroy() { super.onDestroy(); if (mMessageDialog != null) { mMessageDialog.setOnDismissListener(null); mMessageDialog.dismiss(); } } 27
  28. 28. @InjectViewState public class HelloWorldPresenter extends MvpPresenter<HelloWorldView> { ... public void onDismissMessage() { getViewState().hideMessage(); } } 28
  29. 29. @StateStrategyType(AddToEndSingleStrategy.class) public interface HelloWorldView extends MvpView { void showTimer(); void hideTimer(); void setTimer(int seconds); void showMessage(int message); void hideMessage(); } 29
  30. 30. 30
  31. 31. Приложение запускается Проходит 5 секунд На экране отображается количество оставшихся секунд Отображается сообщение в виде стороннейView 31
  32. 32. public class MainActivity extends MvpAppCompatActivity implements HelloWorldView { @Override public void showMessage(int message) { ViewGroup rootView = (ViewGroup) findViewById(R.id.activity_main); mMessageView = inflate(R.layout.item_message, rootView, true); ((TextView) mMessageView.findViewById(R.id.message_text_view)).setText(message); mMessageView.findViewById(R.id.close_button) .setOnClickListener(v -> mHelloWorldPresenter.onDismissMessage()); } @Override public void hideMessage() { ViewGroup rootView = (ViewGroup) findViewById(R.id.activity_main); rootView.removeView(mMessageView); } 32
  33. 33. 33
  34. 34. • В Activity отображается 2 Fragment • Каждый Fragment содержит счётчик нажатий на кнопку • Изменения показаний счётчика одного фрагмента никак не влияют на показания счётчика другого фрагмента 34
  35. 35. public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.frame_1, getFragment(0xffFF80AB)) .add(R.id.frame_2, getFragment(0xffCCFF90)) .commit(); } } private Fragment getFragment(int color) { CounterFragment fragment = new CounterFragment(); Bundle args = new Bundle(); args.putInt("argColor", color); fragment.setArguments(args); return fragment; } } 35
  36. 36. @InjectViewState public class CounterPresenter extends MvpPresenter<CounterView> { private int mCount; public CounterPresenter() { getViewState().showCount(mCount); } public void onPlusClick() { mCount++; getViewState().showCount(mCount); } } 36
  37. 37. public interface CounterView extends MvpView { @StateStrategyType(AddToEndSingleStrategy.class) void showCount(int count); } 37
  38. 38. public class CounterFragment extends MvpAppCompatFragment implements CounterView { @InjectPresenter CounterPresenter mCounterPresenter; @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { view.findViewById(R.id.plus_button).setOnClickListener(v -> mCounterPresenter.onPlusClick()); } @Override public void showCount(int count) { mCounterTextView.setText(String.valueOf(count)); } } 38
  39. 39. 39
  40. 40. В Activity отображается 2 Fragment Каждый Fragment содержит счётчик нажатий на кнопку Показания счётчиков синхронизированы 40
  41. 41. @InjectViewState public class CounterPresenter extends MvpPresenter<CounterView> { @Inject CounterInteractor counterInteractor; private final Disposable disposable; public CounterPresenter() { App.getAppComponent().inject(this); disposable = counterInteractor.getCounter() .subscribe(value -> getViewState().showCount(value)); } public void onPlusClick() { counterInteractor.increase(); } @Override public void onDestroy() { disposable.dispose(); } } 41
  42. 42. public class CounterInteractor { private int counterValue = 0; private Subject<Integer> counter = BehaviorSubject.createDefault(counterValue); public Subject<Integer> getCounter() { return counter; } public void increase() { counterValue++; counter.onNext(counterValue); } } 42
  43. 43. 43
  44. 44. В Activity отображается 2 счётчика Каждый счётчик является CustomView Изменения показаний одного счётчика никак не влияют на показания другого счётчика 44
  45. 45. public class MvpActivity extends Activity { private MvpDelegate<? extends MvpActivity> mMvpDelegate; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getMvpDelegate().onCreate(savedInstanceState); } @Override protected void onStart() { super.onStart(); getMvpDelegate().onAttach(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); getMvpDelegate().onSaveInstanceState(outState); } 45
  46. 46. 46 @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); getMvpDelegate().onSaveInstanceState(outState); getMvpDelegate().onDetach(); } @Override protected void onStop() { super.onStop(); getMvpDelegate().onDetach(); } @Override protected void onDestroy() { super.onDestroy(); getMvpDelegate().onDestroyView(); if (isFinishing()) { getMvpDelegate().onDestroy(); } } }
  47. 47. public class MainActivity extends MvpAppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main3); ((CounterWidget) findViewById(R.id.counter_1)).init(getMvpDelegate()); ((CounterWidget) findViewById(R.id.counter_2)).init(getMvpDelegate()); } } 47
  48. 48. public class CounterWidget extends FrameLayout implements CounterView { private MvpDelegate<CounterWidget> mMvpDelegate; @InjectPresenter CounterPresenter mCounterPresenter; public void init(MvpDelegate parentDelegate) { initMvpDelegate(parentDelegate); mMvpDelegate.onCreate(); mMvpDelegate.onAttach(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mMvpDelegate.onSaveInstanceState(); mMvpDelegate.onDetach(); } public void initMvpDelegate() { mMvpDelegate = new MvpDelegate<>(this); mMvpDelegate.setParentDelegate(mParentDelegate, String.valueOf(getId())); } private TextView mCounterTextView; public CounterWidget(Context context, AttributeSet attrs) { super(context, attrs); LayoutInflater.from(context).inflate(R.layout.item_counter, this, true); mCounterTextView = (TextView) findViewById(R.id.count_text); View button = findViewById(R.id.plus_button); button.setOnClickListener(view -> mCounterPresenter.onPlusClick()); } @Override public void showCount(int count) { mCounterTextView.setText(String.valueOf(count)); } 48
  49. 49. 49
  50. 50. Сделать экран отображения списка новостей Должен быть SwipeToRefresh Если произошла ошибка, то показать её как Toast 50
  51. 51. 51 @StateStrategyType(AddToEndSingleStrategy.class) public interface NewsFeedView extends MvpView { String TAG_LOADING_COMMAND = "tagLoadingCommand"; void showNewsFeed(NewsFeed newsFeed); @StateStrategyType(OneExecutionStateStrategy.class) void showError(String message); @StateStrategyType(value = AddToEndSingleByTagStateStrategy.class, tag = TAG_LOADING_COMMAND) void startLoading(); @StateStrategyType(value = AddToEndSingleByTagStateStrategy.class, tag = TAG_LOADING_COMMAND) void finishLoading(); }
  52. 52. 52 public class AddToEndSingleByTagStateStrategy implements StateStrategy { public <View extends MvpView> void beforeApply(List currentState, ViewCommand command) { Iterator<ViewCommand<View>> iterator = currentState.iterator(); while (iterator.hasNext()) { ViewCommand<View> entry = iterator.next(); if (entry.getTag().equals(incomingCommand.getTag())) { iterator.remove(); break; } } currentState.add(incomingCommand); } public <View extends MvpView> void afterApply(List currentState, ViewCommand command) { // pass } }
  53. 53. Сделать экран деталей конкретной новости 53
  54. 54. @InjectViewState public class DetailsPresenter extends MvpPresenter<DetailsView> { public DetailsPresenter(long newsId) { getViewState().showDetails("Details of "" + newsId + """); } } public interface DetailsView extends MvpView { void showDetails(String details); } 54
  55. 55. public class DetailsActivity extends MvpAppCompatActivity implements DetailsView { @InjectPresenter DetailsPresenter mDetailsPresenter; @ProvidePresenter DetailsPresenter provideDetailsPresenter() { return new DetailsPresenter(getIntent().getLongExtra("extraDetailsId", 0)); } @Override public void showDetails(String details) { Log.i(DetailsActivity.class.getSimpleName(), details); } } 55
  56. 56. 0. Annotation processor: Generate PresenterFields 1. MvpDelegate: onCreate(savedInstanceState) 2. MvpDelegate: Init delegate tag 3. MvpProcessor: Collect all PresenterField for MvpDelegate 4. MvpProcessor: Init each PresenterField 1. MvpProcessor: Generate presenter tag 2. PresenterStore: Get MvpPresenter by type and tag 3. MvpProcessor: MvpPresenter exists? 1. True: 1. MvpProcessor: Init presenter field of Delegated 2. False: 1. PresenterField: Provide presenter 2. PresenterStore: Save presenter 3. MvpProcessor: Init presenter field of Delegated 56
  57. 57. 0. Annotation processor: Generate ViewState 1. MvpPresenter: Construct 2. Binder: Bind presenter 3. Binder: Find ViewState for MvpPresenter 4. Binder: Create ViewState 5. Binder: Set ViewState to MvpPresenter 57
  58. 58. 1. MvpPresenter: Send command 2. ViewState: Instantiation of ViewCommand 3. ViewState: Get StateStrategy of ViewCommand 4. StateStrategy: Called beforeApply(currentState, incomingCommand) 5. ViewState: Have a Views? 1. False: – 2. True: 1. ViewCommand: Apply to each Views 2. StateStrategy: Called afterApply(currentState, incomingState) 6. ViewState: Attached View 7. ViewState: Apply each ViewCommands 1. ViewCommand: Apply to attached View 2. StateStrategy: Called afterApply(currentState, incomingState) 58
  59. 59. 59
  60. 60. 1. Нет проблем с жизненным циклом 2. Boilerplate-code генерируется в compile time 3. Можно использовать несколько Presenter в одном месте 4. Можно любой компонент превратить в MvpView Присоединяйтесь к проекту на github.com! PS: https://github.com/senneco/MoxyCases 60
  61. 61. 61

×