1. Antes de comenzar
Los dispositivos Android pueden tener diferentes formas y tamaños, por lo que debes adaptar el diseño de tu app a los diferentes tamaños de pantalla a fin de que esté disponible para diferentes usuarios y dispositivos con un solo paquete de Android (APK) o Android App Bundle (AAB). Para ello, debes definir tu app con un diseño responsivo y adaptable, en lugar de hacerlo con dimensiones estáticas para un tamaño de pantalla y una relación de aspecto determinados. Los diseños adaptables cambian en función del espacio de pantalla disponible.
En este codelab, aprenderás los conceptos básicos para compilar IU adaptables y modificar una app que muestre una lista de deportes y detalles de cada deporte para admitir dispositivos de pantalla grande. La app de Deportes consta de tres pantallas: principal, favoritos y configuración. En la pantalla principal, se mostrará una lista de deportes y un marcador de posición de noticias cuando seleccionas un deporte de la lista. Las pantallas de configuración y de favoritos también muestran texto de marcadores de posición. Selecciona el elemento asociado en el menú de navegación inferior para cambiar de pantalla.
La app comienza con estos problemas de diseño en pantallas grandes:
- No puedes usarla en orientación vertical.
- Se muestran muchos espacios en blanco en pantallas grandes.
- Siempre se muestra el menú de navegación inferior en pantallas grandes.
Debes hacer que tu app sea adaptable para que suceda lo siguiente:
- Admita las orientaciones horizontal y vertical.
- Muestre la lista de deportes y noticias sobre cada deporte al lado cuando hay suficiente espacio horizontal para hacerlo.
- Muestre el componente de navegación según los lineamientos de Material Design.
La app es una app de una sola actividad con varios fragmentos. Tú trabajas con los siguientes archivos:
- El archivo
AndroidManifest.xml
, que proporciona los metadatos sobre la app de Deportes. - El archivo
MainActivity.kt
, que contiene un código generado con el archivoactivity_main.xml
, la anotaciónoverride
, una claseenum
que representa los tamaños de clase del ancho de la ventana y una definición del método para recuperar la clase de tamaño del ancho para la ventana de la app. El menú de navegación inferior se inicializa cuando se crea la actividad. - El archivo
activity_main.xml
, que define el diseño predeterminado de la actividadMain
. - El archivo
layout-sw600dp/activity_main.xml
, que define un diseño alternativo para la actividadMain
. El diseño alternativo es eficaz cuando el ancho de la ventana de la app es mayor o igual que el valor600dp
. El contenido es el mismo que el diseño predeterminado del punto de partida. - El archivo
SportsListFragment.kt
, que contiene la implementación de la lista de deportes y la navegación hacia atrás personalizada. - El archivo
fragment_sports_list.xml
, que define un diseño de la lista de deportes. - El archivo
navigation_menu.xml
, que define los elementos del menú de navegación inferior.
Figura 1: La app de Deportes admite diferentes tamaños de ventana con un solo APK o AAB
Requisitos previos
- Tener conocimientos básicos sobre el desarrollo de IU basada en vistas
- Tener experiencia con la sintaxis de Kotlin, incluidas las funciones lambda
- Haber completado el codelab de los principios básicos de Jetpack Compose
Qué aprenderás
- Cómo admitir cambios de configuración
- Cómo agregar diseños alternativos con menos modificaciones de código
- Cómo implementar una IU de detalles de lista que se comporte de manera diferente en distintos tamaños de ventana
Qué compilarás
Una app para Android que admita lo siguiente:
- Orientación horizontal del dispositivo
- Tablets, computadoras de escritorio y dispositivos móviles
- Comportamiento de los detalles de lista para diferentes tamaños de pantalla
Requisitos
- Android Studio Bumblebee | 2021.1.1 o una versión posterior
- Una tablet o un emulador de Android
2. Prepárate
Descarga el código de este codelab y configura el proyecto:
- Desde tu línea de comandos, clona el código de este repositorio de GitHub:
$ git clone https://github.com/android/add-adaptive-layouts
- En Android Studio, abre el proyecto
AddingAdaptiveLayout
. El proyecto se compila en varias ramas de git:
- La rama
main
contiene el código de partida para este proyecto. Realiza cambios en esta rama para completar el codelab. - La rama
end
contiene la solución para este codelab.
3. Cómo migrar el componente de navegación superior a Compose
La app de Deportes usa un menú de navegación inferior como componente principal de navegación. Este componente de navegación se implementa con la clase BottomNavigationView
. En esta sección, migrarás el componente de navegación superior a Compose.
Representa el contenido del recurso de menú asociado como una clase sellada
- Crea una clase
MenuItem
sellada para representar el contenido del archivonavigation_menu.xml
y, luego, pásale un parámetroiconId
, un parámetrolabelId
y un parámetrodestinationId
. - Agrega un objeto
Home
, un objetoFavorite
y un objetoSettings
como subclases que correspondan a tus destinos.
MenuItem.kt
sealed class MenuItem(
// Resource ID of the icon for the menu item
@DrawableRes val iconId: Int,
// Resource ID of the label text for the menu item
@StringRes val labelId: Int,
// ID of a destination to navigate users
@IdRes val destinationId: Int
) {
object Home: MenuItem(
R.drawable.ic_baseline_home_24,
R.string.home,
R.id.SportsListFragment
)
object Favorites: MenuItem(
R.drawable.ic_baseline_favorite_24,
R.string.favorites,
R.id.FavoritesFragment
)
object Settings: MenuItem(
R.drawable.ic_baseline_settings_24,
R.string.settings,
R.id.SettingsFragment
)
}
Implementa el menú de navegación inferior como una función de componibilidad
- Define una función de componibilidad
BottomNavigationBar
que tome estos tres parámetros: un objetomenuItems
configurado en un valorList<MenuItem>
, un objetomodifier
configurado en un valorModifier = Modifier
y un objetoonMenuSelected
configurado en una función lambda(MenuItem) -> Unit = {}
. - En el cuerpo de la función de componibilidad
BottomNavigationBar
, llama a una funciónNavigationBar()
que toma un parámetromodifier
. - En la función lambda pasada a la función
NavigationBar()
, llama al métodoforEach()
en el parámetromenuItems
y, luego, llama a una funciónNavigationBarItem()
en la función lambda configurada en la llamada de métodoforeach()
- Pasa la función
NavigationBarItem()
a un parámetroselected
configurado en un valorfalse
; un parámetroonClick
configurado en una función lambda que contiene una funciónonMenuSelected
con un parámetroMenuItem
; un parámetroicon
configurado en una función lambda que contiene una funciónIcon
que toma un parámetropainter = painterResource(id = menuItem.iconId)
y un parámetrocontentDescription = null
; y un parámetrolabel
configurado en una función lambda que contiene una funciónText
que toma un parámetro(text = stringResource(id = menuItem.labelId)
.
Navigation.kt
@Composable
fun BottomNavigationBar(
menuItems: List<MenuItem>,
modifier: Modifier = Modifier,
onMenuSelected: (MenuItem) -> Unit = {}
) {
NavigationBar(modifier = modifier) {
menuItems.forEach { menuItem ->
NavigationBarItem(
selected = false,
onClick = { onMenuSelected(menuItem) },
icon = {
Icon(
painter = painterResource(id = menuItem.iconId),
contentDescription = null)
},
label = { Text(text = stringResource(id = menuItem.labelId))}
)
}
}
}
Reemplaza el elemento BottomNavigationView
por el elemento ComposeView
en el archivo de recursos de diseño
- En el archivo
activity_main.xml
, reemplaza el elementoBottomNavigationView
por el elementoComposeView
. De esta manera, se incorpora el componente de navegación inferior en un diseño de IU basado en vistas con Compose.
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/top_navigation"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/top_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Integra el menú de navegación inferior basado en Compose con un diseño de IU basado en vistas
- En el archivo
MainActivity.kt
, define una variablenavigationMenuItems
configurada en una lista de objetosMenuItem
. Los objetosMenuItems
aparecen en el menú de navegación inferior en el orden de la lista. - Llama a la función
BottomNavigationBar()
para incorporar el menú de navegación inferior en el objetoComposeView
. - Navega al destino asociado con el elemento seleccionado por los usuarios en la función de devolución de llamada que se pasa a la función
BottomNavigationBar
.
MainActivity.kt
val navigationMenuItems = listOf(
MenuItem.Home,
MenuItem.Favorites,
MenuItem.Settings
)
binding.navigation.setContent {
MaterialTheme {
BottomNavigationBar(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
}
}
4. Admite la orientación horizontal
Si tu app admite un dispositivo de pantalla grande, se espera que admita la orientación horizontal y la vertical. En este momento, tu app solo tiene una actividad: la actividad MainActivity
.
La orientación de la pantalla de la actividad en el dispositivo se establece en el archivo AndroidManifest.xml
con el atributo android:screenOrientation
, que está configurado en un valor portrait
.
Haz que la app admita la orientación horizontal de la siguiente manera:
- Configura el atributo
android:screenOrientation
en un valorfullUser
. Con esta configuración, los usuarios pueden bloquear la orientación de su pantalla. El sensor de orientación del dispositivo determina cualquiera de las cuatro orientaciones posibles.
AndroidManifest.xml
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="fullUser">
Figura 2: La app se ejecuta en orientación horizontal después de actualizar el archivo AndroidManifest.xml
5. Clases de tamaño de ventana
Estos son valores de punto de interrupción que ayudan a clasificar el tamaño de la ventana en clases de tamaño predefinidas (compactas, medianas y expandidas) con el tamaño sin procesar de la ventana disponible para tu app. Usarás estas clases de tamaño cuando diseñes, desarrolles y pruebes tu diseño adaptable.
El ancho y las alturas disponibles se particionan de forma independiente para que tu app siempre esté asociada con dos clases de tamaño de ventana: una clase de ancho y otra de alto.
Figura 3: Clases de ancho de ventana y puntos de interrupción asociados
Figura 4: Las clases de tamaño de ventana de alto y sus puntos de interrupción asociados
Las clases de tamaño de ventana representan el tamaño actual de la ventana de tu app. En otras palabras, no puedes determinar la clase de tamaño de ventana según el tamaño del dispositivo físico. Incluso si tu app se ejecuta en el mismo dispositivo, la clase de tamaño de ventana asociada cambia según la configuración, por ejemplo, cuando ejecutas tu app en el modo de pantalla dividida. Esto tiene dos consecuencias importantes:
- Los dispositivos físicos no garantizan una clase específica del tamaño de la ventana.
- La clase de tamaño de ventana puede cambiar durante el ciclo de vida de la app.
Después de agregar diseños adaptables a tu app, debes probarla en todos los tamaños de ventanas, en especial, en clases de tamaño de ventana compacta, mediana y expandida. Las pruebas de cada clase de tamaño de ventana son necesarias, pero en muchos casos no son suficientes. Es importante que pruebes tu app en una variedad de tamaños de ventana para asegurarte de que la IU se ajuste correctamente. Para obtener más información, consulta Diseños de pantalla grande y Calidad de las apps en pantallas grandes.
6. Coloca los paneles de lista y de detalles uno al lado de otro en pantallas grandes
Es posible que una IU de detalles de lista deba comportarse de manera diferente según la clase de tamaño de ventana actual del ancho. Cuando la clase de tamaño de ventana mediana o expandida se asocia con tu app, significa que esta puede tener suficiente espacio para mostrar el panel de lista y el panel de detalles uno al lado de otro, de modo que los usuarios puedan ver la lista de elementos y los detalles del elemento seleccionado sin transición de pantalla. Sin embargo, esto puede verse muy ajustado en pantallas más pequeñas, en las que puede ser mejor mostrar un panel a la vez, con el panel de lista en primer lugar. En el panel de detalles, se muestran los detalles del elemento seleccionado cuando los usuarios presionan un elemento de la lista. La clase SlidingPaneLayout
administra la lógica para determinar cuál de estas dos experiencias del usuario es apropiada para el tamaño actual de la ventana.
Configura el diseño del panel de lista
La clase SlidingPaneLayout
es un componente de la IU basado en vistas. Debes modificar el archivo de recursos de diseño para el panel de lista, que es el archivo fragment_sports_list.xml
de este codelab.
La clase SlidingPaneLayout
toma dos elementos secundarios. Los atributos de ancho y ponderación de cada elemento secundario son los factores clave de la clase SlidingPaneLayout
para determinar si la ventana es lo suficientemente grande para mostrar ambas vistas una al lado de la otra. De lo contrario, se reemplazará la lista de pantalla completa por la IU de detalles de pantalla completa. Los valores de ponderación se refieren al tamaño de los dos paneles de forma proporcional cuando el tamaño de la ventana es mayor que el requisito mínimo para mostrar los paneles uno al lado del otro.
Se aplicó la clase SlidingPaneLayout
al archivo fragment_sports_list.xml
. El panel de lista está configurado para tener un ancho de 1280dp
. Este es el motivo por el que el panel de lista y el de detalles no aparecen uno al lado del otro.
Haz que los paneles se muestren uno al lado del otro cuando la pantalla tenga un ancho mayor que 580dp
:
- Configura el
RecyclerView
en un ancho de280dp
.
fragment_sports_list.xml
<androidx.slidingpanelayout.widget.SlidingPaneLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SportsListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clipToPadding="false"
android:padding="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<androidx.fragment.app.FragmentContainerView
android:layout_height="match_parent"
android:layout_width="300dp"
android:layout_weight="1"
android:id="@+id/detail_container"
android:name="com.example.android.sports.NewsDetailsFragment"/>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
Figura 5: Los paneles de lista y de detalles aparecen uno al lado del otro después de actualizar el archivo de recursos de diseño
Intercambia el panel de detalles
Ahora, los paneles de lista y de detalles aparecen uno al lado del otro cuando la clase de tamaño de ventana de ancho medio o expandido está asociada con tu app. Sin embargo, la pantalla pasa por completo al panel de detalles cuando los usuarios seleccionan un elemento en el panel de lista.
Figura 6: La pantalla pasa al panel de detalles después de seleccionar un deporte de la lista
Este problema se debe a que la navegación se activa cuando los usuarios seleccionan un elemento del panel de lista. El código relevante está disponible en el archivo SportsListFragment.kt
.
SportsListFragment.kt
val adapter = SportsAdapter {
sportsViewModel.updateCurrentSport(it)
// Navigate to the details pane.
val action =
SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
}
Asegúrate de que la pantalla solo complete la transición al panel de detalles cuando no haya suficiente espacio para mostrar los paneles de lista y de detalles uno al lado del otro:
- En la variable de función
adapter
, agrega una sentenciaif
que verifique si el atributoisSlidable
de la claseSlidingPaneLayout
es verdadero y si el atributoisOpen
de la claseSlidingPaneLayout
es falso.
SportsListFragment.kt
val adapter = SportsAdapter {
sportsViewModel.updateCurrentSport(it)
if(slidingPaneLayout.isSlidable && !slidingPaneLayout.isOpen){
// Navigate to the details pane.
val action =
SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
}
}
7. Elige el componente de navegación correcto por clase de tamaño de ancho de ventana
Material Design espera que tu app elija los componentes de manera adaptable. En esta sección, elegirás un componente de navegación para la barra de navegación superior según la clase de ancho de ventana actual. En esta tabla, se describe el componente de navegación esperado para cada clase de tamaño de ventana:
Clase tamaño de ancho de ventana | Componente de navegación |
Compacta | Navegación inferior |
Media | Riel de navegación |
Expandida | Panel lateral de navegación permanente |
Implementa el riel de navegación
- Crea una función de componibilidad
NavRail()
que tome tres parámetros: un objetomenuItems
configurado en un valorList<MenuItem>
, un objetomodifier
configurado en un valorModifier
y una función lambdaonMenuSelected
. - En el cuerpo de la función, llama a una función
NavigationRail()
que tome el objetomodifier
como parámetro. - Llama a una función
NavigationRailItem()
para cada objetoMenuItem
en el objetomenuItems
como lo hiciste con la funciónNavigationBarItem
en la funciónBottomNavigationBar()
.
Implementa el panel lateral de navegación permanente
- Crea una función de componibilidad
NavigationDrawer()
que tome estos tres parámetros: un objetomenuItems
configurado en un valorList<MenuItem>
, un objetomodifier
configurado en un valorModifier
y una función lambdaonMenuSelected
. - En el cuerpo de la función, llama a una función
Column()
que tome el objetomodifier
como parámetro. - Llama a una función
Row()
para cada objetoMenuItem
en el objetomenuItems
. - En el cuerpo de la función
Row()
, agrega las etiquetasicon
ytext
como lo hiciste con la funciónNavigationBarItem
en la funciónBottomNavigationBar()
.
Selecciona el componente de navegación correcto por clase de tamaño de ancho ventana
- Para recuperar la clase de ancho de ventana actual, llama a la función
rememberWidthSizeClass()
dentro de la función de componibilidad pasada al métodosetContent()
del objetoComposeView
. - Crea una rama condicional para elegir el componente de navegación basado en la clase de tamaño de ventana recuperada y llama a la seleccionada.
- Pasa un objeto
Modifier
a la funciónNavigationDrawer
para especificar su ancho como un valor256dp
.
ActivityMain.kt
binding.navigation.setContent {
MaterialTheme {
when(rememberWidthSizeClass()){
WidthSizeClass.COMPACT ->
BottomNavigationBar(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
WidthSizeClass.MEDIUM ->
NavRail(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
WidthSizeClass.EXPANDED ->
NavigationDrawer(
menuItems = navigationMenuItems,
modifier = Modifier.width(256.dp)
) { menuItem ->
navController.navigate(screen.destinationId)
}
}
}
}
Coloca el componente de navegación en la posición correcta
Ahora, tu app puede elegir el componente de navegación correcto en función de la clase de ancho de ventana actual, pero los componentes seleccionados no se pueden colocar como desearías. Esto se debe a que el elemento ComposeView
se coloca debajo del elemento FragmentViewContainer
.
Actualiza el recurso de diseño alternativo para la clase MainActivity
:
- Abre el archivo
layout-sw600dp/activity_main.xml
. - Actualiza la restricción para distribuir los elementos
ComposeView
yFragmentContainer
de forma horizontal.
layout-sw600dp/activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintLeft_toRightOf="@+id/top_navigation"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:navGraph="@navigation/nav_graph" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/top_navigation"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/top_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Después de la modificación, la app elige el componente de navegación correcto en función de la clase de tamaño de ancho de ventana asociada. La primera captura de pantalla muestra la pantalla de la clase de tamaño de ancho de ventana media. La segunda captura de pantalla muestra la pantalla para la clase de tamaño de ancho de ventana expandida.
Figura 7: Pantallas para las clases de tamaño de ancho de ventana media y expandida
8. Felicitaciones
¡Felicitaciones! Completaste este codelab y aprendiste a agregar diseños adaptables a una app para Android basada en View con Compose. Al hacerlo, aprendiste sobre la clase SlidingPaneLayout
, las clases de tamaño de ventana y la selección de componentes de navegación basada en clases de ancho de ventana.