В этой серии статей используется базовое приложение MVP с использованием Retrofit и RxJava для отображения списка репозиториев Github; и преобразует его в современное приложение для Android - попутно познакомит с различными методами, используемыми при разработке приложений для Android, объяснит, почему эти методы используются, и проведет несколько экспериментов для загрузки.

Если вы просто хотите увидеть, как код идет сюда, репо будет обновляться по мере продвижения серии.

Часть 1 - Простая инъекция зависимостей с помощью Dagger

Часть 2 - Преобразование докладчиков в модели просмотра

Часть 3. Архитектура одиночного действия + прикольный кинжал

Далее, это проект после использования MVVM. Следующее изменение будет заключаться в использовании архитектуры единого действия и навигационной библиотеки androidx, плюс есть кое-что необычное, что мы можем сделать с Dagger. Если вам удобно преобразовывать действия во фрагменты, переходите к середине сообщения.

Фрагменты в 2020 году

Фрагменты прошли долгий путь со времен печально известного поста Защита фрагментов за квадратом в 2014 году. В то время по фрагментам было сложно перемещаться между ними, они были подвержены ошибкам и имели громоздкий жизненный цикл.

Перенесемся в 2020 год, и библиотека навигации androidx решает проблему навигации по фрагментам, ошибок немного, а жизненный цикл… ну, жизненный цикл все еще довольно громоздкий.

Но, к лучшему или худшему, архитектура с одним действием - это рекомендуемый подход от Google и, похоже, будущее разработки под Android. Итак, давайте продолжим и преобразуем этот проект в архитектуру единого действия и посмотрим, какие преимущества он может принести.

Одно мероприятие

Это будет единственное действие во всем приложении, концептуально похожее на класс вашего приложения, но специфичное для пользовательского интерфейса вашего приложения. Все наши фрагменты будут запущены в рамках этой активности.

Поскольку это наша единственная деятельность, хорошее место для ее размещения может быть прямым потомком папки пользовательского интерфейса.

Преобразование действий во фрагменты

В настоящее время есть два экрана, оба действия. Оба эти действия расширяют BaseActivity. Итак, изменение здесь простое: переименуйте базовое действие и измените класс, который он расширяет.

Наследование иногда может получить свою долю злоумышленников, но при правильном использовании оно может упростить изменение кода. В этом случае наш BaseActivity содержит всю нашу общую логику на основе экрана, которая одинаково хорошо применима к фрагментам. Здесь у нас есть только 2 действия для рефакторинга, но представьте себе более крупное приложение с 10 или 20? Изменяя одну строку, каждый Fragment имеет все необходимое нам обычное поведение экрана.

Есть области программирования, которыми регулярно злоупотребляют, в том числе наследование, но часто эти области также очень эффективны. Вместо того, чтобы избегать этих областей, лучшая стратегия может заключаться в том, чтобы узнать, когда они полезны, а когда нет.

Но предстоит еще кое-что сделать, чтобы наши экраны работали с жизненным циклом фрагмента.

Жизненный цикл фрагмента

В Activity наиболее распространенном коде инициализации используется onCreate. У фрагмента существенно более сложный жизненный цикл. Когда дело доходит до выбора, где разместить код, который инициализирует ваше представление, есть большой выбор. Я пытаюсь использовать:

  • onCreate для инициализации данных, например получение значений из Bundle.
  • onCreateView для раздувания просмотров. В последних версиях библиотеки фрагментов androidx идентификатор макета можно передать непосредственно в конструктор, так как я планирую добавить привязку данных к этому проекту, я не буду использовать здесь эту функцию.
  • onViewCreated для приведения представлений в их правильное состояние, например применение слушателей и начальных значений.
  • _8 _ / _ 9_ и т. Д. Как обычно

Существует множество противоречивых советов о том, какие методы жизненного цикла лучше всего использовать для чего. В конечном итоге найдите структуру, которая вам нравится, и придерживайтесь ее на протяжении всего проекта. И если вы когда-либо не уверены, какой метод жизненного цикла использовать, проверьте исходный код фрагмента - он хорошо документирован и может точно сказать вам, когда вызываются различные методы жизненного цикла для ваших целей.

И главный фрагмент

Хотя этот этап занимает довольно много времени, ничего сложного здесь нет. Весь код остается неизменным, он просто преобразован в новые методы жизненного цикла.

Код, которому нужна активность

Во фрагменте всегда есть места, где вам понадобится доступ к активности или контексту. Но в API нет гарантии, что контекст или действие будут отличаться от нуля, когда вам это нужно. Так что, чтобы избежать множества нулевых проверок, вам помогут библиотеки androidx.

При необходимости есть также requireActivity метод. А для случаев, когда вам конкретно нужно AppCompatActivity, ComponentActivity или даже ваше отдельное действие (_13 _), вы можете создать свою собственную функцию расширения.

При использовании этих методов помните, что он не совсем чистый. Используя такие методы, мы делаем предположения о том, где используется фрагмент и в каком состоянии фрагменты. В большинстве случаев это нормально, но иногда нет.

Обновите манифест

Теперь, когда все наши экраны преобразованы во фрагменты, наш манифест можно значительно упростить - с помощью одного объявления Activity!

Вот и все, архитектура единого действия готова (без навигации). Вы можете перемещаться между фрагментами вручную, но это часто бывает сложно и подвержено ошибкам. С библиотекой навигации androidx навигация становится проще простого. По библиотеке навигации есть много отличных документов и статей, поэтому я не буду вдаваться в подробности, как ее настроить. Но вы можете посмотреть репозиторий, чтобы увидеть его в действии.

Веселая инъекция зависимостей

Теперь самое интересное!

Обновление 17.06.2020:

С альфа-выпуском рукояти внедрение зависимостей с помощью Dagger станет намного проще! Hilt - это самоуверенная библиотека поверх кинжала, которая удаляет почти весь шаблонный код кинжала. Он упрощает DI и пытается стандартизировать то, как DI пишется в Android. С первого взгляда это огромное улучшение и наверняка станет предпочтительным подходом для DI в Android.

Остальная часть этой статьи посвящена некоторым принципам, касающимся DI, но имейте в виду, что с выпуском рукояти метод DI, используемый в этой статье, следует считать устаревшим, и на самом деле его намного проще реализовать с рукояткой.

Новый компонент

До сих пор наша установка Dagger состояла из одного компонента - AndroidPlaygroundComponent - и каждый объект в этом компоненте имеет доступ ко всем остальным объектам. Это отлично работает для простых приложений, но по мере того, как приложения становятся больше, нам часто приходится создавать дополнительные компоненты, которые имеют доступ только к ограниченному набору объектов.

Чтобы попробовать это, давайте создадим второй компонент ... на самом деле мы уже создавали дополнительные компоненты, каждый раз, когда используется @ContributesAndroidInjector, Dagger создает (специфичный для Android) подкомпонент за кулисами по причинам, которые я здесь не буду вдаваться - достаточно сказать, что ОС Android не дружелюбна к DI.

Зная @ContributesAndroidInjector, легко создать компонент для нашего единственного действия, который содержит объекты с заданной областью действия, к которым могут получить доступ только другие объекты в том же компоненте (psst. Это означает, что все эти объекты могут быть активными, только пока Activity жив).

Аннотация @ContributesAndroidInjector принимает параметр модулей, который определяет все объекты, доступные этому компоненту. Каждый объект в @ContributesAndroidInjector может получить доступ к любому другому объекту. Но если вы попытаетесь внедрить объект в другой компонент, dagger выдаст ошибку времени компиляции.

А если нам понадобится объект fromAndroidPlaygroundComponent? Нет проблем, каждый подкомпонент имеет доступ ко всем объектам в своем родительском компоненте, прародительском компоненте, прародительском компоненте и т. Д.

Добавление фрагментов на график

Поскольку мы используем архитектуру одного действия, фрагменты могут существовать только в одном действии. Таким образом, они также должны быть привязаны к компоненту Activity.

Вы заметите, что во фрагментах используется @ContributesAndroidInjector, это опять же из-за недружелюбия DI Android, что означает, что Dagger необходимо создать новый подкомпонент для каждого Fragment.

Здесь можно правильно сказать, что фрагменты не должны знать друг о друге на графике DI. Нет проблем, субкомпоненты не могут использовать объекты в других субкомпонентах, которые не являются их родительскими. Итак, в нашем мире кинжалов MainFragment ничего не знает о DetailsFragment и наоборот.

Но почему это полезно?

Возьмем что-то вроде навигации: в большинстве приложений для Android код для навигации разбросан по экранам. Это нарушает многие фундаментальные принципы хорошей архитектуры. Что вы понимаете при обновлении до библиотеки навигации androidx - необходимо внести изменения на КАЖДЫЙ экран, в больших приложениях это становится чрезвычайно громоздким. Проще говоря, распределенную ответственность сложно поддерживать, и она подвержена ошибкам.

Архитектурные принципы часто поощряют абстрагирование ответственности за интерфейсами, так что ответственность становится деталями реализации - их легко менять, не влияя на остальную часть приложения. В наши дни это обычно поощряется для уровня данных приложения, и детали реализации, такие как модернизация, часто скрыты за интерфейсами. Так почему бы не использовать ту же концепцию на уровне представления нашего приложения?

Абстрагирование навигации

Что, если наш Fragment не знает, как происходит навигация? Вместо этого фрагменты используют простой интерфейс навигации для упорядочивания навигации. Это упрощает фрагменты, ограничивая их ответственность и поощряя компонуемое поведение в пользовательском интерфейсе, уменьшая дублирование кода.

Поскольку это код, который не связан с Android, теперь он может жить на вашем бизнес-уровне (но желательно на уровне модели представления, если ваше приложение имеет такое количество слоев). И реализация будет жить на вашем уровне Android.

Обычно единственное требование к навигационному коду - это наличие Activity ссылки. К счастью, мы ранее добавили наше единственное действие в граф кинжала, чтобы его можно было внедрить в нашу реализацию. И поскольку область видимости навигатора ограничена Activity, его можно использовать только внутри графа объекта подкомпонентов, то есть только пока Activity жив.

Куда позвонить навигатору

Это немного сложнее.

Простой ответ - вставить навигатор в ваш фрагмент и вызвать его оттуда.

Но есть и другая возможность. Любой, кто использовал MVVM в Android, знаком с одноразовыми действиями - на эту тему есть множество средних статей. Например, модель представления хочет отобразить тост, но хочет отобразить его только один раз, поэтому должна сбросить состояние просмотра после отображения тоста, чтобы избежать повторного отображения тоста после события, такого как поворот экрана.

Поскольку в навигаторе теперь используется концепция, аналогичная бизнес-логике, и наши модели представления будут существовать только пока существует действие, наши модели представления могут стать частью подкомпонента действия, а навигатор может быть введен непосредственно в конструктор модели представления.

Это означает, что нет необходимости предоставлять одноразовые действия, такие как навигация по состояниям просмотра. И ваши модели просмотра становятся такими же простыми, как:

Теперь это очень просто тестировать или отлаживать, ваш код навигации абстрагируется до единственного класса, плюс нет необходимости в каких-то необычных одноразовых действиях в ваших состояниях просмотра.

Единственный недостаток этого подхода заключается в том, что если вы используете androidx ViewModel, этот подход НЕ РАБОТАЕТ. Причина в том, что у ViewModel более длительный жизненный цикл, чем у Activity. Используя этот метод, вы косвенно добавляете ссылку на Activity в ViewModel. Следовательно, при каждом повороте экрана будет происходить утечка Activity. Есть способы обойти это, но ничего красивого.

Так что, если вы используете ViewModel, вы застряли, вставляя навигатор в Fragment.

Расширение концепции

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

Заключение

Архитектура единого действия - это шаг вперед в разработке Android, и с навигационной библиотекой преобразование кодовой базы в эту структуру может быть легким делом. Это также открывает некоторые интересные архитектурные шаблоны, которые теперь требуют меньше шаблонов.

Идеи во второй половине статьи направлены на решение общей проблемы с кодом представления Android, где - даже в хорошо спроектированных кодовых базах - многие фундаментальные принципы, такие как SOLID, забыты, а такие функции, как навигация, разбросаны по действиям и фрагментам.

Главный принцип заключается в том, что, как и любой другой уровень вашего приложения, уровень представления может быть спроектирован как целевые классы составного поведения. Это упрощает изменение пользовательского интерфейса в дальнейшем, переходя с панелей закусок на диалоги для отображения объявлений? Один класс можно изменить, чтобы изменить способ отображения объявлений во всем приложении. Обновление библиотеки навигации androidx? Опять же, нужно изменить отдельный класс.

Многие части вашего представления становятся деталями реализации - их легко менять, не влияя на остальную часть вашего приложения.

Кроме того, упрощается тестирование. Для стандартных модульных тестов слишком большая ответственность приводит к долгим, сложным для написания и сопровождению тестам - запах кода. То же самое справедливо и для тестов пользовательского интерфейса: благодаря тому, что наш слой представления является составным, тесты пользовательского интерфейса намного проще писать и поддерживать.

Я использовал этот подход в нескольких приложениях и обнаружил, что он упрощает разработку, меньше дублируется кода, меньше ошибок и, что наиболее важно, большие изменения пользовательского интерфейса становятся проще простого. Так что ты думаешь? Такой подход полезен?

Окончательный код для этого раздела - здесь.