Руководство разработчика Android по шаблону навигации по фрагментам
Опубликовано: 2022-03-11За прошедшие годы я видел множество различных реализаций шаблонов навигации в Android. Некоторые приложения использовали только действия, в то время как другие действия смешивались с фрагментами и/или пользовательскими представлениями.
Одна из моих любимых реализаций шаблона навигации основана на философии «Одно действие — несколько фрагментов» или просто на шаблоне навигации фрагментов, где каждый экран в приложении является полноэкранным фрагментом, и все или большинство этих фрагментов содержатся в одно действие.
Такой подход не только упрощает реализацию навигации, но и обеспечивает более высокую производительность и, следовательно, более удобный пользовательский интерфейс.
В этой статье мы рассмотрим некоторые распространенные реализации шаблонов навигации в Android, а затем представим шаблон навигации на основе фрагментов, сравнив и сопоставив его с другими. Демонстрационное приложение, реализующее этот шаблон, было загружено на GitHub.
Мир деятельности
Типичное Android-приложение, использующее только действия, организовано в виде древовидной структуры (точнее, в ориентированный граф), где корневое действие запускается лаунчером. Когда вы перемещаетесь по приложению, ОС поддерживает стек активности.
Простой пример показан на диаграмме ниже:
Активность A1 — это точка входа в наше приложение (например, она представляет собой заставку или главное меню), и из нее пользователь может перейти к A2 или A3. Когда вам нужно обмениваться данными между действиями, вы можете использовать startActivityForResult() или, возможно, вы разделяете между ними глобально доступный объект бизнес-логики.
Когда вам нужно добавить новую активность, вам необходимо выполнить следующие шаги:
- Определить новую активность
- Зарегистрируйте его в AndroidManifest.xml .
- Откройте его с помощью startActivity() из другого действия.
Конечно, эта навигационная диаграмма является довольно упрощенным подходом. Это может стать очень сложным, когда вам нужно манипулировать обратным стеком или когда вам нужно повторно использовать одну и ту же активность несколько раз, например, когда вы хотите провести пользователя через некоторые обучающие экраны, но на самом деле каждый экран использует ту же активность, что и экран. основание.
К счастью, у нас есть инструменты для этого, называемые задачами, и некоторые рекомендации по правильной навигации по стеку.
Затем с API уровня 11 появились фрагменты…
Мир фрагментов
Android представила фрагменты в Android 3.0 (уровень API 11), в первую очередь для поддержки более динамичного и гибкого дизайна пользовательского интерфейса на больших экранах, таких как планшеты. Поскольку экран планшета намного больше, чем у мобильного телефона, появляется больше возможностей для комбинирования и замены компонентов пользовательского интерфейса. Фрагменты позволяют создавать такие проекты без необходимости управления сложными изменениями в иерархии представлений. Разделив макет действия на фрагменты, вы получаете возможность изменять внешний вид действия во время выполнения и сохранять эти изменения в резервном стеке, которым управляет действие. – цитата из руководства Google API для фрагментов.
Эта новая игрушка позволила разработчикам создать пользовательский интерфейс с несколькими панелями и повторно использовать компоненты в других действиях. Некоторым разработчикам это нравится, а другим нет. Популярны споры о том, использовать фрагменты или нет, но я думаю, что все согласятся с тем, что фрагменты вносят дополнительную сложность, и разработчикам действительно нужно понимать их, чтобы использовать их правильно.
Полноэкранный кошмар фрагмента в Android
Я стал видеть все больше и больше примеров, когда фрагменты представляли собой не просто часть экрана, а фактически весь экран был фрагментом, содержащимся в активности. Однажды я даже видел дизайн, в котором у каждой активности был ровно один полноэкранный фрагмент и ничего больше, и единственная причина, по которой эти активности существовали, заключалась в том, чтобы размещать эти фрагменты. Помимо очевидного недостатка дизайна, у этого подхода есть еще одна проблема. Взгляните на схему снизу:
Как А1 может общаться с F1? Что ж, A1 имеет полный контроль над F1, так как он создал F1. A1 может передавать пакет, например, при создании F1 или может вызывать его общедоступные методы. Как F1 может общаться с A1? Ну, это более сложно, но его можно решить с помощью шаблона обратного вызова/наблюдателя, где A1 подписывается на F1, а F1 уведомляет A1.
Но как А1 и А2 могут общаться друг с другом? Это уже было рассмотрено, например, с помощью startActivityForResult() .
И теперь возникает реальный вопрос: как F1 и F2 могут общаться друг с другом? Даже в этом случае у нас может быть глобально доступный компонент бизнес-логики, поэтому его можно использовать для передачи данных. Но это не всегда приводит к элегантному дизайну. Что, если F2 нужно передать некоторые данные F1 более прямым способом? Что ж, с шаблоном обратного вызова F2 может уведомить A2, затем A2 завершает работу с результатом, и этот результат фиксируется A1, который уведомляет F1.
Этот подход требует большого количества шаблонного кода и быстро становится источником ошибок, боли и гнева.
Что, если бы мы могли избавиться от всех действий и оставить только одно из них, которое содержит остальные фрагменты?
Шаблон навигации по фрагментам
С годами я начал использовать паттерн «Одно действие — несколько фрагментов» в большинстве своих приложений и до сих пор его использую. Существует много дискуссий об этом подходе, например, здесь и здесь. Однако я упустил конкретный пример, который я могу увидеть и проверить сам.
Давайте посмотрим на следующую схему:
Теперь у нас есть только одно контейнерное действие и несколько фрагментов, которые снова имеют древовидную структуру. Навигация между ними осуществляется FragmentManager , у него есть задний стек.
Обратите внимание, что теперь у нас нет метода startActivityForResult() , но мы можем реализовать шаблон обратного вызова/наблюдателя. Давайте посмотрим на некоторые плюсы и минусы этого подхода:
Плюсы:
1. Более чистый и удобный в сопровождении AndroidManifest.xml
Теперь, когда у нас есть только одно действие, нам больше не нужно обновлять манифест каждый раз, когда мы добавляем новый экран. В отличие от действий, нам не нужно объявлять фрагменты.
Это может показаться второстепенным, но для более крупных приложений с более чем 50 действиями это может значительно улучшить читаемость файла AndroidManifest.xml .
Посмотрите на файл манифеста примера приложения с несколькими экранами. Файл манифеста по-прежнему остается очень простым.

<?xml version="1.0" encoding="utf-8"?> package="com.exarlabs.android.fragmentnavigationdemo.ui" > <application android:name= ".FragmentNavigationDemoApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.exarlabs.android.fragmentnavigationdemo.ui.MainActivity" android:label="@string/app_name" android:screenOrientation="portrait" android:theme="@style/AppTheme.NoActionBar" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
2. Централизованное управление навигацией
В моем примере кода вы увидите, что я использую NavigationManager , который в моем случае внедряется в каждый фрагмент. Этот менеджер можно использовать как централизованное место для ведения журналов, управления бэкстеком и т. д., поэтому поведение навигации отделено от остальной бизнес-логики и не разбросано по реализациям разных экранов.
Давайте представим ситуацию, когда мы хотели бы запустить экран, на котором пользователь может выбрать некоторые элементы из списка людей. Вы также хотели бы передать некоторые аргументы фильтрации, такие как возраст, профессия и пол.
В случае действий вы должны написать:
Intent intent = new Intent(); intent.putExtra("age", 40); intent.putExtra("occupation", "developer"); intent.putExtra("gender", "female"); startActivityForResult(intent, 100);
Затем вам нужно определить onActivityResult где-то ниже и обработать результат.
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); }
Моя личная проблема с этим подходом заключается в том, что эти аргументы являются «дополнительными» и не являются обязательными, поэтому я должен убедиться, что принимающая активность обрабатывает все различные случаи, когда дополнительный отсутствует. Позже, когда будет произведен некоторый рефакторинг и, например, дополнительный «возраст» больше не нужен, мне придется искать везде в коде, где я начинаю это действие, и удостовериться, что все дополнения верны.
Кроме того, не было бы лучше, если бы результат (список лиц) приходил в виде _List
В случае навигации на основе фрагментов все проще. Все, что вам нужно сделать, это написать в NavigationManager метод startPersonSelectorFragment() с необходимыми аргументами и реализацией обратного вызова.
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", new PersonSelectorFragment.OnPersonSelectedListener() { @Override public boolean onPersonsSelected(List<Person> selection) { [do something] return false; } });
Или с РетроЛямбда
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", selection -> [do something]);
3. Улучшенные средства связи между экранами
Между действиями мы можем использовать только Bundle, который может содержать примитивы или сериализованные данные. Теперь с помощью фрагментов мы можем реализовать шаблон обратного вызова, где, например, F1 может прослушивать передачу F2 произвольных объектов. Пожалуйста, взгляните на реализацию обратного вызова в предыдущих примерах, которая возвращает _List
4. Строительные фрагменты дешевле, чем строительные работы.
Это становится очевидным, когда вы используете ящик, который имеет, например, 5 пунктов меню, и на каждой странице ящик должен отображаться снова.
В случае чистой навигации по действиям каждая страница должна раздувать и инициализировать ящик, что, конечно, дорого.
На приведенной ниже диаграмме вы можете увидеть несколько корневых фрагментов (FR*), которые представляют собой полноэкранные фрагменты, к которым можно получить доступ непосредственно из ящика, а также ящик доступен только тогда, когда отображаются эти фрагменты. Все, что находится справа от пунктирной линии на схеме, приведено как пример произвольной схемы навигации.
Поскольку действие контейнера содержит ящик, у нас есть только один экземпляр ящика, поэтому на каждом шаге навигации, где ящик должен быть виден, вам не нужно снова надувать и инициализировать его. Все еще не уверены, как все это работает? Взгляните на мой пример приложения, демонстрирующий использование ящика.
Минусы
Моим самым большим страхом всегда было то, что если я буду использовать шаблон навигации на основе фрагментов в проекте, где-нибудь в будущем я столкнусь с непредвиденной проблемой, которую будет трудно решить из-за дополнительной сложности фрагментов, сторонних библиотек и различных версий ОС. Что, если бы мне пришлось реорганизовать все, что я сделал до сих пор?
Действительно, мне приходилось решать проблемы с вложенными фрагментами, сторонними библиотеками, которые также используют такие фрагменты, как ShinobiControls, ViewPagers и FragmentStatePagerAdapters.
Должен признать, что получение достаточного опыта работы с фрагментами, чтобы иметь возможность решать эти задачи, было довольно долгим процессом. Но во всех случаях дело было не в том, что философия плоха, а в том, что я недостаточно хорошо понимал фрагменты. Возможно, если бы вы понимали фрагменты лучше, чем я, вы бы даже не столкнулись с этими проблемами.
Единственный недостаток, который я могу упомянуть сейчас, заключается в том, что мы все еще можем сталкиваться с проблемами, которые было бы непросто решить, поскольку нет зрелой библиотеки, которая демонстрирует все сложные сценарии сложного приложения с навигацией на основе фрагментов.
Заключение
В этой статье мы рассмотрели альтернативный способ реализации навигации в приложении для Android. Мы сравнили его с традиционной философией навигации, использующей действия, и увидели несколько веских причин, по которым его лучше использовать по сравнению с традиционным подходом.
Если вы еще этого не сделали, ознакомьтесь с демонстрационным приложением, загруженным на GitHub. Не стесняйтесь форкнуть или внести свой вклад в него с более хорошими примерами, которые лучше покажут его использование.