Para reutilizar fragmentos, compílalos como componentes completamente independientes que definan su propio diseño y comportamiento. Una vez que hayas definido esos fragmentos reutilizables, podrás asociarlos con una actividad y conectarlos a la lógica de la aplicación para comprender la IU completa de la composición.
Para reaccionar de manera adecuada ante los eventos del usuario o compartir información de estado, a menudo, necesitas tener canales de comunicación entre una actividad y sus fragmentos, o entre dos o más fragmentos. Para que los fragmentos sigan siendo independientes, no hagas que los fragmentos se comuniquen directamente con otros fragmentos ni con su actividad de host.
La biblioteca de Fragment
proporciona dos opciones para la comunicación: un ViewModel
compartido y la API de resultados de fragmentos. La opción recomendada depende del caso de uso. Para compartir datos persistentes con las APIs personalizadas, usa un ViewModel
. A fin de obtener un resultado único con los datos que se pueden colocar en un Bundle
, debes usar la API de resultados de fragmentos.
En las siguientes secciones, se muestra cómo usar ViewModel
y la API de Fragment Result a efectos de establecer una comunicación entre tus fragmentos y actividades.
Cómo compartir datos mediante un ViewModel
ViewModel
es una opción ideal cuando necesitas compartir datos entre varios fragmentos o entre fragmentos y su actividad del host.
Los objetos de ViewModel
almacenan y administran datos de IU. Para obtener más información sobre ViewModel
, consulta la sección Descripción general de ViewModel.
Cómo compartir datos con la actividad del host
En algunos casos, quizás necesites compartir datos entre fragmentos y su actividad del host. Por ejemplo, es posible que quieras activar o desactivar un componente global de IU basado en una interacción dentro de un fragmento.
Ten en cuenta el siguiente 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; } }
En este ejemplo, los datos almacenados se unen en una clase MutableLiveData
.
LiveData
es una clase de contenedor de datos observables optimizada para ciclos de vida. MutableLiveData
permite cambiar su valor. Para obtener más información sobre LiveData
, consulta la Descripción general de LiveData.
Tanto el fragmento como su actividad de host pueden recuperar una instancia compartida de un ViewModel
que tenga alcance en la actividad pasando la actividad al constructor ViewModelProvider
. ViewModelProvider
controla la creación de la instancia de ViewModel
o su recuperación si aquella ya existe. Ambos componentes pueden observar y modificar estos datos.
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); }); } }
Cómo compartir los datos entre fragmentos
A menudo, dos o más fragmentos de la misma actividad necesitan comunicarse entre sí. Por ejemplo, imagina un fragmento que muestra una lista y otro que permite al usuario aplicar varios filtros a esa lista. La implementación de este caso no es trivial si los fragmentos no se comunican directamente, pero ya no son independientes. Además, los dos fragmentos deben procesar la situación en la que el otro fragmento todavía no se haya creado o no esté visible.
Esos fragmentos pueden compartir un ViewModel
mediante su alcance de actividad para administrar la comunicación. Cuando se comparte ViewModel
de esta manera, los fragmentos no necesitan conocer información acerca del otro, y la actividad no necesita hacer nada para facilitar la comunicación.
En el siguiente ejemplo, se muestra cómo dos fragmentos pueden usar un ViewModel
compartido para la comunicación:
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); } }
Ten en cuenta que ambos fragmentos usan su actividad de host como alcance del ViewModelProvider
. Debido a que los fragmentos usan el mismo alcance, reciben la misma instancia del ViewModel
, lo que les permite comunicarse entre sí.
Cómo compartir datos entre un fragmento superior y uno secundario
Cuando se trabaja con fragmentos secundarios, es posible que el fragmento superior y los secundarios necesiten compartir datos entre sí. Para compartir datos entre estos fragmentos, usa el fragmento superior como alcance de ViewModel
, como se muestra en el siguiente ejemplo:
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); ... } }
Cómo definir el alcance de un ViewModel según el gráfico de Navigation
Si usas la biblioteca de Navigation, también puedes determinar el alcance de ViewModel
en relación con el ciclo de vida de la NavBackStackEntry
de destino. Por ejemplo, el alcance de ViewModel
podría ser la NavBackStackEntry
del 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. } } }
Si quieres obtener más información para determinar el alcance de un ViewModel
en relación con una NavBackStackEntry
, consulta Cómo interactuar de manera programática con el componente Navigation.
Cómo obtener resultados con la API de resultados de fragmentos
En algunos casos, es posible que quieras pasar un valor por única vez entre dos fragmentos o entre un fragmento y su actividad de host. Por ejemplo, puedes tener un fragmento que lee códigos QR y pasa los datos a un fragmento anterior.
En la versión 1.3.0 de Fragment y versiones posteriores, cada FragmentManager
implementa FragmentResultOwner
.
Eso significa que un FragmentManager
puede actuar como un almacenamiento central para los resultados de fragmentos. Este cambio permite que los componentes se comuniquen entre sí configurando los resultados de fragmentos y escuchando esos resultados sin que esos componentes tengan referencias directas entre sí.
Cómo pasar resultados entre fragmentos
Para pasar datos al fragmento A desde el fragmento B, primero configura un objeto de escucha de resultados en el fragmento A (es decir, el que recibe el resultado). Llama a setFragmentResultListener()
en el FragmentManager
del fragmento A, como se muestra en el siguiente ejemplo:
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. } }); }
En el fragmento B, que produce el resultado, debes establecer el resultado en el mismo FragmentManager
con la misma requestKey
. Puedes hacerlo con la API de 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); } });
Luego, el fragmento A recibe el resultado y ejecuta la devolución de llamada del objeto de escucha una vez que el fragmento está STARTED
.
Únicamente puedes tener un solo objeto de escucha y resultado para una clave determinada. Si llamas a setFragmentResult()
más de una vez para la misma clave y si el objeto de escucha no es STARTED
, el sistema reemplazará los resultados pendientes con el resultado actualizado.
Si configuras un resultado sin un objeto de escucha correspondiente para recibirlo, el resultado se almacenará en el FragmentManager
hasta que establezcas un objeto de escucha con la misma clave. Una vez que un objeto de escucha reciba un resultado y active la devolución de llamada onFragmentResult()
, se borrará el resultado. Este comportamiento tiene dos implicaciones importantes:
- Los fragmentos sobre la pila de actividades no reciben resultados hasta que se resaltan y son
STARTED
. - Si un fragmento que escucha un resultado es
STARTED
cuando se establece el resultado, se activa de inmediato la devolución de llamada del objeto de escucha.
Cómo probar los resultados de los fragmentos
Usa FragmentScenario
para hacer llamadas de prueba a setFragmentResult()
y setFragmentResultListener()
.
Crea un escenario para el fragmento en prueba usando launchFragmentInContainer
o launchFragment
, y luego llama de forma manual al método que no se está probando.
A fin de probar setFragmentResultListener()
, crea una situación con el fragmento que hace la llamada a setFragmentResultListener()
. Luego, llama a setFragmentResult()
de forma directa y verifica el resultado:
@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")
}
}
}
Para probar setFragmentResult()
, cree una situación con el fragmento que hace la llamada a setFragmentResult()
. A continuación, llama a setFragmentResultListener()
de forma directa y verifica el resultado:
@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))
}
}
}
Cómo pasar resultados entre fragmentos superiores y secundarios
Para pasar un resultado de un fragmento secundario a uno superior, usa getChildFragmentManager()
del fragmento superior en lugar de getParentFragmentManager()
cuando llames a 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. } }); }
El fragmento secundario establece el resultado en su FragmentManager
. Luego, el superior recibe el resultado una vez que el fragmento está 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); } });
Cómo recibir los resultados en la actividad del host
Para recibir un resultado de fragmento en la actividad del host, configura un objeto de escucha de resultados en el administrador de fragmentos mediante 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. } }); } }