Capa de la IU (Views)

Conceptos y la implementación de Jetpack Compose

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.

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.

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

    val uiState: StateFlow<NewsUiState> = 
}

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>.

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

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

    ...

}

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.

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)
                }
            }
        }
    }
}

Cómo consumir el estado de la IU

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:

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
                }
            }
        }
    }
}

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.

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 }
            }
        }
    }
}

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.