В течение последних лет можно было наблюдать использование различных подходов к построению навигации в Android-приложениях. Кто-то использует только активности (Activity), кто-то смешивает их с фрагментами (Fragment) и/или с модифицированными видами (Custom View).
Один из способов реализации навигации в приложении основывается на философии «Одна активность — много фрагментов». При таком подходе каждый экран приложения состоит из многочисленных фрагментов, при этом все или большинство из них содержат по одной активности. Такое решение упрощает реализацию навигации и улучшает производительность, что положительно сказывается на впечатлении от использования приложения.
В этой статье мы рассмотрим несколько характерных подходов к реализации навигации в Android, а затем поговорим о подходе с использованием фрагментов, сравнив его с другими подходами. Демонстрационное приложение, на примере которого иллюстрирована статья, можно скачать с GitHub.
Мир Activity
Типичное Android-приложение, использующее активность, имеет древовидную структуру (точнее, структуру ориентированного графа), при которой активность, инициализируемая при запуске приложения, является «корнем». В ходе использования приложения операционная система поддерживает работу стека переходов назад (back stack). Простой пример:
Активность A1 является входной точкой приложения (это может быть экран-заставка или основное меню), из которой пользователь может перейти к A2 или A3. Для организации взаимодействия между активностями можно использовать startActivityForResult()
, либо сделать общую, глобально доступную бизнес-логику.
Если вы хотите добавить новую активность, то нужно выполнить следующие шаги:
- Задать саму активность.
- Зарегистрировать её в AndroidManifest.xml.
- Открыть её из другой активности с помощью startActivity().
Конечно, вышеприведённая схема описывает весьма упрощённый подход. Всё может оказаться куда сложнее, если вам понадобится манипулировать стеком переходов назад. Или если вы захотите несколько раз использовать одну и ту же активность: например, пользователь будет переходить между разными обучающими экранами, в основе каждого из которых будет лежать одна и та же активность.
К счастью, у нас есть такие инструменты, как задачи и гайдлайны по правильной работе со стеком переходов назад. Однако с появлением API level 11 были внедрены фрагменты…
Мир фрагментов
Как утверждается в официальной документации от Google, «фрагменты были представлены в Android 3.0 (API level 11) в первую очередь для создания более динамичных и гибких интерфейсов, адаптированных для больших экранов, например, планшетов. У них площадь экранов гораздо больше, чем у смартфонов, соответственно больше места для комбинирования и перестановки элементов интерфейса. Благодаря фрагментам вам не придётся управлять сложными изменениями в иерархии видов при создании подобных интерфейсов. Разделяя структуру активности на фрагменты, вы получаете возможность изменять её отображение в ходе выполнения программы, защищая регулируемые активностью изменения в стеке переходов назад».
Этот новый инструмент позволил разработчикам создавать интерфейсы, состоящие из нескольких отдельных секций, а также повторно использовать компоненты в других активностях. Кому-то это нравится, кому-то нет. Сегодня много спорят о том, нужно ли использовать фрагменты, но, вероятно, все согласятся с тем, что с их появлением выросла сложность разработки и приложений, поэтому разработчики должны хорошо разбираться в том, как правильно применять фрагменты.
Полноэкранные фрагменты в роли ночного кошмара
Всё чаще можно было встретить приложения, в которых не часть экрана состоит из нескольких фрагментов, а весь экран представляет собой один фрагмент, помещённый в активность. Можно даже наткнуться на поделки, в которых каждая активность содержит по одному полноэкранному фрагменту, и ничего более. То есть активности выступают исключительно в роли контейнера для этих фрагментов. У такого подхода, помимо очевидной ошибки проектирования, есть и другая проблема. Взгляните на схему:
Как А1 будет взаимодействовать с F1? Да, А1 полностью контролирует F1, ведь это она его создала. Например, А1 могла бы во время создания передать F1 какой-нибудь бандл (bundle), или вызвать его открытые методы.
А как F1 будет взаимодействовать с А1? Здесь уже несколько сложнее, но можно выйти из положения с помощью методики callback/observer, при которой А1 подписывается на F1, а тот уведомляет своего подписчика.
Ладно, а что насчёт взаимодействия между А1 и А2? Эта задача решается, например, с помощью startActivityForResult()
.
А теперь вопрос: как будут взаимодействовать друг с другом F1 и F2? Даже в этом случае у нас может быть общедоступный компонент, содержащий бизнес-логику, который и можно использовать для передачи данных. Но это не всегда помогает создать красивый дизайн. Допустим, F2 должен передать F1 какие-то данные. При использовании callback-методики F2 может уведомить А2, тот генерирует какой-то результат, А1 его получает и уведомляет F1.
При таком подходе придётся использовать много шаблонного кода, который быстро станет источником багов, боли и раздражения. А что если нам избавиться от всех этих активностей, оставив лишь одну из них, поместив в неё остальные фрагменты?
Методика создания навигации на основе фрагментов
Подход, который будет описан, вызывает много споров (пример 1, пример 2). Давайте рассмотрим конкретный пример.
Теперь у нас осталась единственная активность, выполняющая роль контейнера. Она содержит несколько фрагментов, составляющих древовидную структуру. Навигация между ними осуществляется с помощью FragmentManager
, имеющего собственный стек переходов назад. Обратите внимание, что здесь отсутствует startActivityForResult()
, но мы всё же можем реализовать методику callback/observer. Давайте разберём преимущества и недостатки этого подхода.
Преимущества:
1. Файл 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()
со всеми необходимыми аргументами реализацией callback'ов.
mNavigationManager.startPersonSelectorFragment(40, "developer", "female",
new PersonSelectorFragment.OnPersonSelectedListener() {
@Override
public boolean onPersonsSelected(List<Person> selection) {
[do something]
return false;
}
});
Или с RetroLambda:
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", selection -> [do something]);
3. Улучшение взаимодействия между экранами
Между активностями могут передаваться только бандлы, содержащие примитивы или сериализованные данные. А благодаря фрагментам есть возможность реализовать callback-методику, когда F1, к примеру, может получать от F2 произвольные объекты. Посмотрите предыдущие примеры реализации callback'ов, в которых возвращается _List_.
4. Создание фрагментом обходится дешевле создания активностей
Это становится очевидным при использовании навигационной панели (drawer), содержащей несколько элементов меню, которая должна отображаться на каждой странице.
Если ваша навигация построена на одних лишь активностях, то каждая страница должна «раздуть» (inflate) и инициализировать панель, что потребляет немало ресурсов.
На следующей схеме представлено несколько корневых фрагментов (FR*), являющихся полноэкранными. Получить к ним доступ можно напрямую из панели, а к ней самой можно обратиться лишь тогда, когда отображается один из корневых фрагментов. Часть схемы по правую сторону от пунктирной линии — пример произвольной схемы навигации.
Поскольку панель находится внутри контейнера, то у нас есть лишь один её экземпляр. Поэтому панель должна быть видимой на каждом этапе навигации, её не нужно вновь «раздувать» и инициализировать. Не совсем понятно, как это работает? Проанализируйте приложение-пример, там демонстрируется использование панели.
Недостатки
Начав реализовывать навигацию на основе фрагментов, было бы очень неприятно углубиться в разработку и столкнуться с какими-то непредвиденными, трудноразрешимыми проблемами, связанными с дополнительной сложностью использования фрагментов, сторонними библиотеками и разными версиями ОС. А если придётся рефакторить всё то, что мы уже понаписали?
Действительно, придётся решать проблемы, связанные со вложенными фрагментами, а также со сторонними библиотеками, тоже использующими фрагменты, наподобие ShinobiControls, ViewPagers и FragmentStatePagerAdapters.
Надо сказать, что может потребоваться немало времени на приобретение достаточного опыта, позволяющего справиться с затруднениями. Правда, их наличие чаще всего обусловлено неполным пониманием особенностей работы с фрагментами, а не порочностью самой идеи. Возможно, у вас уже достаточно опыта и вы вообще не столкнётесь ни с какими проблемами.
Так что единственным недостатком, достойным упоминания, является нехватка хороших библиотек, в которых были бы отражены все сложные сценарии из сложных приложений, чья навигация построена на фрагментах.
Заключение
В этой статье мы рассмотрели один из подходов к реализации навигации в Android-приложениях. Было проведено сравнение с традиционным подходом, подразумевающим использование активностей, выявившее ряд преимуществ в сравнении с «классикой».
На тот случай, если вы до сих пор не посмотрели приложение-пример, добро пожаловать на GitHub. Можете свободно форкать или дополнять его более удачными и наглядными решениями.
Автор: NIX Solutions