Cómo compilar y probar una app para usar en el SO Android Automotive con el automóvil estacionado

1. Antes de comenzar

Qué no es este codelab

Requisitos

Qué crearás

En este codelab, aprenderás a migrar al SO Android Automotive Road Reels, una app existente de transmisión de video por Internet para dispositivos móviles

La versión de punto de partida de la app que se ejecuta en un teléfono

La versión completa de la app que se ejecuta en un emulador del SO Android Automotive con un recorte de pantalla

La versión de punto de partida de la app que se ejecuta en un teléfono

La versión completa de la app que se ejecuta en un emulador del SO Android Automotive con un recorte de pantalla

Qué aprenderás

  • Cómo usar el emulador del SO Android Automotive
  • Cómo realizar los cambios necesarios para crear una compilación del SO Android Automotive
  • Suposiciones que suelen hacerse cuando se desarrollan apps para dispositivos móviles que pueden fallar cuando una app se ejecuta en el SO Android Automotive
  • Los diferentes niveles de calidad de las apps para automóviles
  • Cómo usar la sesión multimedia para permitir que otras apps controlen la reproducción de tu app
  • Cómo pueden diferir la IU del sistema y las inserciones de ventanas en los dispositivos con el SO Android Automotive en comparación con los dispositivos móviles

2. Prepárate

Obtén el código

  1. El código para este codelab se puede encontrar en el directorio build-a-parked-app dentro del repositorio car-codelabs de GitHub. Para clonarlo, ejecuta el siguiente comando:
git clone https://github.com/android/car-codelabs.git
  1. También tienes la opción de descargar el repositorio como archivo ZIP:

Abre el proyecto

  • Después de iniciar Android Studio, importa el proyecto. Elige solamente el directorio build-a-parked-app/start. El directorio build-a-parked-app/end contiene el código de la solución, que puedes consultar en cualquier momento si no logras avanzar o si deseas ver el proyecto completo.

Familiarízate con el código

  • Después de abrir el proyecto en Android Studio, dedica un momento a analizar el código de partida.

3. Aprende sobre las apps para usar en el SO Android Automotive con el automóvil estacionado

Las apps destinadas a usarse con el automóvil estacionado conforman un subconjunto de las categorías de apps compatibles con el SO Android Automotive. Al momento de escribir, constan de apps de transmisión de video por Internet, navegadores web y juegos. Estas apps son ideales para automóviles debido al hardware de los automóviles con Google integrado y a la prevalencia en constante crecimiento de los vehículos eléctricos, en los que el tiempo de carga representa una gran oportunidad para que los conductores y los pasajeros interactúen con este tipo de apps.

En muchos sentidos, los automóviles son similares a otros dispositivos con pantalla grande, como las tablets y los dispositivos plegables. Tienen pantallas táctiles con tamaños, resoluciones y relaciones de aspecto similares y que pueden estar en orientación horizontal o vertical (aunque, a diferencia de las tablets, su orientación es fija). También son dispositivos conectados que pueden entrar en la conexión de red y salir de esta. Con todo esto en mente, no sorprende que las apps que ya son adaptativas no suelan requerir mucho trabajo para ofrecer una excelente experiencia del usuario en automóviles.

Al igual que para las pantallas grandes, también hay niveles de calidad de las apps para automóviles:

  • Nivel 3 (lista para el automóvil): Tu app es compatible con pantallas grandes y se puede usar mientras el automóvil está estacionado. Si bien es posible que no tenga ninguna función optimizada para automóviles, los usuarios pueden experimentar la app como lo harían en cualquier otro dispositivo Android con pantalla grande. Las apps para dispositivos móviles que cumplen con estos requisitos son aptas para distribuirse en automóviles sin modificaciones a través del programa de apps para dispositivos móviles listas para usar en automóviles.
  • Nivel 2 (optimizada para el automóvil): Tu app brinda una gran experiencia en la pantalla central del automóvil. Para ello, tendrá ingeniería específica del automóvil para incluir capacidades que se pueden usar en los modos de conducción o estacionamiento, según la categoría de tu app.
  • Nivel 1 (diferenciada para el automóvil): Tu app se diseñó para funcionar con una amplia variedad de hardware en automóviles y puede adaptar su experiencia a los modos de conducción y estacionamiento. Proporciona la mejor experiencia del usuario diseñada para las diferentes pantallas de los automóviles, como la consola central, el clúster de instrumentos y las pantallas adicionales, como las panorámicas que se encuentran en muchos automóviles de alta gama.

4. Ejecuta la app en el emulador del SO Android Automotive

Instala Automotive con las imágenes del sistema de Play Store

  1. Primero, abre SDK Manager en Android Studio y selecciona la pestaña SDK Platforms si aún no está seleccionada. En la esquina inferior derecha de la ventana de SDK Manager, asegúrate de que esté marcada la casilla junto a Show package details.
  2. Instala la imagen del emulador Android Automotive with Google APIs del nivel de API 33 que se indica en Agrega imágenes genéricas del sistema. Las imágenes solo se pueden ejecutar en máquinas que tienen su misma arquitectura (x86/ARM).

Crea un dispositivo virtual del SO Android Automotive

  1. Después de abrir el Administrador de dispositivos, selecciona Automotive debajo de la columna Category en la parte izquierda de la ventana. Luego, selecciona el perfil de hardware incluido Automotive (1408p landscape) de la lista y haz clic en Next.
  2. En la siguiente página, selecciona la imagen del sistema del paso anterior. Haz clic en Next y selecciona las opciones avanzadas que desees antes de crear el AVD haciendo clic en Finish. Nota: Si eliges la imagen del nivel de API 30, es posible que esté en una pestaña que no sea la de Recommended.

Ejecuta la app

Ejecuta la app en el emulador que acabas de crear usando la configuración de ejecución app existente. Experimenta con la app para probar las diferentes pantallas y comparar su comportamiento si la ejecutas en un emulador de teléfono o tablet.

599922cd078f2589.png

5. Actualiza el manifiesto para declarar la compatibilidad con el SO Android Automotive

Si bien la app "simplemente funciona", hay algunos cambios pequeños que se deben realizar para que funcione de forma correcta en el SO Android Automotive y cumpla con los requisitos para poder publicarse en Play Store. Estos cambios se pueden realizar de manera tal que el mismo APK o paquete de aplicación sea compatible con dispositivos móviles y que tengan el SO Android Automotive. El primer conjunto de cambios es actualizar el archivo AndroidManifest.xml para indicar que la app es compatible con dispositivos que ejecutan el SO Android Automotive y que es una app de video.

Declara la función de hardware automotriz

Para indicar que tu app es compatible con dispositivos con el SO Android Automotive, agrega el siguiente elemento <uses-feature> al archivo AndroidManifest.xml:

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <uses-feature
        android:name="android.hardware.type.automotive"
        android:required="false" />
    ...
</manifest>

Usar un valor de false para el atributo android:required permite que el APK o el paquete de aplicación generados se distribuyan a dispositivos con el SO Android Automotive y a dispositivos móviles. Consulta Elige un tipo de segmento para el SO Android Automotive para obtener más información.

Marca tu app como app de video

La última parte de los metadatos que se debe agregar es el archivo automotive_app_desc.xml. Se usa para declarar la categoría de tu app en el contexto de Android para vehículos y es independiente de la categoría que selecciones para tu app en Play Console.

  1. Haz clic con el botón derecho en el módulo app y selecciona la opción New > Android Resource File e ingresa los siguientes valores antes de hacer clic en OK:
  • File name: automotive_app_desc.xml
  • Resource type: XML
  • Root element: automotiveApp
  • Source set: main
  • Directory name: xml

9fc697aec93d9d09.png

  1. Dentro de ese archivo, agrega el siguiente elemento <uses> para declarar que tu app es una app de video.

automotive_app_desc.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses
        name="video"
        tools:ignore="InvalidUsesTagAttribute" />
</automotiveApp>
  1. Dentro del elemento <application> existente, agrega el siguiente elemento <meta-data> que hace referencia al archivo automotive_app_desc.xml que acabas de crear.

AndroidManifest.xml

<application ...>
    <meta-data
        android:name="com.android.automotive"
        android:resource="@xml/automotive_app_desc" />
</application>

Con eso, realizaste todos los cambios necesarios para declarar la compatibilidad con el SO Android Automotive.

6. Cumple con los requisitos de calidad del SO Android Automotive: navegabilidad

Si bien declarar la compatibilidad con el SO Android Automotive es una parte de adaptar tu app para automóviles, aún es necesario asegurarte de que esta se pueda usar y sea segura.

Agrega indicaciones visuales de navegación

Mientras ejecutabas la app en el emulador del SO Android Automotive, es posible que hayas notado que no era posible regresar de la pantalla de detalles a la pantalla principal ni de la del reproductor a la de detalles. A diferencia de otros factores de forma, que pueden requerir un botón Atrás o un gesto táctil para habilitar la navegación hacia atrás, no existe tal requisito para los dispositivos con el SO Android Automotive. Por lo tanto, las apps deben proporcionar opciones de navegación en su IU para garantizar que los usuarios puedan navegar sin quedarse atrapados en una pantalla dentro de la app. Este requisito está codificado como el lineamiento de calidad AN-1.

Para admitir la navegación hacia atrás desde la pantalla de detalles hasta la pantalla principal, agrega un parámetro navigationIcon adicional para el elemento CenterAlignedTopAppBar de la pantalla de detalles de la siguiente manera:

RoadReelsApp.kt

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton

...

} else if (route?.startsWith(Screen.Detail.name) == true) {
    CenterAlignedTopAppBar(
        title = { Text(stringResource(R.string.bbb_title)) },
        navigationIcon = {
            IconButton(onClick = { navController.popBackStack() }) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = null
                )
            }
        }
    )
}

Para admitir la navegación hacia atrás desde la pantalla del reproductor hasta la pantalla principal, haz lo siguiente:

  1. Actualiza el elemento componible TopControls para que tome un parámetro de devolución de llamada con el nombre onClose y agrega un IconButton que lo llame cuando se haga clic.

PlayerControls.kt

import androidx.compose.material.icons.twotone.Close

...

@Composable
fun TopControls(
    title: String?,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Box(modifier) {
        IconButton(
            modifier = Modifier
                .align(Alignment.TopStart),
            onClick = onClose
        ) {
            Icon(
                Icons.TwoTone.Close,
                contentDescription = "Close player",
                tint = Color.White
            )
        }

        if (title != null) { ... }
    }
}
  1. Actualiza el elemento componible PlayerControls para que también tome un parámetro de devolución de llamada onClose y lo pase a TopControls.

PlayerControls.kt

fun PlayerControls(
    uiState: PlayerUiState,
    onClose: () -> Unit,
    onPlayPause: () -> Unit,
    onSeek: (seekToMillis: Long) -> Unit,
    modifier: Modifier = Modifier,
) {
    AnimatedVisibility(
        visible = uiState.isShowingControls,
        enter = fadeIn(),
        exit = fadeOut()
    ) {
        Box(modifier = modifier.background(Color.Black.copy(alpha = .5f))) {
            TopControls(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(dimensionResource(R.dimen.screen_edge_padding))
                    .align(Alignment.TopCenter),
                title = uiState.mediaMetadata.title?.toString(),
                onClose = onClose
            )
            ...
        }
    }
}
  1. A continuación, actualiza el elemento componible PlayerScreen para que tome el mismo parámetro y lo pase a su PlayerControls.

PlayerScreen.kt

@Composable
fun PlayerScreen(
    onClose: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: PlayerViewModel = viewModel()
) {
    ...

    PlayerControls(
        modifier = Modifier
            .fillMaxSize(),
        uiState = playerUiState,
        onClose = onClose,
        onPlayPause = { if (playerUiState.isPlaying) viewModel.pause() else viewModel.play() },
        onSeek = viewModel::seekTo
    )
}
  1. Por último, en RoadReelsNavHost, proporciona la implementación que se pasa a PlayerScreen:

RoadReelsNavHost.kt

composable(route = Screen.Player.name, ...) {
    PlayerScreen(onClose = { navController.popBackStack() })
}

Ahora el usuario puede moverse entre pantallas sin encontrarse con ningún punto muerto. Además, la experiencia del usuario también puede ser mejor en otros factores de forma. Por ejemplo, en un teléfono alto, cuando la mano del usuario ya está cerca de la parte superior de la pantalla, puede navegar más fácilmente por la app sin necesidad de mover el dispositivo en la mano.

46cf7ec051b32ddf.gif

Adapta la app para que sea compatible con la orientación de la pantalla

A diferencia de la gran mayoría de los dispositivos móviles, la mayoría de los automóviles tienen una orientación fija. Es decir, admiten orientación horizontal o vertical, pero no ambas, ya que sus pantallas no se pueden rotar. Por este motivo, las apps deben evitar suponer que se admiten ambas orientaciones.

En la sección Crea un manifiesto del SO Android Automotive, agregaste dos elementos <uses-feature> para las funciones android.hardware.screen.portrait y android.hardware.screen.landscape con el atributo required establecido en false. De esta manera, se garantiza que ninguna dependencia de funciones implícita en cualquier orientación de pantalla pueda impedir que la app se distribuya en automóviles. Sin embargo, esos elementos del manifiesto no cambian el comportamiento de la app, sino la forma en que se distribuye.

En la actualidad, la app tiene una función útil con la que se configura automáticamente la orientación de la actividad en modo horizontal cuando se abre el reproductor de video, de modo que los usuarios del teléfono no tengan que tocar su dispositivo para cambiar su orientación si todavía no está en el modo horizontal.

Lamentablemente, ese mismo comportamiento puede generar un bucle parpadeante o un formato letterbox en dispositivos con orientación vertical fija, lo que incluye muchos automóviles que circulan en la ruta en la actualidad.

Para solucionar este problema, puedes agregar una verificación basada en las orientaciones de pantalla que admita el dispositivo actual.

  1. Para simplificar la implementación, primero, agrega lo siguiente en Extensions.kt:

Extensions.kt

import android.content.Context
import android.content.pm.PackageManager

...

enum class SupportedOrientation {
    Landscape,
    Portrait,
}

fun Context.supportedOrientations(): List<SupportedOrientation> {
    return when (Pair(
        packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE),
        packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT)
    )) {
        Pair(true, false) -> listOf(SupportedOrientation.Landscape)
        Pair(false, true) -> listOf(SupportedOrientation.Portrait)
        // For backwards compat, if neither feature is declared, both can be assumed to be supported
        // 
        else -> listOf(SupportedOrientation.Landscape, SupportedOrientation.Portrait)
    }
}
  1. Luego, protege la llamada para establecer la orientación solicitada. Como las apps pueden tener un problema similar en el modo multiventana en dispositivos móviles, también puedes incluir una verificación para no configurar la orientación de forma dinámica en ese caso.

PlayerScreen.kt

import com.example.android.cars.roadreels.SupportedOrientation
import com.example.android.cars.roadreels.supportedOrientations

...

DisposableEffect(Unit) {
    ...

    // Only automatically set the orientation to landscape if the device supports landscape.
    // On devices that are portrait only, the activity may enter a compat mode and won't get to
    // use the full window available if so. The same applies if the app's window is portrait
    // in multi-window mode.
    if (activity.supportedOrientations().contains(SupportedOrientation.Landscape)
        && !activity.isInMultiWindowMode
    ) {
        activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
    }

    ...
}

La pantalla del reproductor entra en un bucle parpadeante en el emulador de Polestar 2 antes de agregar la verificación (cuando la actividad no controla los cambios de configuración de orientación).

La pantalla del reproductor se muestra en formato letterbox en el emulador de Polestar 2 antes de agregar la verificación (cuando la actividad controla los cambios de configuración de orientación).

La pantalla del reproductor no se muestra en formato letterbox en el emulador de Polestar 2 después de agregar la marca de verificación.

La pantalla del reproductor entra en un bucle parpadeante en el emulador de Polestar 2 antes de agregar la verificación (cuando la actividad no controla los cambios de configuración de orientation).

La pantalla del reproductor se muestra en formato letterbox en el emulador de Polestar 2 antes de agregar la verificación (cuando la actividad controla los cambios de configuración de orientation).

La pantalla del reproductor no se muestra en formato letterbox en el emulador de Polestar 2 después de agregar la marca de verificación.

Como esta es la única ubicación de la app que establece la orientación de la pantalla, ahora la app evita el formato letterbox. En tu propia app, busca atributos screenOrientation o llamadas setRequestedOrientation que sean únicamente para orientaciones horizontales o verticales (incluidas las variantes sensor, reverse y user de cada una), y quítalos o protégelos según sea necesario para limitar el formato letterbox. Para obtener más información, consulta Modo de compatibilidad del dispositivo.

Adapta la app a la capacidad de control de la barra del sistema

Lamentablemente, si bien el cambio anterior garantiza que la app no entre en un bucle parpadeante ni se muestre con formato letterbox, también expone otra suposición que no se cumple, es decir, que las barras del sistema siempre se pueden ocultar. Como los usuarios tienen diferentes necesidades cuando utilizan sus automóviles (en comparación con cuando usan sus teléfonos o tablets), los OEM tienen la opción de evitar que las apps oculten las barras del sistema para garantizar que siempre se pueda acceder a los controles del automóvil, como los de climatización, en la pantalla.

Como resultado, es posible que las apps se rendericen detrás de las barras del sistema cuando lo hagan en modo envolvente y den por sentado que las barras se pueden ocultar. Puedes observarlo en el paso anterior, ya que los controles de la parte inferior y superior del reproductor ya no estarán visibles cuando la app no se muestre en formato letterbox. En este caso específico, ya no se puede navegar por la app, dado que el botón para cerrar el reproductor está oculto, y su funcionalidad se ve impedida, puesto que no se puede usar la barra deslizante de búsqueda.

La solución más fácil sería aplicar el padding de las inserciones de ventana systemBars al reproductor de la siguiente manera:

PlayerScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding

...

Box(
    modifier = Modifier
        .fillMaxSize()
        .windowInsetsPadding(WindowInsets.systemBars)
) {
    PlayerView(...)
    PlayerControls(...)
}

Sin embargo, esta solución no es la ideal, ya que causa que los elementos de la IU se muevan cuando desaparecen las animaciones de las barras del sistema.

9fa1de6d2518340a.gif

Para mejorar la experiencia del usuario, puedes actualizar la app para hacer un seguimiento de las inserciones que se pueden controlar y para aplicar padding solo a las que no se pueden controlar.

  1. Como otras pantallas dentro de la app podrían estar interesadas en controlar las inserciones de ventana, tiene sentido pasar las inserciones controlables como un CompositionLocal. Crea un nuevo archivo LocalControllableInsets.kt en el paquete com.example.android.cars.roadreels y agrega lo siguiente:

LocalControllableInsets.kt

import androidx.compose.runtime.compositionLocalOf

// Assume that no insets can be controlled by default
const val DEFAULT_CONTROLLABLE_INSETS = 0
val LocalControllableInsets = compositionLocalOf { DEFAULT_CONTROLLABLE_INSETS }
  1. Configura un OnControllableInsetsChangedListener para detectar cambios.

MainActivity.kt

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat.OnControllableInsetsChangedListener

...

class MainActivity : ComponentActivity() {
    private lateinit var onControllableInsetsChangedListener: OnControllableInsetsChangedListener

    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        enableEdgeToEdge()

        setContent {
            var controllableInsetsTypeMask by remember { mutableIntStateOf(DEFAULT_CONTROLLABLE_INSETS) }

            onControllableInsetsChangedListener =
                OnControllableInsetsChangedListener { _, typeMask ->
                    if (controllableInsetsTypeMask != typeMask) {
                        controllableInsetsTypeMask = typeMask
                    }
                }

            WindowCompat.getInsetsController(window, window.decorView)
                .addOnControllableInsetsChangedListener(onControllableInsetsChangedListener)

            RoadReelsTheme {
                RoadReelsApp(calculateWindowSizeClass(this))
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        WindowCompat.getInsetsController(window, window.decorView)
            .removeOnControllableInsetsChangedListener(onControllableInsetsChangedListener)
    }
}
  1. Agrega un CompositionLocalProvider de nivel superior que contenga los elementos componibles de la app y el tema, y que vincule valores a LocalControllableInsets.

MainActivity.kt

import androidx.compose.runtime.CompositionLocalProvider

...

CompositionLocalProvider(LocalControllableInsets provides controllableInsetsTypeMask) {
    RoadReelsTheme {
        RoadReelsApp(calculateWindowSizeClass(this))
    }
}
  1. En el reproductor, lee el valor actual y úsalo para determinar las inserciones que se ocultarán y las que se utilizarán para el padding.

PlayerScreen.kt

import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.unit.dp
import com.example.android.cars.roadreels.LocalControllableInsets

...

val controllableInsetsTypeMask by rememberUpdatedState(LocalControllableInsets.current)

DisposableEffect(Unit) {
    ...
    windowInsetsController.hide(WindowInsetsCompat.Type.systemBars().and(controllableInsetsTypeMask))
    ...
}

...

// When the system bars can be hidden, ignore them when applying padding to the player and
// controls so they don't jump around as the system bars disappear. If they can't be hidden
// include them so nothing renders behind the system bars
var windowInsetsForPadding = WindowInsets(0.dp)
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.statusBars()) == 0) {
    windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.statusBars)
}
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.navigationBars()) == 0) {
    windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.navigationBars)
}

Box(
    modifier = Modifier
        .fillMaxSize()
        .windowInsetsPadding(windowInsetsForPadding)
) {
    PlayerView(...)
    PlayerControls(...)
}

El contenido permanece visible cuando no se pueden ocultar las barras del sistema.

El contenido no se mueve cuando se pueden ocultar las barras del sistema.

El contenido permanece visible cuando no se pueden ocultar las barras del sistema.

Mucho mejor: el contenido no se mueve y, al mismo tiempo, los controles son completamente visibles, incluso en los automóviles en los que no se pueden controlar las barras del sistema.

7. Cumple con los requisitos de calidad del SO Android Automotive: distracción del conductor

Por último, hay una diferencia importante entre los automóviles y otros factores de forma: se usan para conducir. Por lo tanto, es muy importante limitar las distracciones mientras se conduce. Todas las apps para usar en el SO Android Automotive con el automóvil estacionado deben pausar la reproducción cuando las restricciones de la experiencia del usuario se activen y evitar que se reanude la reproducción mientras estas restricciones estén activas. Cuando se activan las restricciones de la experiencia del usuario, aparece una superposición del sistema y, a su vez, se llama al evento de ciclo de vida onPause para que se superponga sobre la app. Durante esta llamada, las apps deben pausar la reproducción.

Simula la conducción

Navega a la vista del reproductor en el emulador y comienza a reproducir contenido. Luego, sigue los pasos para simular la conducción y observa que, si bien el sistema oculta la IU de la app, la reproducción no se pausa. Infringe el lineamiento de calidad DD-2 de apps para automóviles.

c2eda16df688c102.png

Pausa la reproducción cuando comienzas a conducir

  1. Agrega una dependencia en el artefacto androidx.lifecycle:lifecycle-runtime-compose, que contiene el LifecycleEventEffect que ayuda a ejecutar el código en eventos de ciclo de vida.

libs.version.toml

androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }

build.gradle.kts (módulo :app)

implementation(libs.androidx.lifecycle.runtime.compose)
  1. Después de sincronizar el proyecto para descargar la dependencia, agrega un LifecycleEventEffect que se ejecute en el evento ON_PAUSE para pausar la reproducción (y, de manera opcional, en el evento ON_RESUME para reanudarla).

PlayerScreen.kt

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect

...

@Composable
fun PlayerScreen(...) {
    ...
    LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
        viewModel.pause()
    }

    LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
        viewModel.play()
    }
    ...
}

Una vez implementada la corrección, sigue los mismos pasos que hiciste antes para simular la conducción durante la reproducción activa y observa que la reproducción se detiene y se cumple con el requisito DD-2.

8. Prueba la app en el emulador de pantalla distante

Un nuevo parámetro de configuración que comenzará a aparecer en los automóviles es un ajuste de dos pantallas con una pantalla principal en la consola central y una pantalla secundaria en la parte superior del panel, cerca del parabrisas. Las apps se pueden mover de la pantalla central a la pantalla secundaria y viceversa para brindar más opciones a los conductores y pasajeros.

Instala la imagen Automotive Distant Display

  1. Primero, abre Preview en SDK Manager en Android Studio y selecciona la pestaña SDK Platforms si aún no está seleccionada. En la esquina inferior derecha de la ventana de SDK Manager, asegúrate de que esté marcada la casilla junto a Show package details.
  2. Instala la imagen del emulador Automotive Distant Display with Google Play del nivel de API 33 para la arquitectura de tu computadora (x86/ARM).

Crea un dispositivo virtual del SO Android Automotive

  1. Después de abrir el Administrador de dispositivos, selecciona Automotive debajo de la columna Category en la parte izquierda de la ventana. Luego, selecciona el perfil de hardware incluido Automotive Distant Display with Google Play de la lista y haz clic en Next.
  2. En la siguiente página, selecciona la imagen del sistema del paso anterior. Haz clic en Next y selecciona las opciones avanzadas que desees antes de crear el AVD haciendo clic en Finish.

Ejecuta la app

Ejecuta la app en el emulador que acabas de crear usando la configuración de ejecución app existente. Sigue las instrucciones que se indican en Usa el emulador de pantalla distante para mover la app hacia la pantalla distante y desde esta. Prueba mover la app cuando esté en la pantalla principal o de detalles y en la del reproductor, y prueba interactuar con la app en ambas pantallas.

b277bd18a94e9c1b.png

9. Mejora la experiencia en la app en la pantalla distante

Cuando usaste la app en la pantalla distante, es posible que hayas notado dos cosas:

  1. La reproducción presenta saltos cuando la app se mueve hacia la pantalla distante y desde esta.
  2. No puedes interactuar con la app mientras está en la pantalla distante, lo que incluye cambiar el estado de reproducción.

Mejora la continuidad de las apps

Los saltos en la reproducción se deben a que se vuelve a crear la actividad por un cambio de configuración. Como la app se escribe con Compose y la configuración que cambia está relacionada con el tamaño, es sencillo permitir que Compose controle los cambios de configuración para ti restringiendo la recreación de actividades para los cambios de configuración basados en el tamaño. De esta manera, la transición entre pantallas se vuelve fluida sin interrupciones en la reproducción ni recargas debido a la recreación de actividades.

AndroidManifest.xml

<activity
    android:name="com.example.android.cars.roadreels.MainActivity"
    ...
    android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout|density">
        ...
</activity>

Implementa los controles de reproducción

Para solucionar el problema por el que no se puede controlar la app mientras está en la pantalla distante, puedes implementar MediaSession. Las sesiones multimedia proporcionan una forma universal de interactuar con un reproductor de audio o video. Para obtener más información, consulta Cómo controlar y anunciar la reproducción con una MediaSession.

  1. Agrega una dependencia en el artefacto androidx.media3:media3-session.

libs.version.toml

androidx-media3-mediasession = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }

build.gradle.kts (módulo :app)

implementation(libs.androidx.media3.mediasession)
  1. En PlayerViewModel, agrega una variable para contener la sesión multimedia y crea un MediaSession con su compilador.

PlayverViewModel.kt

import androidx.media3.session.MediaSession
...

class PlayerViewModel(...) {
    ...

    private var mediaSession: MediaSession? = null
    
    init {
        viewModelScope.launch {
            _player.onEach { player ->
                playerUiStateUpdateJob?.cancel()
                mediaSession?.release()

                if (player != null) {
                    initializePlayer(player)
                    mediaSession = MediaSession.Builder(application, player).build()
                    playerUiStateUpdateJob = viewModelScope.launch {... }
                }
            }.collect()
        }
    }
}
  1. Luego, agrega una línea adicional en el método onCleared para liberar MediaSession cuando ya no se necesite PlayerViewModel.

PlayerViewModel.kt

override fun onCleared() {
    super.onCleared()
    mediaSession?.release()
    _player.value?.release()
}
  1. Por último, cuando estés en la pantalla del reproductor (con la app en la pantalla principal o distante), podrás probar los controles multimedia con el comando adb shell cmd media_session dispatch.
# To play content
adb shell cmd media_session dispatch play

# To pause content
adb shell cmd media_session dispatch pause

# To toggle the playing state
adb shell cmd media_session dispatch play-pause

Restringe la reanudación de la reproducción

Si bien la compatibilidad con MediaSession permite controlar la reproducción mientras la app está en la pantalla distante, presenta un problema nuevo. En particular, permite que se reanude la reproducción mientras se aplican restricciones de experiencia del usuario, lo que incumple el lineamiento de calidad DD-2 (otra vez). Para probarlo por tu cuenta, haz lo siguiente:

  1. Inicia la reproducción.
  2. Simula la conducción.
  3. Usa el comando media_session dispatch. Observa que la reproducción se reanuda aunque la app esté oculta.

Para solucionar este problema, puedes escuchar las restricciones de la experiencia del usuario del dispositivo y solo permitir la reanudación de la reproducción mientras estén activas. Esto incluso se puede hacer de una manera en la que se pueda usar la misma lógica para dispositivos móviles y el SO Android Automotive.

  1. En el archivo build.gradle del módulo app, agrega lo siguiente para incluir la biblioteca de Android Automotive y, luego, realiza una sincronización de Gradle:

build.gradle.kts

android {
    ...
    useLibrary("android.car")
}
  1. Haz clic con el botón derecho en el paquete com.example.android.cars.roadreels y selecciona New > Kotlin Class/File. Ingresa RoadReelsPlayer como nombre y haz clic en el tipo Class.
  2. En el archivo que acabas de crear, agrega la siguiente implementación inicial de la clase. Cuando se extiende ForwardingSimpleBasePlayer, es sencillo modificar los comandos y las interacciones admitidos para un reproductor unido anulando el método getState().

RoadReelsPlayer.kt

import android.content.Context
import androidx.media3.common.ForwardingSimpleBasePlayer
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer

@UnstableApi
class RoadReelsPlayer(context: Context) :
    ForwardingSimpleBasePlayer(ExoPlayer.Builder(context).build()) {
    private var shouldPreventPlay = false

    override fun getState(): State {
        val state = super.getState()

        return state.buildUpon()
            .setAvailableCommands(
                state.availableCommands.buildUpon().removeIf(COMMAND_PLAY_PAUSE, shouldPreventPlay)
                    .build()
            ).build()
    }
}
  1. En PlayerViewModel.kt, actualiza la declaración de la variable del reproductor para usar una instancia de RoadReelsPlayer en lugar de ExoPlayer. En este momento, el comportamiento será exactamente el mismo que antes, ya que shouldPreventPlay nunca se actualiza desde su valor predeterminado de false.

PlayerViewModel.kt

init {
    ...
    _player.update { RoadReelsPlayer(application) }
}
  1. Para comenzar a hacer un seguimiento de las restricciones de la experiencia del usuario, agrega el siguiente bloque init y la implementación handleRelease:

RoadReelsPlayer.kt

import android.car.Car
import android.car.drivingstate.CarUxRestrictions
import android.car.drivingstate.CarUxRestrictionsManager
import android.content.pm.PackageManager
import com.google.common.util.concurrent.ListenableFuture

...

@UnstableApi
class RoadReelsPlayer(context: Context) :
    ForwardingSimpleBasePlayer(ExoPlayer.Builder(context).build()) {
    ...
    private var pausedByUxRestrictions = false
    private lateinit var carUxRestrictionsManager: CarUxRestrictionsManager

   init {
        with(context) {
            // Only listen to UX restrictions if the device is running Android Automotive OS
            if (packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
                val car = Car.createCar(context)
                carUxRestrictionsManager =
                    car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as CarUxRestrictionsManager

                // Get the initial UX restrictions and update the player state
                shouldPreventPlay =
                    carUxRestrictionsManager.currentCarUxRestrictions.isRequiresDistractionOptimization
                invalidateState()

                // Register a listener to update the player state as the UX restrictions change
                carUxRestrictionsManager.registerListener { carUxRestrictions: CarUxRestrictions ->
                    shouldPreventPlay = carUxRestrictions.isRequiresDistractionOptimization

                    if (!shouldPreventPlay && pausedByUxRestrictions) {
                        handleSetPlayWhenReady(true)
                        invalidateState()
                    } else if (shouldPreventPlay && isPlaying) {
                        pausedByUxRestrictions = true
                        handleSetPlayWhenReady(false)
                        invalidateState()
                    }
                }
            }

            addListener(object : Player.Listener {
                override fun onEvents(player: Player, events: Player.Events) {
                    if (events.contains(EVENT_IS_PLAYING_CHANGED) && isPlaying) {
                        pausedByUxRestrictions = false
                    }
                }
            })
        }
    }

    ...

    override fun handleRelease(): ListenableFuture<*> {
        if (::carUxRestrictionsManager.isInitialized) {
            carUxRestrictionsManager.unregisterListener()
        }
        return super.handleRelease()
    }
}

Ten en cuenta lo siguiente:

  • CarUxRestrictionsManager se almacena como una variable lateinit, ya que no se crea una instancia ni se usa en dispositivos que no tengan el SO Android Automotive, pero se debe limpiar su objeto de escucha cuando se lance el reproductor.
  • Solo se hace referencia al valor isRequiresDistractionOptimization cuando se determina el estado de restricción de la UX. Si bien la clase CarUxRestrictions contiene detalles adicionales sobre qué restricciones están activas, no es necesario hacer referencia a ellas, ya que solo están diseñadas para que las usen las apps con optimización de distracciones (como las apps de navegación), ya que esas apps siguen siendo visibles mientras las restricciones están activas.
  • Después de cualquier actualización de la variable shouldPreventPlay, se llama a invalidateState() para informar a los usuarios sobre el estado del cambio del reproductor.
  • En el objeto de escucha, la reproducción se pausa o se reanuda automáticamente llamando a handleSetPlayWhenReady con el valor adecuado.
  1. Ahora, prueba reanudar la reproducción mientras simulas la conducción como se describe al comienzo de esta sección y observa que no se reanuda.
  2. Por último, dado que RoadReelsPlayer controla la pausa de la reproducción cuando las restricciones de la experiencia del usuario se vuelven activas, no es necesario que LifecycleEventEffect pause el reproductor durante ON_PAUSE. En su lugar, se puede cambiar a ON_STOP, de modo que la reproducción se detenga cuando el usuario salga de la app para ir al selector o abrir otra app.

PlayerScreen.kt

LifecycleEventEffect(Lifecycle.Event.ON_START) {
    viewModel.play()
}
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
    viewModel.pause()
}

Resumen

Con eso, la app funciona mucho mejor en automóviles, tanto con pantallas distantes como sin ellas. Sin embargo, más que eso, también funciona mejor en otros factores de forma. En los dispositivos que pueden rotar la pantalla o permitir que los usuarios cambien el tamaño de una ventana de la app, la app ahora también se adapta sin problemas en esas situaciones.

Además, gracias a la integración de la sesión multimedia, la reproducción de la app se puede controlar no solo con controles de hardware y software en automóviles, sino también con otras fuentes, como una pregunta a Asistente de Google o un botón para pausar en un par de auriculares, lo que les brinda a los usuarios más opciones para controlar la app en todos los factores de forma.

10. Prueba la app en diferentes configuraciones del sistema

Como la app ya funciona bien en la pantalla principal y en la pantalla distante, lo último que debes verificar es cómo maneja las diferentes configuraciones de la barra del sistema y los cortes de pantalla. Según describe en Cómo trabajar con inserciones de ventanas y cortes de pantalla, los dispositivos con el SO Android Automotive pueden tener configuraciones que no cumplan las suposiciones que suelen ser verdaderas en los factores de forma de dispositivos móviles.

En esta sección, aprenderás a configurar el emulador para que tenga una barra del sistema izquierda y probarás la app con esa configuración.

Configura una barra del sistema lateral

Como se detalla en Cómo realizar pruebas con el emulador configurable, hay una variedad de opciones para emular diferentes configuraciones del sistema presentes en los automóviles.

Para los fines de este codelab, se puede usar com.android.systemui.rro.left para probar una configuración diferente de la barra del sistema. Para habilitarlo, usa el siguiente comando:

adb shell cmd overlay enable --user 0 com.android.systemui.rro.left

b642703a7278b219.png

Como la app usa el modificador systemBars como contentWindowInsets en Scaffold, el contenido ya se dibuja en un área segura de las barras del sistema. Para ver qué sucedería si la app asumiera que las barras del sistema solo aparecían en la parte inferior y superior de la pantalla, cambia ese parámetro por lo siguiente:

RoadReelsApp.kt

contentWindowInsets = if (route?.equals(Screen.Player.name) == true) WindowInsets(0.dp) else WindowInsets.systemBars.only(WindowInsetsSides.Vertical)

¡Uy! La pantalla de lista y de detalles se renderiza detrás de la barra del sistema. Gracias al trabajo anterior, la pantalla del reproductor estaría bien, incluso si las barras del sistema no se pudieran controlar desde entonces.

9898f7298a7dfb4.gif

Antes de pasar a la siguiente sección, asegúrate de revertir el cambio que acabas de realizar en el parámetro windowContentPadding.

11. Trabaja con cortes de pantalla

Por último, algunos automóviles tienen pantallas con cortes de pantalla muy diferentes a los que se ven en los dispositivos móviles. En lugar de las muescas o los cortes de la cámara estenopeica, algunos automóviles con el SO Android Automotive tienen pantallas curvas que hacen que la pantalla no sea rectangular.

Para ver cómo se comporta la app cuando hay un corte de pantalla, primero habilita el corte de pantalla con el siguiente comando:

adb shell cmd overlay enable --user 0 com.android.internal.display.cutout.emulation.top_and_right

Para probar realmente el comportamiento de la app, habilita también la barra izquierda del sistema que se usó en la última sección, en caso de que aún no lo esté:

adb shell cmd overlay enable --user 0 com.android.systemui.rro.left

Sin modificaciones, la app no se renderiza en el corte de pantalla (la forma exacta del corte es difícil de determinar en este momento, pero se aclarará en el siguiente paso). Una app así está bien y ofrece una mejor experiencia que una app que se renderiza en cortes, pero no se adapta a ellos con cuidado.

935aa1d4ee3eb72.png

Renderiza en el corte de pantalla

Para brindarles a los usuarios la experiencia más envolvente posible, puedes aprovechar mucho más espacio renderizando en el corte de pantalla.

  1. Para renderizar en el corte de pantalla, crea un archivo integers.xml que contenga la anulación específica para los automóviles. Para ello, usa el calificador modo de IU con el valor Car Dock (el nombre es un remanente de cuando solo existía Android Auto, pero también lo usa el SO Android Automotive). Además, como el valor que usarás, LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS, se introdujo en Android R, también agrega el calificador Version de Android con el valor 30. Consulta Cómo usar recursos alternativos para obtener más detalles.

22b7f17657cac3fd.png

  1. En el archivo que acabas de crear (res/values-car-v30/integers.xml), agrega lo siguiente:

integers.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="windowLayoutInDisplayCutoutMode">3</integer>
</resources>

El valor entero 3 corresponde a LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS y anula el valor predeterminado de 0 de res/values/integers.xml, que corresponde a LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT. Ya se hace referencia a este valor entero en MainActivity.kt para anular el modo establecido por enableEdgeToEdge(). Para obtener más información sobre este atributo, consulta la documentación de referencia.

Ahora, cuando ejecutes la app, observa que el contenido se extiende hasta el corte y se ve muy envolvente. Sin embargo, la barra superior de la aplicación y parte del contenido están parcialmente ocultos por el corte de pantalla, lo que provoca un problema similar a lo que sucedió cuando la app dio por sentado que las barras del sistema solo aparecerían en la parte inferior y superior.

1d791b9e2ec91bda.png

Corrige las barras superiores de la aplicación

Para corregir las barras superiores de la aplicación, puedes agregar el siguiente parámetro windowInsets a los elementos componibles CenterAlignedTopAppBar:

RoadReelsApp.kt

import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing

...

windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)

Como safeDrawing consta de las inserciones displayCutout y systemBars, mejora el parámetro windowInsets predeterminado, que solo usa systemBars cuando se posiciona la barra superior de la aplicación.

Además, como la barra superior de la aplicación se posiciona en la parte superior de la ventana, no debes incluir el componente inferior de las inserciones safeDrawing. Si lo haces, es posible que se agregue padding innecesario.

21d9a237572f85c2.png

Corrige la pantalla principal

Una opción para corregir el contenido en la pantalla principal y de detalles sería usar safeDrawing en lugar de systemBars para contentWindowInsets de Scaffold. Sin embargo, la app se ve mucho menos envolvente con esa opción, ya que el contenido se corta de forma abrupta donde comienza el corte de pantalla, lo que no es mucho mejor que si la app no se renderizara en el corte de pantalla.

80bca44f0962a4a1.png

Para obtener una interfaz de usuario más envolvente, puedes controlar las inserciones en cada componente dentro de la pantalla.

  1. Actualiza contentWindowInsets de Scaffold para que sea constantemente de 0 dp (en lugar de solo para PlayerScreen). De esta manera, cada pantalla o componente dentro de una pantalla puede determinar cómo se comporta con respecto a las inserciones.

RoadReelsApp.kt

Scaffold(
    ...,
    contentWindowInsets = WindowInsets(0.dp)
) { ... }
  1. Establece windowInsetsPadding de los elementos componibles Text del encabezado de la fila para usar los componentes horizontales de las inserciones safeDrawing. El componente superior de estas inserciones es controlado por la barra superior de la aplicación, y el componente inferior se mencionará más adelante.

MainScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding

...

LazyColumn(
    contentPadding = PaddingValues(bottom = dimensionResource(R.dimen.screen_edge_padding))
) {
    items(NUM_ROWS) { rowIndex: Int ->
        Text(
            "Row $rowIndex",
            style = MaterialTheme.typography.headlineSmall,
            modifier = Modifier
                .padding(
                    horizontal = dimensionResource(R.dimen.screen_edge_padding),
                    vertical = dimensionResource(R.dimen.row_header_vertical_padding)
                )
                .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
        )
    ...
}
  1. Quita el parámetro contentPadding de LazyRow. Luego, al principio y al final de cada LazyRow, agrega un Spacer del ancho del componente safeDrawing correspondiente para asegurarte de que todas las miniaturas se puedan ver por completo. Usa el modificador widthIn para asegurarte de que estos separadores tengan mínimo el mismo ancho que el padding del contenido. Sin estos elementos, los elementos al principio y los finales de la fila podrían estar ocultos detrás de las barras del sistema o el corte de pantalla, incluso cuando se deslicen por completo al principio o al final de la fila.

MainScreen.kt

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsEndWidth
import androidx.compose.foundation.layout.windowInsetsStartWidth

...

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.list_item_spacing)),
) {
    item {
        Spacer(
            Modifier
                .windowInsetsStartWidth(WindowInsets.safeDrawing)
                .widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
        )
    }
    items(NUM_ITEMS_PER_ROW) { ... }
    item {
        Spacer(
            Modifier
                .windowInsetsEndWidth(WindowInsets.safeDrawing)
                .widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
        )
    }
}
  1. Por último, agrega Spacer al final de LazyColumn para tener en cuenta las posibles barras del sistema o las inserciones de corte de pantalla en la parte inferior de la pantalla. No es necesario un separador equivalente en la parte superior de LazyColumn, ya que la barra superior de la aplicación se encarga de eso. Si la app usara una barra inferior en lugar de una superior, deberías agregar Spacer al comienzo de la lista con el modificador windowInsetsTopHeight. Además, si la app usara una barra inferior y superior, no se necesitaría ningún separador.

MainScreen.kt

import androidx.compose.foundation.layout.windowInsetsBottomHeight

...

LazyColumn(...){
    items(NUM_ROWS) { ... }
    item {
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
    }
}

Las barras superiores de la aplicación son completamente visibles, y, cuando te desplazas hasta el final de una fila, ahora puedes ver todas las miniaturas en su totalidad.

b437a762e31abd02.png

Corrige la pantalla de detalles

f622958a8d0c16c8.png

La pantalla de detalles no es tan mala, pero el contenido aún se corta.

Como la pantalla de detalles no tiene contenido desplazable, para solucionarlo, solo se necesita agregar un modificador windowInsetsPadding en el nivel superior Box.

DetailScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding

...

Box(
    modifier = modifier
        .padding(dimensionResource(R.dimen.screen_edge_padding))
        .windowInsetsPadding(WindowInsets.safeDrawing)
) { ... }

adf17e27b576ec5a.png

Corrige la pantalla del reproductor

Si bien PlayerScreen ya aplica padding para algunas o todas las inserciones de ventana de la barra del sistema, como se comenta en la sección anterior Cumple con los requisitos de calidad del SO Android Automotive: navegabilidad, no es suficiente para asegurarte de que no se oculte ahora que la app se renderiza en cortes de pantalla. En los dispositivos móviles, los recortes de pantalla casi siempre están contenidos, por completo, dentro de las barras del sistema. Sin embargo, en los automóviles, los cortes de pantalla pueden extenderse mucho más allá de las barras del sistema, lo que no cumple las suposiciones.

fc14798bc71110d3.png

Para solucionar este problema, cambia el valor inicial de la variable windowInsetsForPadding de cero a displayCutout:

PlayerScreen.kt

import androidx.compose.foundation.layout.displayCutout

...

var windowInsetsForPadding = WindowInsets(WindowInsets.displayCutout)

cce55d3f8129935d.png

Genial. La app aprovecha al máximo la pantalla y sigue siendo funcional.

Además, si ejecutas la app en un dispositivo móvil, también será más envolvente. Los elementos de la lista se renderizan hasta los bordes de la pantalla, incluso detrás de la barra de navegación.

dc7918499a33df31.png

12. Felicitaciones

Migraste y optimizaste, de forma correcta, tu primera app para usarse en el automóvil estacionado. Ahora es el momento de aplicar lo que aprendiste a tu propia app.

Pruebas para hacer

Lecturas adicionales