Cómo cambiar el tamaño de una app para Android

1. Introducción

El ecosistema de dispositivos Android está en constante evolución. Desde los primeros días de los teclados de hardware integrados hasta el panorama moderno de los dispositivos plegables, las tablets y las ventanas de formato libre a las que se les puede cambiar el tamaño, las apps de Android nunca se han ejecutado en un conjunto de dispositivos tan diverso como en la actualidad.

Si bien las noticias son excelentes para los desarrolladores, se deben implementar algunas optimizaciones en las apps para cumplir con las expectativas de usabilidad y ofrecer una excelente experiencia del usuario en todos los tamaños de pantalla diferentes. En lugar de orientarse a cada nuevo dispositivo de uno en uno, una interfaz de usuario responsiva/adaptable y una arquitectura resiliente pueden ayudar a que tu app se vea y funcione bien, independientemente del tamaño y la forma de los dispositivos de tus usuarios actuales y futuros.

La introducción de entornos redimensionables de formato libre en Android es una excelente manera de poner a prueba tu interfaz de usuario responsiva/adaptable para prepararla para cualquier dispositivo. Este codelab te guiará para que comprendas las implicaciones del cambio de tamaño, así como la implementación de algunas de las prácticas recomendadas para hacer que una app cambie de tamaño de manera sólida y sencilla.

Qué compilarás

Explorarás las implicaciones de cambiar el tamaño de formato libre y optimizarás una app para Android con el objetivo de demostrar las prácticas recomendadas para el cambio de tamaño. Tu app hará lo siguiente:

Tendrá un manifiesto compatible

  • Elimina restricciones que impiden que la app cambie de tamaño libremente.

Mantendrá el estado al cambiar de tamaño

  • Mantiene el estado de la IU al cambiar de tamaño con rememberSaveable.
  • Evita duplicar innecesariamente el trabajo en segundo plano para inicializar la IU.

Qué necesitarás

  1. Saber cómo crear aplicaciones básicas para Android
  2. Saber implementar ViewModel y State en Compose
  3. Un dispositivo de prueba que admita el cambio de tamaño de ventana de formato libre, como uno de los siguientes:

Si, a medida que avanzas con este codelab encuentras algún problema (errores de código, errores gramaticales, texto poco claro, etc.), infórmalo a través del vínculo Informa un error que se encuentra en la esquina inferior izquierda del codelab.

2. Cómo comenzar

Clona el repositorio desde GitHub

git clone https://github.com/android/large-screen-codelabs/

… o descarga un archivo ZIP del repositorio y extráelo.

Cómo importar el proyecto

  • Abre Android Studio.
  • Selecciona Import Project o File > New > Import Project.
  • Navega hasta donde clonaste o extrajiste el proyecto.
  • Abre la carpeta resizing.
  • Abre el proyecto en la carpeta start. Esta carpeta contiene el código de partida.

Cómo probar la app

  • Compila y ejecuta la app.
  • Cambia el tamaño de la app.

¿Qué piensas?

Según la asistencia de compatibilidad de tu dispositivo de prueba, probablemente hayas notado que la experiencia de usuario no es la ideal. No se puede cambiar el tamaño de la app y se queda atascada en la relación de aspecto inicial. ¿Qué sucede?

Restricciones del manifiesto

Si revisas el archivo AndroidManifest.xml de la app, verás que hay algunas restricciones agregadas que impiden que nuestra app se comporte correctamente en un entorno de cambio de tamaño de ventanas de formato libre.

AndroidManifest.xml

            android:maxAspectRatio="1.4"
            android:resizeableActivity="false"
            android:screenOrientation="portrait">

Intenta quitar estas tres líneas problemáticas de tu manifiesto, compila la app de nuevo y vuelve a intentarlo en tu dispositivo de prueba. Verás que la app ya no tiene restricciones para cambiar el tamaño de formato libre. Quitar este tipo de restricciones del manifiesto es un paso importante en la optimización de la app para cambiar el tamaño de las ventanas de formato libre.

3. Cambios en la configuración al cambiar el tamaño

Cuando la ventana de tu app cambia de tamaño, se actualiza la Configuración. Estas actualizaciones tienen implicaciones para tu app. Entenderlas y anticiparte a ellas puede ayudarte a ofrecerles a los usuarios una gran experiencia. Los cambios más obvios son el ancho y la altura de la ventana de la app, pero estos cambios también afectan la relación de aspecto y la orientación.

Cómo observar cambios en la configuración

Para ver cómo se producen estos cambios en una app creada con el sistema de vistas de Android, puedes anular View.onConfigurationChanged. En Jetpack Compose, tenemos acceso a LocalConfiguration.current, que se actualiza automáticamente cuando se llama a View.onConfigurationChanged.

Para ver estos cambios de configuración en tu app de ejemplo, agrega un elemento componible a tu app que muestre valores de LocalConfiguration.current, o bien crea un nuevo proyecto de ejemplo con un componible de este tipo. Un ejemplo de IU para ver estos cambios sería algo como lo siguiente:

val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
    Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
        when (configuration.screenLayout and
                Configuration.SCREENLAYOUT_SIZE_MASK) {
            SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
            SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
            SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
            SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
            else -> "undefined value"
        }
Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
) {
    Text("screenWidthDp: ${configuration.screenWidthDp}")
    Text("screenHeightDp: ${configuration.screenHeightDp}")
    Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
    Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
    Text("screenLayout SIZE: $screenLayoutSize")
}

Puedes ver un ejemplo de implementación en la carpeta del proyecto observing-configuration-changes. Intenta agregar esto a la IU de tu app, ejecútala en tu dispositivo de prueba y observa cómo se actualiza la IU a medida que cambia la configuración de la app.

a medida que cambia el tamaño de la app, la información de configuración cambiante se muestra en la interfaz de la app en tiempo real

Estos cambios en la configuración de tu app te permiten simular rápidamente que pasas de los extremos que esperaríamos con una pantalla dividida en un teléfono celular pequeño a la pantalla completa en una tablet o computadora. No solo es una buena forma de probar el diseño de la app en distintas pantallas, sino que también permite comprobar la capacidad de la app para procesar cambios rápidos de configuración.

4. Cómo registrar eventos de ciclo de vida de Activity

Otra implicación del cambio de tamaño de ventanas de formato libre para tu app son los diferentes cambios en el ciclo de vida de Activity que se producirán en tu app. Para ver estos cambios en tiempo real, agrega un observador de ciclo de vida a tu método onCreate y anula onStateChanged para registrar cada nuevo evento de ciclo de vida.

lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("resizing-codelab-lifecycle", "$event was called")
    }
})

Con este acceso en su lugar, ejecuta nuevamente tu app en el dispositivo de prueba y mira logcat mientras intentas minimizar la app y traerla a primer plano de nuevo.

Observa que tu app queda pausada cuando la minimizas y se reanuda cuando la traes a primer plano. Esto tiene implicaciones para tu app que explorarás en la próxima sección de este codelab centrada en la continuidad.

logcat que muestra los métodos del ciclo de vida de Activity que se invocan al cambiar de tamaño

Ahora mira en Logcat para ver qué devoluciones de llamadas del ciclo de vida de la actividad se invocan cuando redimensionas tu app desde su tamaño más pequeño posible hasta el más grande

Según el dispositivo de prueba, puedes observar diferentes comportamientos, pero probablemente hayas notado que tu actividad se destruye y se vuelve a crear cuando el tamaño de la ventana de tu app cambia significativamente, pero no cuando apenas cambia. Esto se debe a que, a partir del nivel 24 de la API, solo los cambios de tamaño significativos dan lugar a la recreación de Activity.

Ya has visto algunos de los cambios de configuración habituales que puedes esperar en un entorno de renderización en ventanas de formato libre, pero hay otros cambios que debes tener en cuenta. Por ejemplo, si tienes un monitor externo conectado a tu dispositivo de prueba,, puedes ver que la Activity se destruye y se vuelve a crear para considerar los cambios de configuración, como la densidad de la pantalla.

Para abstraer parte de la complejidad asociada a los cambios de configuración, usa una API de nivel superior, como WindowSizeClass, para implementar tu IU adaptable. (Consulta también Cómo brindar compatibilidad con diferentes tamaños de pantalla).

5. Continuidad: cómo mantener el estado interno de los elementos componibles al cambiar el tamaño

En la sección anterior, viste algunos de los cambios de configuración que tu app puede esperar en un entorno de cambio de tamaño de ventanas de formato libre. En esta sección, mantendrás el estado de la interfaz de usuario de tu app de forma continua a lo largo de esos cambios.

Comienza por hacer que la función de componibilidad NavigationDrawerHeader (que se encuentra en ReplyHomeScreen.kt) se expanda para mostrar la dirección de correo electrónico al hacer clic.

@Composable
private fun NavigationDrawerHeader(
    modifier: Modifier = Modifier
) {
    var showDetails by remember { mutableStateOf(false) }
    Column(
        modifier = modifier.clickable {
                showDetails = !showDetails
            }
    ) {


        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            ReplyLogo(
                modifier = Modifier
                    .size(dimensionResource(R.dimen.reply_logo_size))
            )
            ReplyProfileImage(
                drawableResource = LocalAccountsDataProvider
                    .userAccount.avatar,
                description = stringResource(id = R.string.profile),
                modifier = Modifier
                    .size(dimensionResource(R.dimen.profile_image_size))
            )
        }
        AnimatedVisibility (showDetails) {
            Text(
                text = stringResource(id = LocalAccountsDataProvider
                        .userAccount.email),
                style = MaterialTheme.typography.labelMedium,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(
                            R.dimen.drawer_padding_header),
                        end = dimensionResource(
                            R.dimen.drawer_padding_header),
                        bottom = dimensionResource(
                            R.dimen.drawer_padding_header)
                ),


            )
        }
    }
}

Cuando le hayas agregado el encabezado desplegable a tu app:

  1. Ejecútala en el dispositivo de prueba.
  2. Presiona el encabezado para expandirlo.
  3. Intenta cambiar el tamaño de la ventana.

Verás que el encabezado pierde su estado cuando le cambias el tamaño significativamente.

El encabezado del panel lateral de navegación de la app se expande al presionarlo, pero se contrae al cambiar el tamaño de la app

El estado de la IU se pierde debido a que remember te ayuda a retener el estado a través de recomposiciones, pero no a través de la recreación de actividades o procesos. Es habitual usar la elevación de estado, moviendo el estado al llamador de un elemento componible para hacer componibles sin estado, lo que puede evitar este problema por completo. Dicho esto, puedes utilizar remember en algunos casos para mantener el estado de los elementos de la IU dentro de las funciones de componibilidad.

Para resolver estos problemas, reemplaza remember por rememberSaveable. Esto funciona porque rememberSaveable ahorra y restablece el valor recordado a savedInstanceState. Cambia remember a rememberSaveable, ejecuta tu app en el dispositivo de prueba y vuelve a intentar cambiar el tamaño de la app. Observarás que el estado del encabezado desplegable se mantiene durante todo el proceso de cambio de tamaño, tal y como estaba previsto.

6. Cómo evitar la duplicación innecesaria del trabajo en segundo plano

Ya viste cómo utilizar rememberSaveable para conservar el estado interno de la interfaz de usuario de los elementos componibles a través de los cambios de configuración que pueden producirse con frecuencia como resultado del cambio de tamaño de ventanas de formato libre. Sin embargo, una app a menudo debe elevar el estado y la lógica de la IU de los componibles. Trasladar la propiedad del estado a un ViewModel es una de las mejores formas de preservar el estado durante el cambio de tamaño. A medida que elevas tu estado en un ViewModel, puedes experimentar problemas con el trabajo en segundo plano de larga duración, como el acceso pesado al sistema de archivos o las llamadas de red que son necesarias para inicializar tu pantalla.

Para ver un ejemplo del tipo de problemas con los que te puedes encontrar, agrega una instrucción de registro al método initializeUIState en ReplyViewModel.

fun initializeUIState() {
    Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
    val mailboxes: Map<MailboxType, List<Email>> =
        LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
    _uiState.value =
        ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
}

Ahora ejecuta la app en tu dispositivo de prueba e intenta cambiar el tamaño de la ventana de la app varias veces.

Cuando mires Logcat, te darás cuenta de que tu app muestra que el método de inicialización se ejecutó varias veces. Esto puede ser un problema para el trabajo que solo deseas ejecutar una vez para inicializar tu IU. Las llamadas de red adicionales, la E/S de archivos y otros tipos de trabajos pueden entorpecer el rendimiento del dispositivo y provocar otros problemas no deseados.

Para evitar un trabajo en segundo plano innecesario, quita la llamada a initializeUIState() del método onCreate() de tu actividad. En su lugar, inicializa los datos en el método init de ViewModel. Esto garantiza que el método de inicialización solo se ejecute una vez, cuando se cree una instancia de ReplyViewModel por primera vez:

init {
    initializeUIState()
}

Intenta ejecutar la app de nuevo y podrás ver que la innecesaria tarea de inicialización simulada se ejecuta solo una vez, independientemente de cuántas veces cambies el tamaño de la ventana de tu app. Esto se debe a que los ViewModels persisten más allá del ciclo de vida de Activity. Al ejecutar el código de inicialización solo una vez en la creación de ViewModel, lo separamos de cualquier recreación de Activity y evitamos trabajo innecesario. Si en realidad se tratara de una costosa llamada al servidor o de una operación de E/S pesada de archivos para inicializar la IU, se ahorrarían muchos recursos y mejoraría la experiencia del usuario.

7. ¡FELICITACIONES!

¡Lo lograste! ¡Muy bien! Ya implementaste algunas de las prácticas recomendadas para permitir que las apps para Android cambien correctamente de tamaño en ChromeOS y otros entornos multiventana y multipantalla.

Código fuente de muestra

Clona el repositorio desde GitHub…

git clone https://github.com/android/large-screen-codelabs/

… o descarga un archivo ZIP del repositorio y extráelo.