Общаться с фрагментами,Общаться с фрагментами

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

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

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

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

Делитесь данными с помощью ViewModel

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

Делитесь данными с хостом

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

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

Котлин

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

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

Ява

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 или его получение, если оно уже существует. Оба компонента могут наблюдать и изменять эти данные.

Котлин

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)
    }
}

Ява

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 для взаимодействия:

Котлин

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)
}

Ява

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 , как показано в следующем примере:

Котлин

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()})
    ...
}

Ява

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 навигационным графом

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

Котлин

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.
        }
    }
}

Ява

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 см. в разделе Программное взаимодействие с компонентом навигации .

Получите результаты с помощью API результатов фрагмента

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

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

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

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

Котлин

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.
    }
}

Ява

@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() :

Котлин

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact.
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

Ява

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() .

Котлин

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.
    }
}

Ява

@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 фрагмента:

Котлин

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact.
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

Ява

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);
    }
});

Получение результатов в активности хоста

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

Котлин

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.
        }
    }
}

Ява

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.
            }
        });
    }
}