Роль Fragments в современной разработке приложений для Android

Fragment  —  одна из первых библиотек Android. Ее цель  —  унифицировать действия разработчиков на всех устройствах Android. Кроме того, фрагменты, позволяющие разделять пользовательский интерфейс (UI) на отдельные блоки, привнесли модульность и возможность повторно использовать UI activity.

Сейчас роль фрагментов в разработке приложений Android значительно изменилась. Google обычно объявляет об устаревших API в последних релизах, поскольку конечная цель  —  переход от ориентированных на фрагменты API к отдельно тестируемым компонентам, которые интегрируются с фрагментами. Рассмотрим ряд основных понятий, связанных с новой ролью фрагментов.

Введение и обзор

Как мы уже отметили, Fragment  —  одна из первых библиотек Android. Ее предназначение в том, чтобы обеспечивать единообразный подход к разработке на любом устройстве Android.

В дополнение к этому, позволяя разделять UI на отдельные блоки, фрагменты привнесли модульность и возможность повторно использовать UI activity. Это означает, что у них есть собственные View, Lifecycle, SavedInstanceState, Callbacks и onActivityResult. На деле чаще всего единственным способом получить обратные вызовы было переопределение метода во фрагменте.

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

Таким образом, роль фрагментов в современной разработке Android очень изменилась. Иначе говоря, фрагмент по-прежнему содержит view, которое однако может быть реализовано с помощью Android views или Jetpack Compose. Управляя другими обязанностями с помощью отдельно тестируемых компонентов, можно выбирать используемые лишь при необходимости.

Использование Fragments в современной разработке Android

AndroidX Lifecycle

Пакет android.lifecycle включает классы и интерфейсы, позволяющие создавать компоненты с учетом жизненного цикла. Таким образом, эти компоненты могут автоматически изменять свое поведение в зависимости от текущего состояния жизненного цикла activity и fragment.

Сегодня наблюдаемый жизненный цикл AndroidX играет важную роль для фрагментов при разработке приложений. Один из примеров  —  работа с View Pager. Когда вы проводите пальцем по нескольким фрагментам на экране, важно получать определенные сигналы внутри текущего фрагмента. Таким образом, запускать ресурсоемкие операции (например, определение места пользователя на фрагменте карты) можно только после выбора пользователем этого фрагмента.

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

В ViewPager 2 возобновляется только этот текущий фрагмент. Таким образом, другие будут в запущенном состоянии. Поэтому сбор потока сопрограммы Kotlin происходит только тогда, когда текущий фрагмент в ViewPager может быть выполнен через вызов repeatOnLifecycle с возобновленным состоянием.

Когда пользователь проводит пальцем по экрану в обратном направлении, фрагмент приостанавливается, а блок repeatOnLifecycle автоматически отменяется. Поэтому не требуется отслеживать какие-либо дополнительные состояния. Например:

override fun onViewCreated(view: View, savedInstanceState: Bundle?){

  viewLifecycleOwner.lifecycleScope.launch {
      viewLifecycleOwner.repeatOnLifecycle(RESUMED) {
          viewModel.someDataFlow.collect {
             //...
   
                }
            }
        }
    }

Уровень приложения UI и ViewModel

Чтобы получить согласованную и надежную область действия, можно применить жизненный цикл AndroidX. Однако проблема в том, что жизненный цикл привязан к существованию фрагмента или его view (представлению).

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

Для успешного решения этой задачи Google предлагает ряд рекомендаций и лучших практик по разработке производительных приложений Android с эффективной архитектурой. Рекомендуется разделить уровень UI в архитектуре приложения на два слоя. Как правило, UI включает две составляющих: UI elements + UI state = UI.

В этом случае элементы UI принадлежат непосредственно фрагменту для рендеринга данных на экране, а держатели состояния (State holders) взаимодействуют с другими уровнями приложения Android. Уровень держателей обеспечивает состояние UI для фрагмента и обрабатывает из него события, которые обновляют состояние. В целом, State holders, например классы ViewModel, могут эффективно хранить данные, предоставлять их UI и управлять логикой.

Фактически каждый фрагмент реализует интерфейс ViewModelStoreOwner. Это означает, что можно связать одну или несколько ViewModels с фрагментом. При этом они будут очищены автоматически, когда фрагмент будет сброшен из обратного стека (back stack).

Для связанного с одним фрагментом ViewModel, этот фрагмент не должен взаимодействовать с сетью или загрузкой данных. В этом случае уровень ViewModel должен поддерживать Kotlin Flow (или другой наблюдаемый источник данных) для фрагмента при применении к представлениям.

Поток (flow) в сопрограммах  —  это тип, который может последовательно выдавать несколько значений, в отличие от функций приостановки (suspend functions), которые возвращают только одно значение.

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

Кроме того, ViewModel может быть совместно используемым ресурсом, достаточно задействовать другой ViewModelStoreOwner. Например, если у вас есть ViewPager 2 внутри другого фрагмента, каждая страница может получить доступ к одной и той же ViewModel, используя родительский фрагмент в качестве ViewModelStoreOwner.

Эта концепция также применима к области действия ViewModels (activity scope) и при использовании navigation component с фрагментами к навигационному графу в области View Models.

Изначально библиотека Fragment предлагает два варианта взаимодействия с фрагментами: совместно используемая ViewModel и API Fragment Result. В первом случае можно столкнуться с такими вариантами.

  1. Совместное использование данных с activity хоста.
  2. Обмен данными между фрагментами.
  3. Совместное использование данных родительским и дочерним фрагментами.
  4. Привязка ViewModel к Navigation Graph (если использовать библиотеку Navigation).

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

Для примера возьмем фрагмент, который считывает QR-коды и возвращает данные предыдущему фрагменту. Для получения результатов в этом случае можно использовать вторую опцию, как упоминалось ранее, называемую API Fragment Result.

Каждый FragmentManager реализует FragmentResultOwner из версии Fragment 1.3.0. Это изменение позволяет компонентам взаимодействовать друг с другом, устанавливая результаты фрагмента и прослушивание этих результатов.

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

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFragmentResultListener("requestKey") { requestKey, bundle ->
val result = bundle.getString("bundleKey")
//...
}
}

Кроме того, при взаимодействии вы увидите следующий код для второго фрагмента:

button.setOnClickListener {
val result = "result"
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

Затем первый фрагмент получает результат и выполняет обратный вызов прослушивателя, как только фрагмент запускается.

Заключение

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

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Kayvan Kaseb: The Role of Fragments in Modern Android Development

Предыдущая статьяРуководство по применению паттерна Event Bus в архитектуре React 
Следующая статьяКак работает шлюз API на Golang: на примере одного симпатичного платья