En una aplicación de Compose, la elevación del estado de la IU depende de si la lógica empresarial o la lógica de la IU lo requieren. En este documento, se presentan estas dos situaciones principales.
Práctica recomendada
Debes elevar el estado de la IU al principal común más bajo entre todos los elementos componibles que lo leen y escriben. Además, debes mantener el estado más cercano al lugar donde se consume. Desde el propietario del estado, expón a los consumidores el estado y los eventos inmutables para modificar el estado.
El principal común más bajo también puede estar fuera de la composición. Por ejemplo, cuando elevas el estado en un elemento ViewModel
porque se involucra en la lógica empresarial.
En esta página, se explica esta práctica recomendada en detalle, y se deben tener en cuenta las advertencias.
Los tipos de estado y lógica de la IU
A continuación, se muestran las definiciones de los tipos de estado y lógica de la IU que se usan en todo este documento.
Estado de la IU
El estado de la IU es la propiedad que describe la IU. Existen dos tipos de estados de la IU:
- El estado de la IU de la pantalla es aquello que necesitas mostrar en la pantalla. Por ejemplo, una clase
NewsUiState
puede contener los artículos de noticias y otra información necesaria para renderizar la IU. Por lo general, este estado se conecta con otras capas de la jerarquía porque incluye datos de app. - El estado de un elemento de la IU hace referencia a propiedades intrínsecas a los elementos de la IU que influyen en la forma en que se renderizan. Un elemento de la IU se puede ocultar o mostrar, y puede tener una fuente determinada, con cierto tamaño o color. En las vistas de Android, la Vista es la que administra este estado, ya que es un elemento inherentemente con estado, y expone métodos para modificar o consultar su estado. Un ejemplo de esto son los métodos
get
yset
de la claseTextView
para su texto. En Jetpack Compose, el estado es externo al elemento que admite composición, y hasta puedes elevarlo fuera de las inmediaciones de ese elemento hasta la función de componibilidad que realiza la llamada o hasta un contenedor de estado. Un ejemplo esScaffoldState
para el elementoScaffold
que admite composición.
Lógica
La lógica en una aplicación puede ser lógica empresarial o de IU:
- La lógica empresarial es la implementación de los requisitos del producto para los datos de app. Por ejemplo, agregar un artículo a favoritos en una app de lectura de noticias cuando el usuario presione el botón. Por lo general, esta lógica para guardar un favorito en un archivo o una base de datos se coloca en las capas de dominio o de datos. El contenedor de estado suele delegar esta lógica a esas capas llamando a los métodos que exponen.
- La lógica de la IU está relacionada con cómo se muestra el estado de la IU en la pantalla. Por ejemplo, obtener la sugerencia correcta de la barra de búsqueda cuando el usuario selecciona una categoría, desplazarse a un elemento determinado de una lista o establecer la lógica de navegación a una pantalla determinada cuando el usuario hace clic en un botón.
Lógica de la IU
Cuando la lógica de la IU necesita leer o escribir el estado, debes definir este último en función de la IU, de acuerdo con su ciclo de vida. Para ello, debes elevar el estado al nivel correcto en una función de componibilidad. Como alternativa, puedes hacerlo en una clase de contenedor de estado sin formato, también centrada en el ciclo de vida de la IU.
A continuación, ofrecemos una descripción de las soluciones y las explicaciones sobre cuándo utilizarlas.
Elementos componibles como propietario del estado
Contar con la lógica de la IU y el estado de sus elementos en objetos componibles es un buen enfoque si el estado y la lógica son simples. Puedes dejar tu estado interno a un elemento que admite composición o elevarlo según sea necesario.
No se necesita la elevación de estado
No siempre se requiere el estado de elevación. El estado puede ser interno en un elemento componible cuando ningún otro elemento necesita controlarlo. En este fragmento, hay un elemento componible que se expande y se contrae cuando se presiona:
@Composable fun ChatBubble( message: Message ) { var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state ClickableText( text = AnnotatedString(message.content), onClick = { showDetails = !showDetails } // Apply simple UI logic ) if (showDetails) { Text(message.timestamp) } }
La variable showDetails
es el estado interno de este elemento de la IU. Solo se lee y modifica en este elemento componible, y la lógica que se le aplica es muy simple.
Por lo tanto, la elevación del estado en este caso no generaría muchos beneficios y es posible dejarlo interno. Si lo haces, el elemento componible es propietario y la única fuente de información del estado expandido.
Elevación dentro de elementos componibles
Si necesitas compartir el estado de tu elemento de la IU con otros elementos componibles y aplicar su lógica en diferentes lugares, puedes elevarlo más en la jerarquía de la IU. También permite que se vuelvan a utilizar más, y será más fácil probarlos.
El siguiente ejemplo es una app de chat que implementa dos funciones:
- El botón
JumpToBottom
desplaza la lista de mensajes hasta la parte inferior. El botón realiza la lógica de la IU en el estado de la lista. - La lista
MessagesList
se desplaza hasta la parte inferior después de que el usuario envía mensajes nuevos. UserInput realiza la lógica de la IU en el estado de la lista.
![App de chat que muestra el botón JumpToBottom y un desplazamiento hasta la parte inferior para ver mensajes nuevos](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-chat.png?authuser=7&hl=es)
JumpToBottom
y desplázate hasta el final para ver los mensajes nuevosLa jerarquía componible es la siguiente:
![Árbol de chat componible](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-initial-tree.png?authuser=7&hl=es)
El estado LazyColumn
se eleva a la pantalla de la conversación, de modo que la app pueda realizar la lógica de la IU y leer el estado de todos los elementos componibles que lo requieran:
![El estado LazyColumn se eleva desde LazyColumn hasta ConversationScreen](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-animated.gif?authuser=7&hl=es)
LazyColumn
de LazyColumn
a ConversationScreen
Por último, los elementos componibles son los siguientes:
![Árbol de chat componible con LazyListState elevado a ConversationScreen](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-passing-state.png?authuser=7&hl=es)
LazyListState
elevado a ConversationScreen
El código es el siguiente:
@Composable private fun ConversationScreen(/*...*/) { val scope = rememberCoroutineScope() val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen MessagesList(messages, lazyListState) // Reuse same state in MessageList UserInput( onMessageSent = { // Apply UI logic to lazyListState scope.launch { lazyListState.scrollToItem(0) } }, ) } @Composable private fun MessagesList( messages: List<Message>, lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value ) { LazyColumn( state = lazyListState // Pass hoisted state to LazyColumn ) { items(messages, key = { message -> message.id }) { item -> Message(/*...*/) } } val scope = rememberCoroutineScope() JumpToBottom(onClicked = { scope.launch { lazyListState.scrollToItem(0) // UI logic being applied to lazyListState } }) }
LazyListState
se eleva tanto como sea necesario para la lógica de la IU que se debe aplicar. Como se inicializa en una función de componibilidad, se almacena en la composición y sigue su ciclo de vida.
Ten en cuenta que lazyListState
se define en el método MessagesList
, con el valor predeterminado de rememberLazyListState()
. Este es un patrón común en Compose.
Permite que los elementos componibles sean flexibles y se puedan utilizar más. Luego, puedes usar el elemento componible en diferentes partes de la app que quizás no necesites controlar el estado. Por lo general, este es el caso cuando se realiza una prueba o se obtiene una vista previa de un elemento componible. Así es exactamente
LazyColumn
define su estado.
![El principal común más bajo para LazyListState es ConversationScreen.](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-lca.png?authuser=7&hl=es)
LazyListState
es ConversationScreen
Clase de contenedor de estado sin formato como propietario del estado
Cuando un elemento componible contiene una lógica de IU compleja que involucra uno o varios campos de estado de un elemento de la IU, debe delegar esa responsabilidad a los contenedores de estado, como una clase de contenedor de estado sin formato. De esta manera, se permite probar, de forma aislada, la lógica del elemento componible y se reduce su complejidad. Este enfoque favorece el principio de separación de problemas: el elemento componible está a cargo de emitir los elementos de la IU y el contenedor del estado incluye la lógica de la IU y el estado de sus elementos.
Las clases de contenedores de estado sin formato proporcionan funciones convenientes a los llamadores de la función de componibilidad, de modo que no tienen que escribir esta lógica ellos mismos.
Estas clases sin formato se crean y recuerdan en la composición. Como siguen el ciclo de vida del elemento componible, pueden tomar los tipos que proporciona la biblioteca de Compose, como rememberNavController()
o rememberLazyListState()
.
Un ejemplo de esto es la clase de contenedor de estado sin formato LazyListState
, que se implementa en Compose para controlar la complejidad de la IU de LazyColumn
o LazyRow
.
// LazyListState.kt @Stable class LazyListState constructor( firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0 ) : ScrollableState { /** * The holder class for the current scroll position. */ private val scrollPosition = LazyListScrollPosition( firstVisibleItemIndex, firstVisibleItemScrollOffset ) suspend fun scrollToItem(/*...*/) { /*...*/ } override suspend fun scroll() { /*...*/ } suspend fun animateScrollToItem() { /*...*/ } }
LazyListState
encapsula el estado de LazyColumn
que almacena scrollPosition
para este elemento de la IU. También expone métodos para modificar la posición de desplazamiento, por ejemplo, con el desplazamiento a un elemento determinado.
Como puedes ver, aumentar las responsabilidades de un elemento componible incrementa la necesidad de un contenedor de estado. Las responsabilidades pueden estar en la lógica de la IU o solo en la cantidad de estado del que se debe realizar un seguimiento.
Otro patrón común es usar una clase de contenedor de estado sin formato para controlar la complejidad de las funciones raíz de componibilidad en la app. Puedes usar esa clase para encapsular el estado a nivel de la app, como el estado de navegación y el tamaño de la pantalla. Puedes encontrar una descripción completa en la página sobre la lógica de la IU y el contenedor de estado.
Lógica empresarial
Si las clases de elementos componibles y los contenedores de estado sin formato están a cargo de la lógica de la IU y el estado del elemento de la IU, un contenedor de estado en el nivel de la pantalla está a cargo de las siguientes tareas:
- Brindar acceso a la lógica empresarial de la aplicación (por lo general, se ubica en otras capas de la jerarquía, como las capas empresariales y de datos)
- Preparar los datos de la aplicación para mostrarlos en una pantalla particular (que se convierte en el estado de la IU de la pantalla)
ViewModels como propietario del estado
Los beneficios de AAC ViewModels en el desarrollo de Android permiten que sean adecuados para brindar acceso a la lógica empresarial y preparar los datos de la aplicación para mostrarlos en la pantalla.
Cuando elevas el estado de la IU en ViewModel
, lo quitas de la composición.
![El estado elevado al ViewModel se almacena fuera de la composición.](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-vm.png?authuser=7&hl=es)
ViewModel
se almacena fuera de la composición.Los ViewModel no se almacenan como parte de la composición. El framework los proporciona, y se limitan a ViewModelStoreOwner
, que puede ser una actividad, un fragmento, un gráfico de navegación o el destino de un gráfico de navegación. Para obtener más información sobre los alcances de ViewModel
, puedes consultar la documentación.
ViewModel
es la fuente de información y el principal común más bajo para el estado de la IU.
Estado de la IU de la pantalla
Según las definiciones anteriores, se produce el estado de la IU de la pantalla con la aplicación de reglas empresariales. Como el contenedor de estado en el nivel de la pantalla es responsable de esta aplicación, el estado de la IU de la pantalla, por lo general, se eleva en el contenedor de estado en este nivel, en este caso, un ViewModel
.
Considera ConversationViewModel
de una app de chat y cómo expone el estado de la IU de la pantalla y los eventos para modificarla:
class ConversationViewModel( channelId: String, messagesRepository: MessagesRepository ) : ViewModel() { val messages = messagesRepository .getLatestMessages(channelId) .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = emptyList() ) // Business logic fun sendMessage(message: Message) { /* ... */ } }
Los elementos componibles consumen el estado de la IU de la pantalla que se elevó en ViewModel
. Debes insertar la instancia de ViewModel
en los elementos componibles en el nivel de la pantalla para brindar acceso a la lógica empresarial.
A continuación, se muestra un ejemplo de un ViewModel
que se usa en un elemento componible en el nivel de la pantalla:
Estos son los elementos ConversationScreen()
componibles que consumen el estado de la IU de la pantalla que se elevan en ViewModel
:
@Composable private fun ConversationScreen( conversationViewModel: ConversationViewModel = viewModel() ) { val messages by conversationViewModel.messages.collectAsStateWithLifecycle() ConversationScreen( messages = messages, onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) } ) } @Composable private fun ConversationScreen( messages: List<Message>, onSendMessage: (Message) -> Unit ) { MessagesList(messages, onSendMessage) /* ... */ }
Desglose de propiedades
El "desglose de propiedades" alude a pasar datos a través de varios componentes secundarios anidados a la ubicación en la que se leyeron.
Un ejemplo típico de dónde puede aparecer el desglose de propiedades en Compose es cuando insertas en el nivel superior el contenedor de estado en el nivel de la pantalla y pasas el estado y los eventos a elementos secundarios componibles. Además, podría generar una sobrecarga de firmas de funciones de componibilidad.
Si bien exponer eventos como parámetros lambda individuales podría sobrecargar la firma de la función, maximiza la visibilidad de cuáles son las responsabilidades de la función de componibilidad. Puedes dar un vistazo para ver lo que hace.
Se recomienda el desglose de propiedades en lugar de crear clases de wrapper para encapsular estados y eventos en un solo lugar, ya que reduce la visibilidad de las responsabilidades componibles. Si no tienes clases de wrapper, es más probable que le pases solo los elementos componibles a los parámetros que necesitan, lo que es una práctica recomendada.
La misma práctica recomendada se aplica si estos eventos son de navegación. Puedes obtener más información sobre este tema en los documentos de navegación.
Si identificaste un problema de rendimiento, también puedes optar por diferir la lectura del estado. Consulta los documentos de rendimiento para obtener más información.
Estado del elemento de la IU
Puedes elevar el estado del elemento de la IU al contenedor de estado en el nivel de la pantalla si hay una lógica empresarial que necesite leerlo o escribirlo.
Para continuar con el ejemplo de una app de chat, la app muestra las sugerencias del usuario en un chat en grupo cuando el usuario escribe @
y una sugerencia. Esas sugerencias provienen de la capa de datos, y la lógica para calcular una lista de sugerencias de usuarios se considera empresarial. La función se ve de la siguiente manera:
![Función que muestra sugerencias de usuarios en un chat en grupo cuando el usuario escribe "@" y una sugerencia](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-suggestions.png?authuser=7&hl=es)
@
y una sugerenciaViewModel
que implementa esta función se verá de la siguiente manera:
class ConversationViewModel(/*...*/) : ViewModel() { // Hoisted state var inputMessage by mutableStateOf("") private set val suggestions: StateFlow<List<Suggestion>> = snapshotFlow { inputMessage } .filter { hasSocialHandleHint(it) } .mapLatest { getHandle(it) } .mapLatest { repository.getSuggestions(it) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = emptyList() ) fun updateInput(newInput: String) { inputMessage = newInput } }
inputMessage
es una variable que almacena el estado TextField
. Cada vez que el usuario escribe una entrada nueva, la app llama a la lógica empresarial para producir suggestions
.
suggestions
es el estado de la IU de la pantalla y se consume desde la IU de Compose mediante la recopilación desde StateFlow
.
Advertencias
Para algunos estados de elementos de la IU de Compose, elevar a ViewModel
puede requerir consideraciones especiales. Por ejemplo, algunos contenedores de estado de elementos de la IU de Compose exponen métodos para modificar el estado. Algunos de estos podrían ser funciones de suspensión que activan animaciones. Estas funciones de suspensión pueden generar excepciones si las llamas desde un CoroutineScope
que no está dentro del alcance de la composición.
Supongamos que el contenido del panel de apps es dinámico y que debes recuperarlo y actualizarlo desde la capa de datos después de cerrarlo. Debes elevar el estado del panel lateral a ViewModel
, de modo que puedas llamar a la IU y la lógica empresarial de este elemento desde el propietario del estado.
Sin embargo, llamar al método close()
de DrawerState
mediante viewModelScope
de la IU de Compose genera una excepción de tiempo de ejecución del tipo IllegalStateException
con un mensaje que dice "a MonotonicFrameClock
is not available in this CoroutineContext”
.
Para solucionar este problema, usa un CoroutineScope
que se limita a la composición. Proporciona MonotonicFrameClock
en CoroutineContext
que es necesario para que se puedan usar las funciones de suspensión.
Para solucionar esta falla, cambia CoroutineContext
de la corrutina en ViewModel
a uno que se limite a la composición. Puede verse de la siguiente manera:
class ConversationViewModel(/*...*/) : ViewModel() { val drawerState = DrawerState(initialValue = DrawerValue.Closed) private val _drawerContent = MutableStateFlow(DrawerContent.Empty) val drawerContent: StateFlow<DrawerContent> = _drawerContent.asStateFlow() fun closeDrawer(uiScope: CoroutineScope) { viewModelScope.launch { withContext(uiScope.coroutineContext) { // Use instead of the default context drawerState.close() } // Fetch drawer content and update state _drawerContent.update { content } } } } // in Compose @Composable private fun ConversationScreen( conversationViewModel: ConversationViewModel = viewModel() ) { val scope = rememberCoroutineScope() ConversationScreen(onCloseDrawer = { conversationViewModel.closeDrawer(uiScope = scope) }) }
Más información
Para obtener más información sobre el estado y Jetpack Compose, consulta los siguientes recursos adicionales.
Ejemplos
Codelabs
Videos
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Cómo guardar el estado de la IU en Compose
- Listas y cuadrículas
- Cómo crear la arquitectura de tu IU de Compose