Capa de la IU

La función de la IU es mostrar los datos de la aplicación en la pantalla y servir como punto principal de interacción con el usuario. Cuando los datos cambian, ya sea debido a la interacción del usuario (como cuando presiona un botón) o una entrada externa (como una respuesta de red), la IU debe actualizarse para reflejar los cambios. En efecto, la IU es una representación visual del estado de la aplicación tal como se recuperó de la capa de datos.

Sin embargo, los datos de aplicación que obtienes de la capa de datos suelen estar en un formato diferente al de la información que necesitas mostrar. Por ejemplo, es posible que solo necesites parte de los datos de la IU, o bien que debas combinar dos fuentes de datos diferentes para presentar información que al usuario le resulte relevante. Sin importar la lógica que apliques, debes pasarle a la IU toda la información que necesita para renderizarse por completo. La capa de IU es la canalización que convierte los cambios en los datos de la aplicación en un formato que la IU puede presentar y, luego, mostrar.

En una arquitectura típica, los elementos de la IU de la capa de la IU dependen de los contenedores de estado, que, a su vez, dependen de las clases de la capa de datos o de la capa opcional de dominio.
Figura 1: El rol de la capa de la IU en la arquitectura de la app.

Caso de éxito básico

Piensa en una app que recupera artículos de noticias para que los lea un usuario. La app tiene una pantalla que presenta los artículos que están disponibles para leer y también les permite a los usuarios que accedan marcar como favoritos los que más les interesan. Por otro lado, como puede haber muchos artículos en cualquier momento, el lector debe ser capaz de explorar por categoría. En resumen, la app les permite a los usuarios hacer lo siguiente:

  • Ver artículos disponibles para leer
  • Explorar artículos por categoría
  • Acceder y marcar artículos como favoritos
  • Acceder a algunas funciones premium si cumplen con los requisitos correspondientes
Figura 2: Una aplicación de noticias de muestra para un caso de éxito de la IU

En las siguientes secciones, se usa este ejemplo como caso de éxito en el que se presentan los principios del flujo unidireccional de datos y se ilustran los problemas que estos principios ayudan a resolver en el contexto de la arquitectura de la app para la capa de la IU.

Arquitectura de la capa de la IU

El término IU hace referencia a los elementos de la IU, como las actividades y los fragmentos que muestran los datos, independientemente de las APIs que usen en sus tareas (vistas o Jetpack Compose). Debido a que la función de la capa de datos es retener, administrar y proporcionar acceso a los datos de la app, la capa de la IU debe realizar los siguientes pasos:

  1. Consumir los datos de la app y transformarlos en datos que la IU pueda renderizar con facilidad
  2. Consumir los datos que se pueden renderizar en la IU y transformarlos en elementos de la IU para presentarlos al usuario
  3. Consumir eventos de entrada del usuario a partir de esos elementos de la IU ensamblados y reflejar sus efectos en los datos de la IU según sea necesario
  4. Repetir los pasos 1 al 3 durante el tiempo que sea necesario

En el resto de esta guía, se muestra cómo implementar una capa de IU que realice estos pasos. En particular, en esta guía, se abarcan las siguientes tareas y conceptos:

  • Cómo definir el estado de la IU
  • El flujo unidireccional de datos (UDF) como medio para producir y administrar el estado de la IU
  • Cómo exponer el estado de la IU con tipos de datos observables según los principios del flujo unidireccional de datos
  • Cómo implementar una IU que consuma su estado observable

El más importante de todos es la definición del estado de la IU.

Cómo definir el estado de la IU

Consulta el caso de éxito que se describió anteriormente. En resumen, la IU muestra una lista de artículos junto con algunos metadatos para cada uno. Esta información que la app presenta al usuario es el estado de la IU.

En otras palabras, si la IU es lo que ve el usuario, el estado de la IU es lo que la app dice que debería ver. Al igual que dos caras de una moneda, la IU es la representación visual del estado de la IU. Cualquier cambio en el estado de la IU se refleja de inmediato en la IU.

La IU es el resultado de la vinculación de sus elementos en la pantalla con el estado correspondiente.
Figura 3: La IU es el resultado de la vinculación de sus elementos en la pantalla con el estado correspondiente.

Considera este caso de éxito: para cumplir con los requisitos de la app de Noticias, la información necesaria para renderizar por completo la IU se puede encapsular en una clase de datos NewsUiState definida de la siguiente manera:

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

Inmutabilidad

La definición del estado de la IU en el ejemplo anterior es inmutable. El beneficio clave de esto es que los objetos inmutables proporcionan garantías sobre el estado de la aplicación en un momento determinado. De esta manera, se libera la IU para enfocarse en una sola función: leer el estado y actualizar sus elementos según corresponda. Como resultado, nunca debes modificar el estado de la IU directamente en la IU, a menos que la IU sea la única fuente de datos. Infringir este principio genera varias fuentes verídicas para la misma información, lo que genera inconsistencias en los datos y errores leves.

Por ejemplo, si la marca bookmarked en un objeto NewsItemUiState del estado de la IU en el caso de éxito se actualiza en la clase Activity, esa marca competiría con la capa de datos como fuente del estado "agregado a favoritos" de un artículo. Las clases de datos inmutables son muy útiles para evitar este tipo de antipatrón.

Convenciones de nombres en esta guía

En esta guía, las clases de estado de la IU se nombran según la funcionalidad de la pantalla o parte de la pantalla que describen. La convención es la siguiente:

funcionalidad + UiState

Por ejemplo, el estado de una pantalla que muestra noticias podría llamarse NewsUiState, y el estado de un elemento de noticias en una lista de noticias podría ser NewsItemUiState.

Cómo administrar el estado con un flujo unidireccional de datos

En la sección anterior, se estableció que el estado de la IU es una instantánea inmutable de los detalles necesarios para la renderización de la IU. Sin embargo, debido a la naturaleza dinámica de los datos en las apps, el estado puede cambiar con el tiempo. Esto podría deberse a la interacción del usuario o a otros eventos que modifiquen los datos subyacentes que se usan para propagar la app.

Estas interacciones pueden beneficiarse de un mediador para procesarlas, definir la lógica que se aplicará a cada evento y realizar las transformaciones necesarias en las fuentes de datos de respaldo para crear el estado de la IU. Estas interacciones y su lógica pueden estar alojadas en la IU, pero podrían volverse difíciles de controlar cuando la IU comienza a ser más de lo que su nombre sugiere: se convierte en propietario, productor, transformador y mucho más. Además, esto puede afectar la capacidad de prueba porque el código resultante es una amalgama de acoplamiento alto y sin límites discernibles. En última instancia, la IU se beneficia de una reducción de carga. A menos que el estado de la IU sea muy simple, la única responsabilidad de la IU será el consumo y la visualización del estado de la IU.

En esta sección, se analiza el flujo unidireccional de datos, un patrón de arquitectura que ayuda a aplicar esta separación saludable de responsabilidades.

Contenedores de estado

Las clases que son responsables de la producción del estado de la IU y contienen la lógica necesaria para esa tarea se denominan contenedores de estado. Los contenedores de estado están disponibles en una variedad de tamaños, según el alcance de los elementos correspondientes de la IU que administran, lo que comprende desde un solo widget, como la barra inferior de una app, hasta una pantalla completa o un destino de navegación.

En el último caso, la implementación típica es una instancia de un ViewModel, aunque, según los requisitos de la aplicación, una clase simple puede ser suficiente. La app de Noticias del caso de éxito, por ejemplo, usa una clase NewsViewModel como contenedor de estado para producir el estado de la IU de la pantalla que se muestra en esa sección.

Hay muchas maneras de modelar la codependencia entre la IU y su productor de estado. Sin embargo, debido a que la interacción entre la IU y su clase ViewModel se puede comprender en gran medida como una entrada de evento y su resultado de estado posterior, la relación se puede representar como se muestra en el siguiente diagrama:

Los datos de la aplicación fluyen desde la capa de datos hasta el ViewModel. El estado de la IU fluye desde el ViewModel hasta los elementos de la IU, y los eventos fluyen de los elementos de la IU de vuelta al ViewModel.
Figura 4: Diagrama de cómo funciona el flujo unidireccional de datos en la arquitectura de la app

El patrón en el que el estado fluye hacia abajo y los eventos fluyen hacia arriba se denomina flujo unidireccional de datos. Las implicaciones de este patrón para la arquitectura de la app son las siguientes:

  • ViewModel conserva y expone el estado que consumirá la IU. El estado de la IU son los datos de aplicación que transforma ViewModel.
  • La IU notifica al ViewModel los eventos de usuario.
  • ViewModel controla las acciones del usuario y actualiza el estado.
  • El estado actualizado se envía a la IU para su renderización.
  • El proceso anterior se repite para cualquier evento que cause una mutación del estado.

Para pantallas o destinos de navegación, ViewModel funciona con repositorios o clases de caso de uso para obtener datos y transformarlos en el estado de la IU, a la vez que incorpora los efectos de los eventos que pueden causar mutaciones del estado. El caso de éxito mencionado anteriormente contiene una lista de artículos, cada uno con un título, una descripción, una fuente, un nombre de autor, la fecha de publicación y si se agregó a favoritos. La IU de cada elemento de artículo se ve así:

Figura 5: IU de un elemento de artículo en la app del caso de éxito

Un usuario que solicita agregar un artículo a favoritos es un ejemplo de un evento que puede generar mutaciones del estado. Como productor de estado, la responsabilidad del ViewModel es definir toda la lógica necesaria para propagar todos los campos en el estado de la IU y procesar los eventos necesarios para que la IU se renderice por completo.

Un evento de IU se produce cuando el usuario agrega un artículo a favoritos. ViewModel notifica a la capa de datos sobre el cambio de estado. La capa de datos conserva los cambios de datos y actualiza los datos de la aplicación. Los datos de app nuevos con el artículo agregado a favoritos se pasan al ViewModel, que produce el nuevo estado de IU y lo pasa a los elementos de la IU para su visualización.
Figura 6: Diagrama que ilustra el ciclo de eventos y datos en el flujo unidireccional de datos

En las siguientes secciones, se analizan con más detalle los eventos que causan cambios de estado y cómo se pueden procesar mediante un flujo unidireccional de datos.

Tipos de lógica

Agregar un artículo a favoritos es un ejemplo de lógica empresarial porque aporta valor a tu app. Para obtener más información, consulta la página sobre capa de datos. Sin embargo, hay distintos tipos de lógica que es importantes definir:

  • La lógica empresarial es la implementación de los requisitos del producto para los datos de app. Como ya se mencionó, un ejemplo es agregar a favoritos un artículo en la app del caso de éxito. Por lo general, la lógica empresarial se ubica en las capas de dominio o datos, pero nunca en la capa de la IU.
  • La lógica de comportamiento de la IU o lógica de la IU es cómo se muestran los cambios de estado en la pantalla. Algunos ejemplos incluyen obtener el texto adecuado para mostrar en la pantalla con Android Resources, navegar a una pantalla en particular cuando el usuario hace clic en un botón o mostrar un mensaje en pantalla con un aviso o una barra de notificaciones.

La lógica de la IU, en especial cuando implica tipos de IU como Context, debería encontrarse en la IU, no en el ViewModel. Si la IU aumenta su complejidad y deseas delegar su lógica a otra clase para favorecer la capacidad de prueba y la separación de los problemas, puedes crear una clase simple como contenedor de estado. Las clases simples creadas en la IU pueden tomar dependencias del SDK de Android porque siguen el ciclo de vida de la IU. Los objetos del ViewModel tienen una vida útil más larga.

Para obtener más información sobre los contenedores de estado y cómo se ajustan al contexto de la ayuda de la IU de compilación, consulta la Guía de estado de Jetpack Compose.

¿Por qué usar el flujo unidireccional de datos?

El flujo unidireccional de datos modela el ciclo de producción del estado, como se muestra en la Figura 4. También separa el lugar donde se originan los cambios de estado, el lugar en el que se transforman y el lugar donde se consumen. Esta separación permite que la IU haga exactamente lo que su nombre implica: mostrar información observando cambios de estado y retransmitiendo la intención del usuario pasando esos cambios al ViewModel.

En otras palabras, el flujo unidireccional de datos permite lo siguiente:

  • Coherencia de los datos. Hay una sola fuente de información para la IU.
  • Capacidad de realizar pruebas. Como la fuente de estado está aislada, se puede probar de forma independiente de la IU.
  • Capacidad de mantenimiento. La mutación del estado sigue un patrón bien definido en el que las mutaciones son el resultado de los eventos de un usuario y las fuentes de datos con las que trabajan.

Cómo exponer el estado de la IU

Después de definir el estado de tu IU y determinar cómo administrarás la producción de ese estado, el siguiente paso es presentar el estado producido en la IU. Como usarás un flujo unidireccional de datos para administrar la producción del estado, puedes considerar que el estado producido es una transmisión. Es decir que se generarán varias versiones del estado a lo largo del tiempo. Como resultado, debes exponer el estado de la IU en un contenedor de datos observables, como LiveData o StateFlow. La idea es que la IU pueda reaccionar a cualquier cambio realizado en el estado sin tener que extraer de forma manual los datos directamente desde el ViewModel. Estos tipos también ofrecen el beneficio de tener siempre la versión más reciente del estado de la IU almacenada en caché, lo cual es útil para realizar un restablecimiento rápido del estado después de cambios de configuración.

Vistas

class NewsViewModel(...) : ViewModel() {

    val uiState: StateFlow<NewsUiState> = …
}

Compose

class NewsViewModel(...) : ViewModel() {

    val uiState: NewsUiState = …
}

Para ver una introducción a LiveData como contenedor de datos observable, consulta este codelab. También puedes ver una introducción similar a los flujos de Kotlin en Flujos de Kotlin en Android.

Cuando los datos expuestos a la IU son relativamente simples, vale la pena unirlos en un tipo de estado de IU porque así se transmite la relación entre la emisión del contenedor de estado y su pantalla o elemento de la IU asociados. Además, a medida que el elemento de la IU se vuelve más complejo, siempre es más fácil agregarlo a la definición del estado de la IU para adaptarlo a la información adicional necesaria para procesar ese elemento.

Una forma común de crear una transmisión de UiState es exponer una transmisión mutable de respaldo como una transmisión inmutable desde ViewModel, por ejemplo, exponer una MutableStateFlow<UiState> como StateFlow<UiState>.

Vistas

class NewsViewModel(...) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

Compose

class NewsViewModel(...) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

ViewModel puede exponer los métodos que mutan de forma interna el estado, lo que publica actualizaciones para que consuma la IU. Tomemos, por ejemplo, el caso en el que se debe realizar una acción asíncrona: se puede iniciar una corrutina a través de viewModelScope y el estado mutable se puede actualizar después de su finalización.

Vistas

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                 }
            }
        }
    }
}

Compose

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

   var uiState by mutableStateOf(NewsUiState())
        private set

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                uiState = uiState.copy(newsItems = newsItems)
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                val messages = getMessagesFromThrowable(ioe)
                uiState = uiState.copy(userMessages = messages)
            }
        }
    }
}

En el ejemplo anterior, la clase NewsViewModel intenta recuperar artículos para una categoría determinada y, luego, refleja el resultado del intento (ya sea éxito o error) en el estado de la IU, en el que la IU puede reaccionar de forma adecuada. Consulta la sección Mostrar errores en la pantalla para obtener más información sobre la resolución de errores.

Consideraciones adicionales

Además de la guía anterior, ten en cuenta lo siguiente cuando expongas el estado de la IU:

  • Un objeto de estado de la IU debería controlar los estados relacionados entre sí. Esta implementación genera menos inconsistencias y facilita la comprensión del código. Si expones la lista de elementos de noticias y la cantidad de favoritos en dos transmisiones diferentes, podrías terminar en una situación en la que una se actualice y la otra no. Si usas una sola transmisión, todos los elementos se mantendrán actualizados. Además, parte de la lógica empresarial podría requerir una combinación de fuentes. Por ejemplo, es posible que debas mostrar un botón de favoritos solo si el usuario accede a su cuenta y está suscrito a un servicio de noticias premium. Podrías definir una clase de estado de IU de la siguiente manera:

    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
    
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium
    

    En esta declaración, la visibilidad del botón de favoritos es una propiedad derivada de otras dos. A medida que la lógica empresarial se vuelve más compleja, tener una clase UiState singular en la que todas las propiedades estén disponibles de inmediato se vuelve cada vez más importante.

  • Estados de la IU: ¿una o varias transmisiones? El principio guía clave para elegir entre exponer el estado de la IU en una sola o en varias transmisiones es la viñeta anterior: la relación entre los elementos emitidos. La mayor ventaja de una exposición de una sola transmisión es la comodidad y la coherencia de los datos: los consumidores de estado siempre tienen la información más reciente disponible en cualquier momento. Sin embargo, hay instancias en las que tener transmisiones de estado separadas del ViewModel podría ser apropiado:

    • Tipos de datos no relacionados: Algunos estados necesarios para procesar la IU podrían ser completamente independientes entre sí. En casos como estos, los costos de agrupar estos estados dispares podrían superar los beneficios, en especial si uno de estos estados se actualiza con más frecuencia que otro.

    • Diffing de UiState: Cuantos más campos haya en un objeto UiState, más probable será que la transmisión emita contenido como resultado de la actualización en uno de sus campos. Debido a que las vistas no tienen un mecanismo de diferenciación para comprender si las emisiones consecutivas son distintas o iguales, cada emisión genera una actualización para la vista. Por lo tanto, podría requerirse una mitigación con las APIs o los métodos de Flow, como distinctUntilChanged() en LiveData.

Cómo consumir el estado de la IU

A fin de consumir el flujo de objetos UiState en la IU, aplica el operador de terminal para el tipo de datos observable que usas. Por ejemplo, para LiveData, usa el método observe(), y para flujos de Kotlin, usa el método collect() o sus variantes.

Cuando uses contenedores de datos observables en la IU, asegúrate de tener en cuenta el ciclo de vida de la IU. Esto es importante porque la IU no debería observar su estado cuando la vista no se le muestra al usuario. Para obtener más información sobre este tema, consulta esta entrada de blog. Cuando usas LiveData, LifecycleOwner se encarga de forma implícita de los asuntos del ciclo de vida. Cuando se usan flujos, es mejor controlarlos con el permiso de corrutina correcto y la API de repeatOnLifecycle:

Vistas

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Compose

@Composable
fun LatestNewsScreen(
    viewModel: NewsViewModel = viewModel()
) {
    // Show UI elements based on the viewModel.uiState
}

Cómo mostrar las operaciones en curso

Una forma sencilla de representar estados de carga en una clase UiState es con un campo booleano:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

El valor de esta marca representa la presencia o ausencia de una barra de progreso en la IU.

Vistas

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

Compose

@Composable
fun LatestNewsScreen(
    modifier: Modifier = Modifier,
    viewModel: NewsViewModel = viewModel()
) {
    Box(modifier.fillMaxSize()) {

        if (viewModel.uiState.isFetchingArticles) {
            CircularProgressIndicator(Modifier.align(Alignment.Center))
        }

        // Add other UI elements. For example, the list.
    }
}

Cómo mostrar errores en la pantalla

Mostrar errores en la IU es similar a mostrar operaciones en curso porque ambos se representan fácilmente con valores booleanos que denotan su presencia o ausencia. Sin embargo, los errores también pueden incluir un mensaje asociado para retransmitir al usuario, o bien para una acción asociada con ellos que reintente la operación fallida. Por lo tanto, mientras una operación en curso se carga o no, es posible que los estados de error necesiten modelarse con clases de datos que alojen los metadatos adecuados para el contexto de ese error.

Por ejemplo, considera el ejemplo de la sección anterior que mostraba una barra de progreso mientras se recuperaban los artículos. Si esta operación da como resultado un error, es posible que quieras mostrar uno o más mensajes al usuario en los que se detalle lo que salió mal.

data class Message(val id: Long, val message: String)

data class NewsUiState(
    val userMessages: List<Message> = listOf(),
    ...
)

Los mensajes de error se pueden presentar al usuario en forma de elementos de la IU, como barras de notificaciones. Y como esto se relaciona con la manera en que se producen y consumen los eventos de la IU, consulta la página Eventos de la IU para obtener más información.

Subprocesos y simultaneidad

Cualquier trabajo que se realice en un ViewModel debe ser seguro para llamar desde el subproceso principal. Esto se debe a que las capas de datos y dominio son responsables de mover el trabajo a un subproceso diferente.

Si un ViewModel realiza operaciones de larga duración, también es responsable de mover esa lógica a un subproceso en segundo plano. Las corrutinas de Kotlin son una excelente manera de administrar operaciones simultáneas, y los componentes de la arquitectura de Jetpack proporcionan compatibilidad integrada. Para obtener más información sobre el uso de corrutinas en apps para Android, consulta Corrutinas de Kotlin en Android.

Los cambios en la navegación de las apps suelen deberse a las emisiones que parecen eventos. Por ejemplo, después de que una clase SignInViewModel realiza un acceso, el UiState puede tener un campo isSignedIn establecido en true. Estos activadores deben consumirse tal como los que se abordan en la sección Cómo consumir el estado de la IU anterior, excepto que la implementación del consumo debe remitirse al componente Navigation.

Paging

La biblioteca de Paging se consume en la IU con un tipo llamado PagingData. Debido a que PagingData representa y contiene elementos que pueden cambiar con el tiempo (es decir, no es un tipo inmutable), no debe representarse en un estado de IU inmutable. En cambio, debes exponer estos datos desde el ViewModel de forma independiente en su propia transmisión. Consulta el codelab Android Paging para ver un ejemplo específico.

Animaciones

Para proporcionar transiciones de navegación de primer nivel y que se vean fluidas, puedes esperar a que la segunda pantalla cargue los datos antes de iniciar la animación. El framework de vista de Android proporciona hooks para retrasar las transiciones entre destinos de fragmentos con las APIs de postponeEnterTransition() y startPostponedEnterTransition(). Estas APIs garantizan que los elementos de la IU de la segunda pantalla (por lo general, una imagen recuperada de la red) estén listos para mostrarse antes de que la IU anime la transición a esa pantalla. Para obtener más detalles e información específica sobre la implementación, consulta la muestra de Android Motion.

Ejemplos

En los siguientes ejemplos de Google, se demuestra el uso de la capa de IU. Explóralos para ver esta guía en práctica: