Обмен данными с фрагментами

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

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

Библиотека Fragment предоставляет два варианта взаимодействия: общая ViewModel и Fragment Result API. Рекомендуемый вариант зависит от конкретного случая. Чтобы передавать постоянные данные с помощью пользовательских API, используйте ViewModel. Если вам нужен однократный результат с данными, которые можно поместить в Bundle, используйте Fragment Result API.

В следующих разделах показано, как использовать ViewModel и Fragment Result API для обмена данными между фрагментами и действиями.

Как передавать данные с помощью ViewModel

ViewModel – идеальный выбор, когда вам нужно обмениваться данными между несколькими фрагментами или между фрагментами и их хост-активностью. Объекты ViewModel хранят данные пользовательского интерфейса и управляют ими. Подробнее ViewModel о ViewModel…

Как передавать данные в основное действие

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

Рассмотрим следующий пример (ItemViewModel):

Kotlin

class ItemViewModel : ViewModel() {
    private val mutableSelectedItem = MutableLiveData<Item>()
    val selectedItem: LiveData<Item> get() = mutableSelectedItem

    fun selectItem(item: Item) {
        mutableSelectedItem.value = item
    }
}

Java

public class ItemViewModel extends ViewModel {
    private final MutableLiveData<Item> selectedItem = new MutableLiveData<Item>();
    public void selectItem(Item item) {
        selectedItem.setValue(item);
    }
    public LiveData<Item> getSelectedItem() {
        return selectedItem;
    }
}

В этом примере сохраненные данные упакованы в класс MutableLiveData. LiveData – это класс наблюдаемых данных, учитывающий жизненный цикл. MutableLiveData позволяет изменять свое значение. Подробнее LiveData рассказывается в обзоре LiveData.

И фрагмент, и его основное действие могут получить общий экземпляр ViewModel с областью действия, передав действие в конструктор ViewModelProvider. ViewModelProvider создает экземпляр ViewModel или извлекает его, если он уже существует. Оба компонента могут наблюдать и изменять эти данные.

Kotlin

class MainActivity : AppCompatActivity() {
    // Using the viewModels() Kotlin property delegate from the activity-ktx
    // artifact to retrieve the ViewModel in the activity scope.
    private val viewModel: ItemViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.selectedItem.observe(this, Observer { item ->
            // Perform an action with the latest item data.
        })
    }
}

class ListFragment : Fragment() {
    // Using the activityViewModels() Kotlin property delegate from the
    // fragment-ktx artifact to retrieve the ViewModel in the activity scope.
    private val viewModel: ItemViewModel by activityViewModels()

    // Called when the item is clicked.
    fun onItemClicked(item: Item) {
        // Set a new item.
        viewModel.selectItem(item)
    }
}

Java

public class MainActivity extends AppCompatActivity {
    private ItemViewModel viewModel;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewModel = new ViewModelProvider(this).get(ItemViewModel.class);
        viewModel.getSelectedItem().observe(this, item -> {
            // Perform an action with the latest item data.
        });
    }
}

public class ListFragment extends Fragment {
    private ItemViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(ItemViewModel.class);
        ...
        items.setOnClickListener(item -> {
            // Set a new item.
            viewModel.select(item);
        });
    }
}

Обмен данными между фрагментами

Двум или более фрагментам в одном действии часто требуется взаимодействовать друг с другом. Например, один фрагмент может показывать список, а другой – позволять пользователю применять к нему различные фильтры. Реализовать этот случай непросто без прямой связи между фрагментами, но тогда они перестанут быть автономными. Кроме того, оба фрагмента должны обрабатывать сценарий, в котором другой фрагмент ещё не создан или не виден.

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

В следующем примере показано, как два фрагмента могут использовать общий объект ViewModel для взаимодействия:

Kotlin

class ListViewModel : ViewModel() {
    val filters = MutableLiveData<Set<Filter>>()

    private val originalList: LiveData<List<Item>>() = ...
    val filteredList: LiveData<List<Item>> = ...

    fun addFilter(filter: Filter) { ... }

    fun removeFilter(filter: Filter) { ... }
}

class ListFragment : Fragment() {
    // Using the activityViewModels() Kotlin property delegate from the
    // fragment-ktx artifact to retrieve the ViewModel in the activity scope.
    private val viewModel: ListViewModel by activityViewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.filteredList.observe(viewLifecycleOwner, Observer { list ->
            // Update the list UI.
        }
    }
}

class FilterFragment : Fragment() {
    private val viewModel: ListViewModel by activityViewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.filters.observe(viewLifecycleOwner, Observer { set ->
            // Update the selected filters UI.
        }
    }

    fun onFilterSelected(filter: Filter) = viewModel.addFilter(filter)

    fun onFilterDeselected(filter: Filter) = viewModel.removeFilter(filter)
}

Java

public class ListViewModel extends ViewModel {
    private final MutableLiveData<Set<Filter>> filters = new MutableLiveData<>();

    private final LiveData<List<Item>> originalList = ...;
    private final LiveData<List<Item>> filteredList = ...;

    public LiveData<List<Item>> getFilteredList() {
        return filteredList;
    }

    public LiveData<Set<Filter>> getFilters() {
        return filters;
    }

    public void addFilter(Filter filter) { ... }

    public void removeFilter(Filter filter) { ... }
}

public class ListFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
        viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
            // Update the list UI.
        });
    }
}

public class FilterFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
        viewModel.getFilters().observe(getViewLifecycleOwner(), set -> {
            // Update the selected filters UI.
        });
    }

    public void onFilterSelected(Filter filter) {
        viewModel.addFilter(filter);
    }

    public void onFilterDeselected(Filter filter) {
        viewModel.removeFilter(filter);
    }
}

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

Обмен данными между родительским и дочерним фрагментами

При работе с дочерними фрагментами вашему родительскому фрагменту и его дочерним фрагментам может потребоваться обмениваться данными друг с другом. Чтобы передавать данные между этими фрагментами, используйте родительский фрагмент в качестве области действия ViewModel, как показано в следующем примере:

Kotlin

class ListFragment: Fragment() {
    // Using the viewModels() Kotlin property delegate from the fragment-ktx
    // artifact to retrieve the ViewModel.
    private val viewModel: ListViewModel by viewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.filteredList.observe(viewLifecycleOwner, Observer { list ->
            // Update the list UI.
        }
    }
}

class ChildFragment: Fragment() {
    // Using the viewModels() Kotlin property delegate from the fragment-ktx
    // artifact to retrieve the ViewModel using the parent fragment's scope
    private val viewModel: ListViewModel by viewModels({requireParentFragment()})
    ...
}

Java

public class ListFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(this).get(ListViewModel.class);
        viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
            // Update the list UI.
        }
    }
}

public class ChildFragment extends Fragment {
    private ListViewModel viewModel;
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(requireParentFragment()).get(ListViewModel.class);
        ...
    }
}

Как ограничить область действия ViewModel графом навигации

Если вы используете библиотеку Navigation, вы также можете ограничить область действия ViewModel жизненным циклом NavBackStackEntry пункта назначения. Например, ViewModel можно ограничить NavBackStackEntry для ListFragment:

Kotlin

class ListFragment: Fragment() {
    // Using the navGraphViewModels() Kotlin property delegate from the fragment-ktx
    // artifact to retrieve the ViewModel using the NavBackStackEntry scope.
    // R.id.list_fragment == the destination id of the ListFragment destination
    private val viewModel: ListViewModel by navGraphViewModels(R.id.list_fragment)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.filteredList.observe(viewLifecycleOwner, Observer { item ->
            // Update the list UI.
        }
    }
}

Java

public class ListFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
    NavController navController = NavHostFragment.findNavController(this);
        NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.list_fragment)

        viewModel = new ViewModelProvider(backStackEntry).get(ListViewModel.class);
        viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
            // Update the list UI.
        }
    }
}

Дополнительную информацию о том, как ограничить область действия ViewModel до NavBackStackEntry, можно найти в статье Программное взаимодействие с компонентом навигации.

Как получить результаты с помощью Fragment Result API

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

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

Передача результатов между фрагментами

Чтобы передать данные обратно во фрагмент A из фрагмента B, сначала установите прослушиватель результатов во фрагменте A, который получает результат. Вызовите setFragmentResultListener() для FragmentManager фрагмента A, как показано в примере ниже.

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Use the Kotlin extension in the fragment-ktx artifact.
    setFragmentResultListener("requestKey") { requestKey, bundle ->
        // We use a String here, but any type that can be put in a Bundle is supported.
        val result = bundle.getString("bundleKey")
        // Do something with the result.
    }
}

Java

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getParentFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
        @Override
        public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
            // We use a String here, but any type that can be put in a Bundle is supported.
            String result = bundle.getString("bundleKey");
            // Do something with the result.
        }
    });
}
Фрагмент B отправляет данные во фрагмент A с помощью FragmentManager.
Рисунок 1. Фрагмент B отправляет данные фрагменту A с помощью FragmentManager.

Во фрагменте B, который возвращает результат, задайте результат в том же FragmentManager, используя тот же requestKey. Для этого используйте API setFragmentResult().

Kotlin

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact.
    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);
    }
});

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

Для каждого ключа можно использовать только один прослушиватель и результат. Если вы вызываете setFragmentResult() несколько раз для одного и того же ключа и прослушиватель не равен STARTED, система заменяет все ожидающие результаты обновленным результатом.

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

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

Результаты тестирования фрагментов

Используйте FragmentScenario, чтобы протестировать вызовы setFragmentResult() и setFragmentResultListener(). Создайте сценарий для тестируемого фрагмента с помощью launchFragmentInContainer или launchFragment, а затем вручную вызовите метод, который не тестируется.

Чтобы протестировать setFragmentResultListener(), создайте сценарий с фрагментом, который вызывает setFragmentResultListener(). Затем вызовите setFragmentResult() напрямую и проверьте результат:

@Test
fun testFragmentResultListener() {
    val scenario = launchFragmentInContainer<ResultListenerFragment>()
    scenario.onFragment { fragment ->
        val expectedResult = "result"
        fragment.parentFragmentManager.setFragmentResult("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)
        // Use the Kotlin extension in the fragment-ktx artifact.
        setFragmentResultListener("requestKey") { requestKey, bundle ->
            result = bundle.getString("bundleKey")
        }
    }
}

Чтобы протестировать setFragmentResult(), создайте сценарий с фрагментом, который вызывает setFragmentResult(). Затем вызовите setFragmentResultListener() напрямую и проверьте результат:

@Test
fun testFragmentResult() {
    val scenario = launchFragmentInContainer<ResultFragment>()
    lateinit var actualResult: String?
    scenario.onFragment { fragment ->
        fragment.parentFragmentManager
                .setFragmentResultListener("requestKey") { requestKey, 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"
            // Use the Kotlin extension in the fragment-ktx artifact.
            setFragmentResult("requestKey", bundleOf("bundleKey" to result))
        }
    }
}

Передача результатов между родительскими и дочерними фрагментами

Чтобы передать результат из дочернего фрагмента в родительский, используйте getChildFragmentManager() из родительского фрагмента вместо getParentFragmentManager() при вызове setFragmentResultListener().

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Set the listener on the child fragmentManager.
    childFragmentManager.setFragmentResultListener("requestKey") { key, bundle ->
        val result = bundle.getString("bundleKey")
        // Do something with the result.
    }
}

Java

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Set the listener on the child fragmentManager.
    getChildFragmentManager()
        .setFragmentResultListener("requestKey", this, new FragmentResultListener() {
            @Override
            public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
                String result = bundle.getString("bundleKey");
                // Do something with the result.
            }
        });
}
дочерний фрагмент может использовать FragmentManager для отправки результата
            своему родителю
Рисунок 2. Дочерний фрагмент может использовать FragmentManager, чтобы отправить результат родительскому фрагменту.

Дочерний фрагмент задает результат в своем FragmentManager. Родительский элемент получает результат, когда фрагмент STARTED:

Kotlin

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact.
    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");
        // The child fragment needs to still set the result on its parent fragment manager.
        getParentFragmentManager().setFragmentResult("requestKey", result);
    }
});

Получение результатов в основном действии

Чтобы получить результат фрагмента в объекте activity, настройте прослушиватель результатов в диспетчере фрагментов с помощью getSupportFragmentManager().

Kotlin

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportFragmentManager
                .setFragmentResultListener("requestKey", this) { requestKey, bundle ->
            // We use a String here, but any type that can be put in a Bundle is supported.
            val result = bundle.getString("bundleKey")
            // Do something with the result.
        }
    }
}

Java

class MainActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
            @Override
            public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
                // We use a String here, but any type that can be put in a Bundle is supported.
                String result = bundle.getString("bundleKey");
                // Do something with the result.
            }
        });
    }
}