En el desarrollo de Android, la capacidad de crear interfaces de usuario modulares y reutilizables siempre ha sido un pilar fundamental. Durante muchos años, los Fragments fueron la solución principal para este propósito. Nos permitían verlos como un "pedazo de interfaz" independiente, con su propio ciclo de vida, que podíamos reutilizar en diferentes actividades. Sin embargo, el ecosistema de Android ha evolucionado significativamente, y con la llegada de Jetpack Compose, el paradigma ha cambiado por completo.
Este artículo, originalmente escrito en 2013, explicaba cómo anidar Fragments para resolver problemas de persistencia de componentes en la UI. Hoy, actualizamos ese conocimiento para mostrar cómo Jetpack Compose no solo simplifica este desafío, sino que lo resuelve de una manera más elegante e intuitiva, haciendo que el enfoque de Fragments sea considerado Legacy (heredado).
El Problema Original: Un Componente Persistente
Recordemos el escenario original: una aplicación con un reproductor de audio. La meta es que este reproductor permanezca visible y funcional mientras el usuario navega por distintas pantallas de la aplicación. Si el usuario está escuchando una canción, esta no debería detenerse ni reiniciarse al cambiar de pantalla.

En el antiguo enfoque basado en Vistas y Fragments, esto era complejo. Se requería que el componente del reproductor estuviera en la Actividad principal y se gestionara cuidadosamente el ciclo de vida de los Fragments para no destruir el estado del reproductor. La comunicación entre el reproductor y los diferentes fragments era verbosa y propensa a errores.
La Solución Moderna: Jetpack Compose y State Hoisting
Jetpack Compose es un moderno kit de herramientas declarativo para construir UI nativa. En lugar de manipular vistas a través de XML y código imperativo, simplemente describes cómo debería ser tu UI en un momento dado, y Compose se encarga de actualizarla cuando los datos cambian.
La unidad fundamental en Compose es una función anotada con @Composable. Estas funciones son, por definición, modulares y reutilizables. El concepto de "anidar" componentes de UI es intrínseco a Compose.
Para resolver nuestro problema del reproductor, usamos un patrón llamado state hoisting (elevación de estado). En lugar de que cada pantalla (o fragment, en el mundo antiguo) gestione su propio estado, "elevamos" el estado importante a un ancestro común. En arquitecturas modernas, este ancestro es típicamente un ViewModel.
// Primero, definimos el estado de nuestro reproductor
data class PlayerUiState(
val currentSong: String? = null,
val isPlaying: Boolean = false
)
// Luego, un ViewModel para gestionar ese estado
class PlayerViewModel : ViewModel() {
private val _uiState = MutableStateFlow(PlayerUiState())
val uiState: StateFlow<PlayerUiState> = _uiState.asStateFlow()
fun onPlayPause() {
_uiState.update { it.copy(isPlaying = !it.isPlaying) }
}
fun onSongSelected(song: String) {
_uiState.update { it.copy(currentSong = song, isPlaying = true) }
}
}
Construyendo la UI Modular con Composables
Con el estado centralizado, nuestra UI se convierte en un reflejo de ese estado. La estructura principal de la aplicación ya no necesita FrameLayouts para intercambiar Fragments. En su lugar, usamos un Scaffold y un NavHost de Navigation Compose.
La clave es colocar el Composable del reproductor fuera del NavHost. De esta forma, el reproductor no se ve afectado por la navegación entre pantallas.
@Composable
fun MainScreen(playerViewModel: PlayerViewModel = viewModel()) {
// Observamos el estado del ViewModel
val playerState by playerViewModel.uiState.collectAsState()
val navController = rememberNavController()
Scaffold(
bottomBar = {
// El reproductor se coloca en la barra inferior y solo se muestra si hay una canción
if (playerState.currentSong != null) {
PlayerUI(
songName = playerState.currentSong!!,
isPlaying = playerState.isPlaying,
onPlayPause = { playerViewModel.onPlayPause() }
)
}
}
) { paddingValues ->
// El NavHost gestiona las "pantallas" que se muestran en el área de contenido
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(paddingValues)
) {
composable("home") {
HomeScreen(
onSongClicked = { song ->
playerViewModel.onSongSelected(song)
}
)
}
composable("profile") {
ProfileScreen()
}
// ... otras pantallas
}
}
}
El PlayerUI es un Composable simple que solo se encarga de mostrar la información y notificar los eventos de clic.
@Composable
fun PlayerUI(songName: String, isPlaying: Boolean, onPlayPause: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primaryContainer)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = songName, modifier = Modifier.weight(1f))
IconButton(onClick = onPlayPause) {
Icon(
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
contentDescription = "Play/Pause"
)
}
}
}
De RecyclerView a LazyColumn
El artículo original usaba un RecyclerView para mostrar listas. En Compose, el equivalente moderno y mucho más sencillo es LazyColumn. Para nuestra pantalla principal (HomeScreen), podemos mostrar una lista de canciones. Al hacer clic en una, simplemente llamamos a la función del ViewModel.
@Composable
fun HomeScreen(onSongClicked: (String) -> Unit) {
val songs = listOf("Canción 1", "Canción 2", "Canción 3", "Canción 4")
LazyColumn(contentPadding = PaddingValues(16.dp)) {
items(songs) { song ->
Text(
text = song,
modifier = Modifier
.fillMaxWidth()
.clickable { onSongClicked(song) }
.padding(16.dp)
)
}
}
}Como puedes ver, no hay adaptadores, ni ViewHolders, ni FragmentTransactions. El clic en un elemento de la lista invoca directamente la lógica en el ViewModel, el estado se actualiza, y la UI (tanto HomeScreen como PlayerUI) "reacciona" a ese cambio automáticamente.
Conclusión: La Sencillez de lo Declarativo
El enfoque ha cambiado radicalmente. Hemos pasado de gestionar manualmente transacciones de Fragments y ciclos de vida complejos a simplemente declarar cómo debe ser nuestra UI en función de un estado centralizado.
Aunque la tecnología de Fragments sigue existiendo y es necesaria para interoperar con vistas antiguas o en proyectos grandes en migración (el enfoque Legacy), para cualquier desarrollo nuevo en Android, Jetpack Compose es la forma recomendada y moderna de construir interfaces. La modularidad ya no es algo que se logra a través de un componente como Fragment, sino una característica inherente al propio sistema de diseño de UI.
Fragments la forma - XML (Legacy)
Te dejo por aquí, la forma legacy que fue sustituida por lo comentado inicialmente en el articulo.
Los Fragment eran Android son un elemento fundamental en Android que nos permite reutilizar componentes en Android con relativa facilidad; los fragments podemos verlo como un pedazo de interfaz independiente de otros fragments y por lo tanto son muy recomendables cuando queramos desarrollar en Android.
Lo que puede que tal vez no sea tan claro es que dentro de los fragments podemos crear o embeber otros fragments llevando esto a un segundo nivel y aunque en primera instancia puede sonar a locura esto en realidad puede solucionar algunos problemas muy puntuales que se nos puedan plantear.
Supongamos que tenemos algún componente que está atado a nuestra actividad la cual es a primera instancia la que incrusta nuestros fragments, supongamos que dicho componente atado a nuestra actividad tiene que estar visible en toda la aplicación y que obviamente la aplicación tiene muchas pantallas como las pantallas presentadas en la siguiente imagen:
Cómo puede darse cuenta una de esas pantallas corresponde a múltiples fragments dentro de ella y según la dinámica del usuario va a ir pasando de una a otra pantalla a gusto propio.
Vamos a ponerle nombre a nuestro componente de ejemplo, supongamos que el componente es un reproductor de audio:

El cual es un componente que podemos embeber dentro dentro de nuestra actividad, la cual si es destruida o recreada el reproductor se verá afectado por la misma situación cosa que no es beneficiosa, ya que la idea sería que el usuario siga escuchando la misma música sin necesidad de que esta se detenga si el mismo decide pasar de una pantalla a otra en la aplicación y aquí es una buena oportunidad para emplear los fragments que incluyen otros fragments; por ejemplo el fragment 1 que llamaremos Container1Fragment incluye otros fragments como podemos ver a continuación:
View v = inflater.inflate(R.layout.fragment_container1, container, false);
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.flGeneros, new RecyclerViewGenerosFragmentView()).commit();
Bundle bundle = new Bundle();
bundle.putInt("fragment_id", 1);
RecyclerViewArtistasFragmentView recyclerViewArtistasFragmentView = new RecyclerViewArtistasFragmentView();
recyclerViewArtistasFragmentView.setArguments(bundle);
fragmentManager = getActivity().getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.flArtistas, recyclerViewArtistasFragmentView).commit();
bundle = new Bundle();
bundle.putInt("fragment_id", 2);
RecyclerViewArtistasFragmentView recyclerViewArtistasFragmentViewTop = new RecyclerViewArtistasFragmentView();
recyclerViewArtistasFragmentViewTop.setArguments(bundle);
fragmentManager = getActivity().getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.flArtistasTop, recyclerViewArtistasFragmentViewTop).commit();
bundle = new Bundle();
bundle.putInt("id", 13);
bundle.putString("nombre", "");
bundle.putInt("tipo", 3);
bundle.putString("imagen", "");
RecyclerViewCancionesFragmentView recyclerViewCancionesFragmentViewLista = new RecyclerViewCancionesFragmentView();
recyclerViewCancionesFragmentViewLista.setArguments(bundle);
fragmentManager = getActivity().getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.flCancionesLista, recyclerViewCancionesFragmentViewLista).commit();No nos preocupemos mucho por los parámetros del Bundle, el objetivo es captar la idea la cual es embeber un fragment dentro de otro fragment; la vista para este fragment es la siguiente:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/llContainer1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#EEEEEE"
android:orientation="vertical">
<TextView
style="@style/TextViewClasic"
android:text="@string/tgeneros" />
<FrameLayout
android:id="@+id/flGeneros"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"></FrameLayout>
<TextView
style="@style/TextViewClasic"
android:text="@string/tartistas" />
<FrameLayout
android:id="@+id/flArtistas"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"></FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#CCCCCC"></LinearLayout>
<TextView
style="@style/TextViewClasic"
android:text="@string/ttop" />
<FrameLayout
android:id="@+id/flArtistasTop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"></FrameLayout>
<TextView
style="@style/TextViewClasic"
android:text="@string/cttop" />
<FrameLayout
android:id="@+id/flCancionesLista"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"></FrameLayout>
</LinearLayout>Como vemos, tenemos algunas etiquetas de guía y lo principal que son los tags FrameLayout para incluir los fragments mostrados anteriormente desde el código java.
Ahora veamos el contenido de uno de los fragments que incluiremos en Container1Fragment:
public class RecyclerViewCancionesFragmentView extends Fragment {
...
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
id = getArguments().getInt("id");
tipo = getArguments().getInt("tipo");
imagen = getArguments().getString("imagen");
nombre = getArguments().getString("nombre");
descripcion = getArguments().getString("descripcion");
View v;
v = inflater.inflate(R.layout.fragment_canciones, container, false);
rvData = (RecyclerView) v.findViewById(R.id.rvData);
tvIdentificador = (TextView) v.findViewById(R.id.tvIdentificador);
init();
return v;
}
...
}Cómo podemos darnos cuenta, está últimos fragments o último nivel (segundo nivel) de fragment, es el que realmente empleamos para interactuar con el usuario, en específico (o para este ejemplo) creamos un listado a través del RecycleView pero puede hacer cualquier otra cosa como leer un código QR, un webview etc.
En nuestra actividad principal que es la que incluirá nuestro componente (por ejemplo el reproductor) entre varias cosas, hacemos lo siguiente:
...
FragmentManager fragmentManager = getSupportFragmentManager();
container1Fragment = new Container1Fragment();
fragmentManager.beginTransaction().replace(R.id.frameLayout, container1Fragment).commit();
...Aunque aquí podemos incluir todos los fragments que queramos, en nuestro caso para no complicar mucho el experimento y la idea general de esta entrada sólo incluimos uno que es nuestro Container1Fragment.
La vista o layout de nuestra actividad es la siguiente:
<LinearLayout
android:id="@+id/llParent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:background="#EEEEEE"
android:orientation="vertical"
android:padding="0dp">
<FrameLayout
android:id="@+id/frameLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="200px"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:orientation="vertical"></FrameLayout>
<LinearLayout
android:id="@+id/llContainerR"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
</LinearLayout>Ahora, para variar el contenido de cada fragment (primer nivel) o sustituir algunos según la acción ejercida por el usuario para este experimento para nuestro caso empleamos los Adapters, es decir, nuestro experimento está compuesto de puros listados y cuando el usuario realiza un clic (toca uno de los elementos del listado) se ejecuta un evento el cual actualiza toda la pantalla (reemplaza el contenido de Container1Fragment por ejemplo) muestra otro fragment o conjunto de fragments embebidos o cualquier otra cosa que queramos dependiendo de nuestras necesidades; en este ejemplo, el evento clic reemplaza el contenido del fragment principal es decir Container1Fragment:
...
artistaViewHolder.llContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Bundle bundle = new Bundle();
bundle.putInt("id", artista.getArtista_id());
bundle.putInt("album_id", 0);
bundle.putInt("tipo", 1);
bundle.putString("imagen", artista.getImagen_url());
bundle.putString("nombre", artista.getNombre());
Container2Fragment container2Fragment = new Container2Fragment();
container2Fragment.setArguments(bundle);
FragmentManager fragmentManager = ((FragmentActivity) activity).getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.frameLayout, container2Fragment).addToBackStack(NULL).commit();
}
});
...El resto del código del adaptador es el tradicional (inicialización de las variables y layouts y poco más); como podemos darnos cuenta, a través del método clic del adaptador reemplazamos el Container1Fragment con Container2Fragment, lo que a efectos es reemplazar todo el contenido de una ventana con otro contenido.
Por lo tanto solo mantenemos una sola actividad pero podemos variar las pantallas de las mismas fácilmente con los fragments que embebe la actividad y con los fragments embebidos por otros fragments y de esta forma garantizamos que nuestra actividad no será destruida mientras el usuario navega por la aplicación y tampoco nuestro componente o reproductor de música según sea el caso.