Pull to refresh
0
e-legion
Делаем приложения, которыми пользуются миллионы

Получаем результат правильно (Часть 2). Fragment Result API

Reading time6 min
Views16K

Мы продолжаем рассказ о новинках библиотеки Jetpack, призванных упростить обмен данными между компонентами Android приложения. Первая часть была посвящена передаче данных из Activity и новому Api Activity Result.  

На этот раз посмотрим, какое решение Google предлагает для Fragment. Ввиду популярности паттерна “Single Activity” работа с фрагментами представляет большой практический интерес для многих Android-разработчиков.

“Как передать данные между двумя фрагментами?” - частый вопрос на собеседованиях. Ответить на него можно по-разному: создание общей ViewModel, имплементация интерфейса в Activity, использование targetFragment и другие способы. 

С появлением Fragment Result Api в этот список добавился простой способ передачи небольшого объема информации из одного фрагмента в другой. Например, возвращение результата какого-либо пользовательского сценария. Мы разберем, как применять новый Api на практике, но сначала немного теории.

Теория

Начиная с версии 1.3.0-alpha04, FragmentManager реализует интерфейс FragmentResultOwner. Это означает, что FragmentManger является диспетчером для результатов, которые отправляют фрагменты. Благодаря этому фрагменты могут обмениваться информацией, не имея прямых ссылок друг на друга.

Таким образом, всё взаимодействие происходит через FragmentManager:

  • Если фрагмент ожидает получить некоторые данные от другого фрагмента, он должен зарегистрировать слушатель во FragmentManger с помощью метода setFragmentResultListener()

  • Если фрагменту необходимо вернуть результат другому фрагменту, он передает FragmentManger объект Bundle, содержащий информацию. Для этого вызывается метод setFragmentResult()

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

Упрощенно данную схему можно представить так:

FragmentB передает данные в FragmentA . FragmentManager выполняет роль диспетчера
FragmentB передает данные в FragmentA . FragmentManager выполняет роль диспетчера

Достоинством Fragment Result Api является lifecycle-безопасность - результат передается во фрагмент, только когда тот достиг состояния STARTED, но еще не находится в состоянии DESTROYED. 

“Под капотом” FragmentManger хранит все зарегистрированные слушатели и все отправленные результаты в потокобезопасных реализациях Map:

  • Map<String, Bundle> для результатов, отправленных фрагментами 

  • Map<String, LifecycleAwareResultListener> для зарегистрированных слушателей

Когда фрагмент регистрирует FragmentResultListener, FragmentManager добавляет его в Map, а при уничтожении фрагмента, слушатель удаляется из Map. Для того, чтобы учитывать жизненный цикл фрагмента, FragmentResultListener оборачивается в LifecycleAwareResultListener.

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

А теперь практика.

Практика

В качестве примера возьмем следующий кейс: ProductsFragment содержит список товаров, которые можно сортировать по различным критериям, а SortFragment позволяет указать нужную сортировку. Информация о выбранной сортировке будет передаваться с помощью Fragment Result Api. 

Так выглядит итоговая реализация, которую можно найти по ссылке ниже
Так выглядит итоговая реализация, которую можно найти по ссылке ниже

В коде все выглядит довольно просто. Чтобы наладить передачу результата, необходимо выполнить всего два шага. 

Шаг 1

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

Регистрацию слушателя можно произвести в колбеке onCreate():

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setFragmentResultListener("request_key") { key, bundle ->
        val selectedSort = bundle.getParcelable<Sort>("extra_key")
        // применение полученной сортировки
   }
}

Шаг 2

Когда SortFragment будет готов отправить результат, вызывается метод setFragmentResult, в который передается тот же строковый ключ и заполненный объект Bundle. 

applyButton.setOnClickListener {
   setFragmentResult(
      "request_key",
       bundleOf("extra_key" to getSelectedSort())
   )
}

Вот и всё, что требуется для передачи результата с помощью Fragment Result Api. 

Важно

Хотя Api довольно прост, стоит разобрать некоторые нюансы его работы, связанные с правильным выбором FragmentManager и жизненным циклом фрагментов.

Выбор FragmentManager

FragmentManager выполняет основную работу в передаче результата от одного фрагмента к другому. Но каждому фрагменту доступен выбор из нескольких вариантов: parentFragmentManager, childFragmentManager и FragmentManager у активити-хоста. Разберемся, в каких случаях стоит выбирать тот или иной FragmentManager.

Сначала представим так называемую master-detail конфигурацию. Активити содержит два фрагмента, FragmentA и FragmentB, между которыми требуется передать результат.

Активити является хостом для FragmentA и FragmentB
Активити является хостом для FragmentA и FragmentB

В таком случае передавать результат между фрагментами может FragmentManager активити-хоста, т.к. доступ к нему имеют оба фрагмента. Получить данный FragmentManager можно путем вызова requireActivity().supportFragmentManager либо parentFragmentManager.

Следующая ситуация характерна, например, для открытия DialogFragment или в случае, если FragmentA размещает внутри себя FragmentC. 

FragmentA является хостом для FragmentС
FragmentA является хостом для FragmentС

При таком сценарии, передать результат из FragmentС в FragmentA можно двумя способами:

  • Через FragmentManager активити с помощью requireActivity().supportFragmentManager

  • Через дочерний FragmentManager у FragmentA. Чтобы получить на него ссылку, FragmentA должен обращаться к childFragmentManager, а FragmentС к parentFragmentManager.

Особенности Lifeсycle

Как уже сказано, Fragment Result Api обеспечивает lifecycle-безопасность - результат доставляется, только если фрагмент находится на экране. Рассмотрим несколько примеров. 

Представим стандартный случай - фрагмент подписывается в колбеке onCreate, затем переходит в состояние STARTED, и как только другой фрагмент передает во FragmentManager результат, фрагмент-подписчик его получает.

Стандартный сценарий
Стандартный сценарий

Если еще до перехода фрагмента в состояние STARTED, во FragmentManager было передано несколько результатов, то фрагмент получит лишь последний из них (так как FragmentManager хранит результаты в Map<String, Bundle>, то каждый последующий перезаписывает предыдущий).

Фрагмент получит лишь bundle3, так как он был отправлен последним
Фрагмент получит лишь bundle3, так как он был отправлен последним

Теперь представим ситуацию, когда фрагмент был закрыт до отправки результата во FragmentManager. В таком случае подписка закроется сразу по достижении состояния DESTROYED и FragmentManager не будет передавать результат во фрагмент.  

Автоматическая отписка фрагментов происходит при достижении 
состояния DESTROYED
Автоматическая отписка фрагментов происходит при достижении состояния DESTROYED

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

Если фрагмент фрагмент-подписчик был закрыт до отправки результата,
 он получит его при повторном открытии.
Если фрагмент фрагмент-подписчик был закрыт до отправки результата, он получит его при повторном открытии.

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

Сценарий при нахождении фрагмента в бэкстеке в момент 
передачи результата
Сценарий при нахождении фрагмента в бэкстеке в момент передачи результата

Все рассмотренные ситуации объединяет то, что фрагмент подписывался по уникальному строковому ключу. Но что если сразу несколько подписчиков будут использовать один и тот же ключ? Напомним, что FragmentManager сохраняет информацию о подписках в Map<String, LifecycleAwareListener>, следовательно не может содержать несколько записей с одним и тем же ключом. Именно поэтому результат будет доставлен в тот фрагмент, который зарегистрировал слушатель последним.

Результат получает только последний подписчик
Результат получает только последний подписчик

Заключение

Подводя итог, отметим достоинства нового способа передачи результата между фрагментами: 

  • Fragment Result Api является стабильным, можно не бояться использовать его в продакшене. Тем, кто использует targetFrament особенно стоит присмотреться, ведь targetFrament стал Deprecated.

  • Api прост в использовании и не требует написания большого количества кода

  • Учитывает жизненный цикл фрагментов - при получении результата, можно сразу работать со view фрагмента

  • Позволяет пережить изменение конфигурации и даже смерть процесса (FragmentManager умеет сохранять данные о переданных результатах в Parcelable)

Но присутствуют и недостатки: 

  • необходимо следить за уникальностью строковых ключей, а также выбирать правильное место для их хранения

  • так как результат передается в Bundle, отсутствует его типизация. При неаккуратном обращении, можно получить ClassCastException. 

В целом, Fragment Result Api оставляет положительное впечатление, и точно стоит того, чтобы его опробовать, а наглядный пример можно найти по ссылке

Tags:
Hubs:
+3
Comments2

Articles

Information

Website
www.e-legion.ru
Registered
Founded
Employees
101–200 employees
Location
Россия