Android Fragment Result Listener



    В Android передача данных между фрагментами может осуществляться разными способами: передача через родительскую Activity, используя ViewModel или даже Fragments API. Fragment Target API с недавних пор получил статус Deprecated и вместо него Google рекомендует использовать Fragment result API.


    Что такое Fragment result API? Это новый инструмент от Google который позволяет передавать данные между фрагментами по ключу. Для этого используется FragmentManager, который в свою очередь реализует интерфейс FragmentResultOwner. FragmentResultOwner выступает в качестве центрального хранилища для данных, которые мы передаем между фрагментами.


    Как это работает?


    image

    Как упоминалось выше, наш FragmentManager реализует интерфейс FragmentResultOwner, который хранит в себе ConcurrentHashMap<String, Bundle>. Эта HashMap хранит наши Bundle-ы по строковому ключу. Как только один из фрагментов подписывается (или уже подписан) то он получает результат по тому самому ключу.


    Что важно знать:


    • Если какой-либо фрагмент подписывается на результат методом setResultFragmentListener() после того, как отправляющий фрагмент вызовет setFragmentResult(), то он немедленно получит результат
    • Каждую связку “Key + Result (Bundle)“ фрагмент получает только 1 раз
    • Фрагменты которые находятся в бек стеке получат результат только после того как перейдут в состояние STARTED
    • После того как фрагмент перейдет в состояние DESTROYED мы больше не сможем подписываться на ResultListener

    Как это выглядит в коде?


    Передача данных


    Для передачи данных в другой фрагмент нам необходимо вызвать метод:


    FragmentManager.setFragmentResult(key: String, bundle: Bundle)

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


    Kotlin


    button.setOnClickListener {
        val result = "result"
        // Здесь мы можем использовать Kotlin экстеншен функцию из fragment-ktx
        setFragmentResult("requestKey", bundleOf("bundleKey" to result))
    }

    Java


    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Bundle result = new Bundle();
            result.putString("bundleKey", "result");
            getParentFragmentManager().setFragmentResult("requestKey", result);
        }
    });

    Получение данных


    Для получения данных через FragmentManager мы регистрируем наш FragmentResultListener и задаем ключ по которому мы будем получать данные. Тот самый ключ который мы указывали в методе FragmentManager.setFragmentResult()


    Kotlin


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
         // Здесь так же используется Kotlin экстеншен 
        setFragmentResultListener("requestKey") { key, bundle ->
            // Здесь можно передать любой тип, поддерживаемый Bundle-ом
            val result = bundle.getString("bundleKey")
        }
    }

    Java


    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getParentFragmentManager().setFragmentResultListener("key", this, new FragmentResultListener() {
            @Override
            public void onFragmentResult(@NonNull String key, @NonNull Bundle bundle) {
                String result = bundle.getString("bundleKey");
            }
        });
    }

    Здесь мы видим 2 аргумента: key: String и bundle: Bundle.
    Первый — это тот самый ключ, по которому мы передаем сюда данные. Второй — Bundle, в котором лежат переданные данные.


    Parent Fragment Manger


    image

    Выбор FragmentManager-а для передачи данных между фрагментами зависит от принимающего фрагмента:


    • Если оба фрагмента находятся в одном и том же FragmentManager (например оба фрагмента находятся в Activity), то мы должны использовать родительский FragmentManager, который хранит в себе Activity
    • Если у нас один фрагмент вложен в другой фрагмент, то для передачи данных мы используем childFragmentManager (он же родительский фрагмент для принимающего фрагмента)

    Важно понимать, что наш FragmentResultListener должен находиться в общем для двух фрагментов FragmentManager-е.


    Тестирование


    Для тестирования отправки/получения данных через FragmentResultListener, мы можем использовать FragmentScenario API, который предоставляет нам все преимущества тестирования фрагментов в изоляции.


    Передача данных


    Как мы можем протестировать, что наш фрагмент корректно отправляет данные через родительский FragmentManager? Для этого нам необходимо внутри теста отправить результат и проверить, что наш FragmentResultListener получил корректные данные:


    @Test
    fun testFragmentResult() {
        val scenario = launchFragmentInContainer<ResultFragment>()
        lateinit var actualResult: String?
        scenario.onFragment { fragment ->
            fragment.parentFragmentManagager.setResultListener("requestKey") { key, bundle ->
                actualResult = bundle.getString("bundleKey")
            }
        }
        onView(withId(R.id.result_button)).perform(click())
        assertThat(actualResult).isEqualTo("result")
    }
    
    class ResultFragment : Fragment(R.layout.fragment_result) {
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            view.findViewById(R.id.result_button).setOnClickListener {
                val result = "result"
                setResult("requestKey", bundleOf("bundleKey" to result))
            }
        }
    }

    Получение данных


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


    @Test
    fun testFragmentResultListener() {
        val scenario = launchFragmentInContainer<ResultListenerFragment>()
        scenario.onFragment { fragment ->
            val expectedResult = "result"
            fragment.parentFragmentManagager.setResult("requestKey", bundleOf("bundleKey" to expectedResult))
            assertThat(fragment.result).isEqualTo(expectedResult)
        }
    }
    
    class ResultListenerFragment : Fragment() {
        var result : String? = null
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setResultListener("requestKey") { key, bundle ->
                result = bundle.getString("bundleKey")
            }
        }
    }

    Вывод


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


    Для того чтоб получить возможность использовать FragmentResultListener нам нужно подключить в зависимостях версию фрагментов 1.3.0-alpha04 или новее:


    • Версия для Java: androidx.fragment:fragment:1.3.0-alpha04
    • Версия для Kotlin: androidx.fragment:fragment-ktx:1.3.0-alpha04
    • Тесты: androidx.fragment:fragment-testing:1.3.0-alpha04
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 4

      +2

      Как Андроид-разработчик, я иногда поражаюсь, насколько Гугл любит переусложнять и переизобретать вещи.

        +1
        Переизобретать любит. А переусложнения то в о чем? Обычный как по мне observer.
          0
          Ну, например, в том, что передачей-получением результатов у нас по каким-то причинам заведует FragmentManager. По идее, это не должно быть его заботой, это должно быть заботой отдельного класса, который занимается только получением-передачей этих результатов. Диаграмма с тем, какой FragmentManager нужно использовать, чтобы правильно получить результаты — это весело, конечно. Захотел использовать Fragment внутри другого Fragment'а в одном месте, а в другом внутри Activity? Пиши if-else на выбор FragmentManager'а.
          Короче говоря, обычный observer становится совсем необычным.

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

          Понятно, конечно, что все это сделано максимально адски в связи с жизненным циклом Fragment/Activity и возможностью убийства процесса, а, значит, нужно сохранять состояние и его восстанавливать, но выглядит это чересчур неуклюже и громоздко.
        0

        Only users with full accounts can post comments. Log in, please.