Cómo comunicarse con fragmentos

Si deseas volver a usar fragmentos, debes crear cada uno como un componente completamente independiente que defina 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.

A fin de reaccionar de manera adecuada ante los eventos del usuario o para 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 debes hacer 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 cualquier API personalizada, debes usar 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 resultados de fragmentos a los efectos de establecer una comunicación entre tus fragmentos y actividades.

Cómo compartir datos con 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 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 que se almacenan están unidos en una clase MutableLiveData. LiveData es una clase de contenedor de datos observable optimizada para ciclos de vida. MutableLiveData permite que se cambie 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 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. Es posible que, en ese caso, la implementación no sea trivial si los fragmentos no se comunican directamente, lo que significaría que esos elementos 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 hacerlo, usa el fragmento superior como alcance de 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);
        ...
    }
}

Cómo determinar 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. A partir de Fragment 1.3.0-alpha04, 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
        }
    });
}
El fragmento b envía datos al fragmento a por medio de un FragmentManager
Figura 1: El fragmento B envía datos al fragmento A por medio de un FragmentManager

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 resalten y sean 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, este último debe usar getChildFragmentManager() en lugar de getParentFragmentManager() cuando se llama a setFragmentResultListener().

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // We 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);
    // We 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 fragmento secundario puede usar FragmentManager para enviar un resultado a su superior
Figura 2: Un fragmento secundario puede usar FragmentManager para enviar un resultado a su superior

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