O slideshow foi denunciado.
Utilizamos seu perfil e dados de atividades no LinkedIn para personalizar e exibir anúncios mais relevantes. Altere suas preferências de anúncios quando desejar.

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

1.729 visualizações

Publicada em

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

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

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

Publicada em: Engenharia
  • Seja o primeiro a comentar

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

×