Код, що розширюється Android-додатків з MVP, SavePearlHarbor

Ще одна копія хабора

Головне меню

Навігація за записами

Код, що розширюється Android-додатків з MVP

Приклад коду, який ілюструє більшість підходів, описаних далі, можна знайти тут: https://github.com/remind101/android-arch-sample

Олдскульний Android

Поділ відповідальності, який мається на увазі Android-фреймворком, виглядає так:Модель може бути будь-яким POJO,Уявлення - це XML-розмітка, а фрагмент (або спочатку активіті) виступає в роліКонтролера/Презентера. Теоретично це працює досить непогано, але як тільки ваш додаток розростається, в Контролері з'являється багато коду, що відноситься до Подання. Все тому, що не так багато можна зробити з XML, тому вся прив'язка даних (дата-біндинг), анімації, обробка введення і т. д. проводиться у фрагменті, поряд з бізнес-логікою.

Все стає ще гірше, коли складні елементи інтерфейсу розміщуються в списках або гридах (прим. пров. - має на увазі GridView/GridLayout, та й взагалі "сіточні елементи"). Тепер на адаптер лягає відповідальність не лише зберігати код подання та контролера для всіх цих елементів, а й керувати ними як колекцією. Оскільки всі ці елементи сильно пов'язані, їх стає дуже важко підтримувати і складніше тестувати.

Вводимо Model-View-Presenter

MVP надає нам можливість виділити весь той нудний низькорівневий Android-код, який потрібен для відображення нашого інтерфейсу та взаємодії з ним, уУявлення, а більш високорівневу бізнес-логіку того, що наш додаток має робити, виселити вПрезентер.

Для досягнення цього на андроїді треба розглядати активіті або фрагмент як шарподання, і надати легковажний презентер для того, щоб контролювати виставу. Найважливіше — визначити відповідальність кожного шару та стандартизувати інтерфейс між ними. Ось загальний опис поділу, який дуже непогано працює у нас:

Подання (активіті чи фрагмент) відповідає за:

  1. Створення екземпляра презентера та механізм його приєднання/від'єднання;
  2. Оповіщення презентера про важливі для нього події життєвого циклу;
  3. Повідомлення презентеру про вхідні події;
  4. Розміщення в'юх та з'єднання їх з даними;
  5. Анімації;
  6. Відстеження подій;
  7. Перехід до інших екранів.

Презентер відповідає за:

  1. Завантаження моделей;
  2. Збереження посилання на модель та стан подання;
  3. Форматування того, що має бути відображене на екрані, та вказівка ​​подання відобразити це;
  4. Взаємодія з репозиторіями (база даних, мережа тощо) (прим. пер. Repository — це патерн, про всяк випадок);
  5. Визначення необхідних дій, коли отримано вхідні події від подання.

Ось приклад того, яким може бути інтерфейс між поданням та презентером:

Є кілька цікавих моментів, які варто розглянути у зв'язку з цим інтерфейсом:

Як з'ясовано на практиці, якщо код вашого презентера містить код Android-фреймворку, а не тільки pure Java, можливо, ви щось робите невірно. І відповідно, якщо ваші уявлення потребують посилання на модель, мабуть, ви також робите щось неправильне.

Як тільки виникне питання тестів, більшість коду, який вам необхідно протестувати, буде в презентері. Що круто, так це те, що цьому коду не потрібнийAndroid для запуску, так як у нього є лише посилання на інтерфейс уявлення, а не його реалізацію в контексті Android. Це означає, що ви можете просто мокнути інтерфейс подання та написати чисті JUnit-тести для бізнес-логіки, що перевіряють правильність виклику методів у мокнутого уявлення. [https://github.com/remind101/android-arch-sample/blob/master/app/src/test/java/com/remind101/archexample/presenters/CounterPresenterTest.java](Ось так) тепер виглядають наші тести.

Як щодо списків?

До цього моменту ми припускали, що наші уявлення — це активіти та фрагменти, але насправді вони можуть бути чим завгодно. У нас досить непогано вдалося працювати зі списками, маючиViewHolder, що реалізує інтерфейс представлення (як RecyclerView.ViewHolder, так і звичайний старий ViewHolder для використання у зв'язці з ListView). В адаптері вам лише потрібна базова логіка для обробки приєднання/від'єднання презентерів (приклад всього цього є в гіт-репозиторії).

Якщо ви подивіться на приклад екрана, що містить список повідомлень, прогрес завантаження та порожню завірюху, поділ відповідальності буде наступним:

  • Презентер списку відповідальний за завантаження повідомлень та логіку відображення в'юх списку/прогресу/порожньої заглушки;
  • Фрагмент відповідає за реалізацію логіки відображення в'юх списку/прогресу/заглушки та переходу на інші екрани;
  • Адаптер зіставляє презентери їх ViewHolder-ам;
  • Презентер повідомлення відповідає за бізнес-логіку окремого повідомлення;
  • ViewHolder відповідальний за відображення повідомлення.

Всі ці компоненти слабо пов'язані і можуть бути окремо протестовані один від одного.

Більше того, якщо у вас є екран списку повідомлень та екранПодробиць, ви можете перевикористовувати той же презентер повідомлення і просто мати дві різні реалізації інтерфейсу уявлення (у ViewHolder-і та фрагменті). Це зберігає ваш код DRY (прим. пров. - "Don't Repeat Yourself", або "Не повторюйтеся", хто не знає).

Подібним чином, інтерфейс уявлення можуть реалізовувати кастомні завірюхи. Це дозволяє вам використовувати MVP в кастомних віджетах, щоб перевикористовувати це в різних частинах програми, або просто розбивати складні інтерфейси на блоки простіше.

MVP та зміна конфігурації

Якщо ви вже якийсь час пишете під Android, ви знаєте, скільки болю завдає підтримка зміни орієнтації та конфігурації:

  • Фрагмент/активіті повинні вміти поновлювати свій стан. Кожного разу при роботі з фрагментом ви повинні запитувати себе, як ця штука повинна діяти при зміні орієнтації, що треба помістити в бандл збереженняіденціїState і т.д.
  • Довгі операції у фонових потоках дуже важко зробити правильно. Одна з найпопулярніших помилок - зберігати посилання на фрагмент/активити у фоновому потоці, тому що йому потрібно оновити UI після завершення роботи. Це призводить до витоку пам'яті (і, ймовірно, падіння програми через збільшення споживання пам'яті), а також до того, що нова активіті ніколи не отримає колбек і, відповідно, не оновить UI.

Правильне використання MVP може вирішити це питання без необхідності взагалі про це замислюватися. Так як у презентерів немає сильного посилання на поточний UI, вони дуже легкі і можуть бути відновлені при зміні орієнтації! Оскільки презентер зберігає посилання модель і стан вистави, може відновити потрібний стан вистави після зміни орієнтації. Ось зразковий опис того, що відбувається приповороті екрана, якщо використовується даний патерн:

  • Спочатку активіті створено (назвемо її "перший екземпляр");
  • Створюється новий презентер;
  • Презентер прив'язується до активіті;
  • Користувач натискає кнопку "Завантажити";
  • У презентері запускається довга операція;
  • Змінюється орієнтація;
  • Презентер відв'язується від першого екземпляра активіті
  • На перший екземпляр активіті більше немає посилань, і тепер вона доступна збирачеві сміття;
  • Презентер збережено, фонова операція продовжується;
  • Створюється другий екземпляр активіті;
  • Другий екземпляр активіті прив'язується до презентера.
  • Завершується завантаження;
  • Презентер оновлює подання (другий екземпляр активіті).
  • Як зберігати фрагменти між змінами орієнтації, можна побачити у репозиторії, у класі PresenterManager.

    Так, це кінець. Сподіваюся, вдалося продемонструвати, як поділ відповідальності на кшталт MVP допоможе вам писати підтримуваний і тестований код.

    • Відокремлюйте вашу бізнес-логіку, виносячи її в голий java-об'єкт презентатора;
    • Витратьте час на чистий інтерфейс між вашими презентерами та уявленнями;
    • Нехай ваші активіті, фрагменти та кастомні завірюхи реалізують інтерфейс уявлення;
    • Для списків реалізовувати інтерфейс подання повинен ViewHolder;
    • Ретельно тестуйте ваші презентери;
    • Зберігайте презенти під час зміни орієнтації.