Communiquer avec des fragments

Pour réutiliser des fragments, créez-les en tant que composants entièrement autonomes définissant leur propre mise en page et leur propre comportement. Une fois que vous avez défini ces fragments réutilisables, vous pouvez les associer à une activité et les connecter à la logique d'application pour créer l'interface utilisateur globale.

Pour réagir correctement aux événements utilisateur et partager des informations d'état, des canaux de communication entre une activité et ses fragments, ou entre deux fragments ou plus, sont souvent nécessaires. Pour que les fragments restent autonomes, ils ne doivent pas communiquer directement avec les autres fragments ni avec leur activité hôte.

La bibliothèque Fragment fournit deux options de communication : un ViewModel partagé et l'API Fragment Result. L'option recommandée dépend du cas d'utilisation. Pour partager des données persistantes avec des API personnalisées, utilisez un ViewModel. Pour obtenir un résultat unique comportant des données pouvant être placées dans un Bundle, utilisez l'API Fragment Result.

Les sections suivantes expliquent comment utiliser ViewModel et l'API Fragment Result pour communiquer entre vos fragments et vos activités.

Partager des données à l'aide d'un ViewModel

ViewModel est un choix idéal lorsque vous devez partager des données entre plusieurs fragments ou entre des fragments et leur activité hôte. Les objets ViewModel stockent et gèrent les données d'interface utilisateur. Pour en savoir plus sur ViewModel, consultez la section Présentation de ViewModel.

Partager des données avec l'activité de l'hôte

Dans certains cas, vous devrez peut-être partager des données entre les fragments et leur activité hôte. Imaginons que vous souhaitiez activer ou désactiver un composant d'interface utilisateur global en fonction d'une interaction au sein d'un fragment.

Examinez l'élément ItemViewModel suivant :

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

Dans cet exemple, les données stockées sont encapsulées dans une classe MutableLiveData. LiveData est une classe de conteneur de données observable compatible avec le cycle de vie. MutableLiveData permet la modification de sa valeur. Pour en savoir plus sur LiveData, consultez la présentation de LiveData.

Votre fragment et son activité hôte peuvent récupérer une instance partagée d'un ViewModel avec un champ d'application d'activité en transmettant l'activité au constructeur ViewModelProvider. ViewModelProvider gère l'instanciation de ViewModel ou sa récupération s'il existe déjà. Les deux composants peuvent observer et modifier ces données.

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

Partager des données entre fragments

Deux fragments d'une même activité ou plus doivent souvent communiquer entre eux. Imaginez, par exemple, un fragment qui affiche une liste et un autre permettant à l'utilisateur d'appliquer divers filtres à cette liste. La mise en œuvre de ce scénario n'est pas simple si les fragments ne communiquent pas directement, auquel cas ils ne seront plus autonomes. En outre, les deux fragments doivent gérer le scénario dans lequel l'autre fragment n'est pas encore créé ni visible.

Ces fragments peuvent partager un ViewModel avec le champ d'application de leur activité pour gérer cette communication. En partageant le ViewModel de cette manière, les fragments n'ont pas besoin de se connaître, et l'activité n'a rien à faire pour permettre la communication.

L'exemple suivant montre comment deux fragments peuvent communiquer à l'aide d'un ViewModel partagé :

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

Les deux fragments utilisent leur activité hôte comme champ d'application pour ViewModelProvider. Comme les fragments reposent sur le même champ d'application, ils reçoivent la même instance de ViewModel, ce qui leur permet de communiquer entre eux.

Partager des données entre un fragment parent et un fragment enfant

Lorsque vous utilisez des fragments enfants, le fragment parent et ses fragments enfants peuvent avoir besoin de partager des données les uns avec les autres. Pour partager des données entre ces fragments, utilisez le fragment parent en tant que champ d'application du fichier ViewModel, comme illustré dans l'exemple suivant :

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

Appliquer un ViewModel au graphique de navigation

Si vous utilisez la bibliothèque de navigation, vous pouvez également appliquer un ViewModel au cycle de vie de l'élément NavBackStackEntry d'une destination. Par exemple, le champ d'application du ViewModel peut être limité à l'élément NavBackStackEntry de 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.
        }
    }
}

Pour en savoir plus sur l'application d'un champ d'application ViewModel à un élément NavBackStackEntry, consultez la section Interagir de façon programmatique avec le composant Navigation.

Obtenir des résultats à l'aide de l'API Fragment Result

Dans certains cas, vous pouvez transmettre une valeur ponctuelle entre deux fragments ou entre un fragment et son activité hôte. Par exemple, vous pouvez avoir un fragment qui lit des codes QR en transmettant les données à un fragment précédent.

Dans les versions 1.3.0 et ultérieures de l'API Fragment, chaque FragmentManager implémente FragmentResultOwner. Autrement dit, un FragmentManager peut agir en tant que stockage central pour les résultats de fragment. Cette modification permet aux composants de communiquer entre eux en définissant des résultats de fragment et en écoutant ces résultats sans que ces composants ne renvoient directement les uns aux autres.

Transmettre des résultats entre fragments

Pour renvoyer des données au fragment A à partir du fragment B, commencez par définir un écouteur de résultats au niveau du fragment A (fragment qui recevra le résultat). Appelez setFragmentResultListener() au niveau de la fonction FragmentManager du fragment A, comme illustré dans l'exemple suivant :

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.
        }
    });
}
Le fragment B envoie des données au fragment A à l&#39;aide d&#39;un FragmentManager.
Figure 1 : Le fragment B envoie des données au fragment A à l'aide d'un FragmentManager.

Dans le fragment B, c'est-à-dire le fragment qui génère le résultat, définissez le résultat sur le même FragmentManager en utilisant le même élément requestKey. Pour ce faire, utilisez l'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);
    }
});

Le fragment A reçoit ensuite le résultat et exécute le rappel de l'écouteur une fois que le fragment est lancé (STARTED).

Il ne peut y avoir qu'un seul écouteur et un seul résultat pour une clé donnée. Si vous appelez setFragmentResult() plusieurs fois pour la même clé et que l'écouteur n'est pas lancé (STARTED), le système remplace tous les résultats en attente par votre résultat mis à jour.

Si vous spécifiez un résultat sans qu'un écouteur correspondant ne soit prêt à le recevoir, il sera stocké dans le FragmentManager jusqu'à ce que vous définissiez un écouteur avec la même clé. Une fois qu'un écouteur reçoit un résultat et déclenche le rappel onFragmentResult(), le résultat est effacé. Ce comportement a deux conséquences majeures :

  • Les fragments de la pile "Retour" ne reçoivent pas de résultats tant qu'ils n'ont pas été transmis et qu'ils ne sont pas lancés (STARTED).
  • Si un fragment qui écoute un résultat est lancé (STARTED) lorsque le résultat est défini, le rappel de l'écouteur est déclenché immédiatement.

Tester les résultats du fragment

Utilisez FragmentScenario pour tester les appels à setFragmentResult() et setFragmentResultListener(). Créez un scénario pour le fragment testé en utilisant launchFragmentInContainer ou launchFragment, puis appelez manuellement la méthode qui n'est pas actuellement testée.

Pour tester setFragmentResultListener(), créez un scénario avec le fragment qui appelle setFragmentResultListener(). Appelez ensuite setFragmentResult() directement et vérifiez le résultat :

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

Pour tester setFragmentResult(), créez un scénario avec le fragment qui appelle setFragmentResult(). Appelez ensuite setFragmentResultListener() directement et vérifiez le résultat :

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

Transmettre les résultats entre les fragments parents et enfants

Pour transmettre un résultat d'un fragment enfant à un parent, utilisez getChildFragmentManager() à partir du fragment parent au lieu de getParentFragmentManager() lors de l'appel de 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.
            }
        });
}
Un fragment enfant peut utiliser FragmentManager pour envoyer un résultat à son parent.
Figure 2 : Un fragment enfant peut utiliser FragmentManager pour envoyer un résultat à son parent.

Le fragment enfant définit le résultat sur son FragmentManager. Le parent reçoit ensuite le résultat une fois que le fragment est lancé (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);
    }
});

Recevoir les résultats dans l'activité de l'hôte

Pour recevoir un résultat de fragment dans l'activité de l'hôte, définissez un écouteur de résultat au niveau du gestionnaire de fragments à l'aide de 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.
            }
        });
    }
}