En este módulo trataremos varios aspectos importantes como son:
- La arquitectura de una Aplicación Android
- La gestión de la navegación en Compose
- Diseñar aplicaciones que respondan a los cambios de configuración y se adapten a los cambios de estado y configuración del dispositivo, así como gestionar las interrupciones que se producen en el uso de nuestra App.
- Testing y Logging
Video Wellcome to Unit 4 -> App Architecture; Navigation; Adaptive Layouts
Lecturas y enlaces relacionados:
- Jetpack Compose State Guideline (Repaso sobre el funcionamiento interno y buenas técnicas sobre el estado)
- Activity Lifecycle (Android developers) – Understanding Activity Lifecycle (codepath*)
- Cómo crear la arquitectura de tu IU de Compose
- App architecture (de Android Developers);
- Guide to app architecture (Android Developers)
- Principles of navigation (de Android Developers)
- Descripción general de ViewModel
Dessert Clicker: Introducción al Android Lifecycle:
Unidad 4 : Ruta de aprendizaje 1 -> TRAINING ->ANDROID BASICS WITH COMPOSE -> NAVIGATION AND ARCHITECTURE -> ARCHITECTURE
TODO: Realizar el codelab en Stages of the Activity lifecycle (Etapas del ciclo de vida de la actividad) –> Codelab
En GitHub :
Dessert Clicker starter branch
Dessert Clicker codelab branch
Note: An Android app can have multiple activities. However, it is recommended to have a single activity.
Esto es lo que se llama patrón single activity, una única activity y múltiples fragments. Veremos los fragments más a delante. En cualquier caso se pueden tener varias actividades siempre que la aplicación lo requiere.
Notas sobre persistencia del estado:
Composable functions have their own lifecycle that is independent of the Activity lifecycle.
Its lifecycle is composed of the events:
- Enters the Composition,
- Recomposing 0 or more times
- Leaving the Composition.
While Compose remembers the variable state during recompositions, it does not retain this state during a configuration change.
For Compose to retain the state during a configuration change, you must use rememberSaveable.
Use rememberSaveable to save values across configuration changes
Lecturas relacionadas
El código Starter de esta aplicación ya utiliza temas que es necesario revisar o leer, para entender en profundidad lo que nos dan como código de partida.
- El contexto
- Intents and Intent Filters (Android Developers), Intents comunes (Android Developers)
- Jetpack Compose: Implicit Intents, How to use Implicit Intents with Compose?
- Sending Emails
- Pick an Image from Gallery
Instrucciones adicionales al Codelab
Esta App parte de un código de inicio que nos dan, Podéis descargarlo de aqui pero fijarse que esté seleccionada la rama Starter
O bien de la rama Starter de mi GitHub DesertClicker
La app de inicio no funciona correctamente pues presenta errores por culpa de no tratar bien el Android lifecycle pero se irán corrigiendo en el codelab, por ejemplo en ciertas circunstancias la app resetea los valores a 0 y se pierde el estado.
Se os pedirán también ejercicios adicionales.
Hemos tratado:
- Logging – Utilizamos el LogCat para ver detalles de debug, ver View logs with Logcat
- Utilizamos los siguientes filtros en el Logcat (versíon nueva) : package:mine tag:MainActivity level:debug
- AppBar -> Share Button -> Crearmos un implicit intent que lanza un chooser para enviar información, en este caso texto plano.
- Intents and Intent Filters
- Toast -> Mostrar mensajes emergentes. Hay que capturar el contexto en el composable con Local.Context.current o recibirlo en los parámetros del composable
Lecturas recomendadas:
Unscramble: ViewModel StateHolder:
Unidad 4 : Ruta de aprendizaje 1 -> TRAINING ->ANDROID BASICS WITH COMPOSE -> NAVIGATION AND ARCHITECTURE -> ARCHITECTURE
TODO: Realizar el codelab en ViewModel and State in Compose –> Codelab
En GitHub :
Starter App: En el repositorio Unscramble podéis descargar la versión de incio para la App.
La funcionalidad no está implementada en la Starter App. En Mi Github podéis encontrar también la aplicación Unscramble
Navegación por Categorías: Prototipo simplificado con pantalla de selección de Categorías y como se resuelve la actualización en el ViewModel. UnscrambleWithCategoriesPrototype
En el Starter App:
- Define un package UI
- En el fichero de MainActivity se limita a tener el lanzador del interfaz de usuario para esa pantalla.
- La pantalla GameScreen.kt lo cloloca bajo el package ui.
- wrapContentWidth y .wrapContentWidth
- Arrangement.spacedBy -> Arrangement
- OutlinedTextField -> Texto en Compose
- OutlinedButton -> Material2 Buttons -> Material3 Buttons
- AlertDialog (Jetpack Compose Playground),
AddingAlertDialog
with Jetpack Compose to Android apps
Show custom alert dialog in Jetpack Compose - var activity = (LocalContext.current as Activity)
activity.finish()
Hemos tratado:
- WordsData.kt Contiene la lista de palabras.
- Ejercicio opcional: Cambiar la lista de palabras para poner una lista de palabras en castellano que podéis obtener de Aqui.
Más adelante podremos crear una base de datos de palabras desde la que cargar la lista de palabras.
- Ejercicio opcional: Cambiar la lista de palabras para poner una lista de palabras en castellano que podéis obtener de Aqui.
- Introduction to Kotlin Flow
- Flujos de Kotlin en Android No hemos visto todavía las corrutinas, pero podemos entender los flows sin necesdiad de las corrutinas.
- Kotlin flows in Jetpack Compose: the ultimate guide
- Using MutableStateFlow in Android
- StringResource con parámteros:
stringResource(R.string.word_count, wordCount)
stringResource(R.string.word_count, wordCount) <string name="word_count">%d of 10 words</string>
Dessert Clicker: ViewModel StateHolder:
Unidad 4 : Ruta de aprendizaje 1 -> TRAINING ->ANDROID BASICS WITH COMPOSE -> NAVIGATION AND ARCHITECTURE -> ARCHITECTURE
TODO: Realizar el codelab en Practice: Add a ViewModel to Dessert Clicker –> Codelab
En GitHub : Dessert Clicker (Viewmodel Branch)
Starter App: Partimos de la aplicación Dessert Clicker que hemos realizado anteriormente, con las instrucciones del codelab juntoc con estas
Instrucciones adicionales:
- (Step 4) Dentro del package data creamos la nueva clase UI State Class (Step 4) que pide el tutorial, que podemos llamar DessertUiState en este fichero colocaremos todas las variables que actualmente componen el estado en nuestro componente DessertClickerApp, en concreto revenue, dessertsSold, currentDessertIndex, currentDessertPrice y currentDessertImageId que serán parámetros inmutables (val) de nuestra DataClass DessertUiState
Recordar marcar con @DrawableRes el parámetro currentDessertImageId - (Step 5) Dentro del package ui creamos la clase ViewModel que podemos llamar DessertViewModel y que nos pide en el Step 5.
Crearemos la clase y editaremos para añadir la herencia de ViewModel(), importando por tanto androidx.lifecycle.ViewModel
En la vista Android al hacer click derecho y añadir el Data Class lo mete dentro the ui.theme, por lo que si queremos que cuelgue directamente del package ui y no de ui.theme tendremos que o bien moverlo, con refactor -> move, o bien crearlo directamente en la vista Project Files y seleccionar ui al hacer click derecho para añadir el Data Class. - (Step6) Pasamos la lógica al ViewModel
Primero creamos una variable privada _dessertStateFlow que es un MutableStateFlow al que se pasa como parametro nuestra Data Class DessertUiState
Luego creamos una variable pública dessertStateFlow que es la que se comparte cuyo tipo de datos es un StateFlow<DessertUiState> que se inicializa a _dessertStateFlow.asStateFlow()
Recordar que hemos creado una clase que guarda un estado (conjunto de variables que pueden cambiar).
Que podemos tener tantas clases de estado como conjuntos de variables van a gestionarse en distintos momentos y vistas de nuestra interfaz de usuario.
En este caso hemos creado sólo un estado, que hemos llamado DesssertUiState.
Y que un ViewModel es una clase que me ayuda a gestionar ese estado ofreciéndome métodos que llamaré para que hagan cosas con el estado o que en función del estado me devuelvan valores que me interesen… Es decir es una clase que me ofrece métodos para ser llamados en respuesta a eventos del usuario y a otras cosas que como programador necesito que me resuelva, ya que conoce bien el estado.
En nuestra App el estado cambia cuando el usuario hace click en el dibujo del dulce, por tanto nuestra clase DessertViewModel deberá ofrecer un método para ese evento, que llamaremos onDessertClicked()
En nuestro método onDessertClicked() tenemos pues que cambiar el estado y esto lo hacemos llamando al método update de la variable privada que mantiene el Flow de nuestro estado, es decir a nuestra variable _dessertStateFlow.
LLamamos por tanto a update() (método de la clase StateFlow), es decir llamamos a _dessertStatFlow.update() que es un Lambda que recibe un estado y devuelve un estado. El estado que recibe es del tipo que se puso en su creación, en nuestro caso un DessertUiState, por lo que recibe un objeto de ese tipo y devuelve un objeto de este tipo, pero cambiando lo que necesite en el código del lamba que ejecutamos cuando el usuario lance el evento (haga click).
Como ya sabemos de los lambdas de kotlin, en el código del lamda, el parametro lo podemos llamar como queramos o no ponerle nombre en cuyo caso lo referenciaremos como it. Podemos llarmarlo postreState ->
Nota: Al abrir las llaves { para poner el código de nuestro lambda onDessertClicked() Android Studio nos ofrece la posibilidad de crear una función de extensión, podemos hacerlo u obviar el mensaje y hacer lo que tenemos que hacer en nuestro código. Hasta que no pongamos postreState.copy para darle valor al objeto de retorno no parará de mostar las lineas rojas de posible error.
Lo que tiene que hacer nuestro onDessertClicked() por tanto es modificar el valor del estado que recibe, eso lo hacemos con el operador copia, es decir con postreSate.copy( …. ) y en los parámetros vamos diciendo cómo cambia cada una de las variables de nuestro data class DessertUiState.
Cuando queremos cambiar el valor de currentDessertIndex nos damos cuenta que necesitamos una lógica que determine el siguiente estado, por eso definimos antes del postreState.copy una variable nextIndex (por ejemplo) que llamará a una función de nuestro ViewModel que sepa cual es el siguiente índice.
Creamos pues la función determineDessertIndex que recibe un parametro Int (el número de postres vendidos) y devuelve un Int, el nuevo índice del array de datos que corresponde con el postre a mostrar.
- (Step 7) Usar el DessertViewModel
Tendremos que crear una instancia del DessertViewModel en algún sitio que perdure a la rotación del dispositivo…. ¿donde? Podemos colocarlo como variable global de nuestro módulo MainActivity (fuera de la propia activity, pues esta se destruye).
Entonces en el SetContent() podemos llamar a DessertClickerApp(dessertViewModel) pasándole la variable global de tipo DessertViewModel que hemos declarado global.
(Step7) Forma correcta.
En la forma anterior, se crea una variable global que tiene el ViewModel, pero si revisamos el tutorial antrior, vemos que no es necesario, que se puede pasar como parámetro que se inicializa genéricamente con viewModel().
Finalmente el prototipo de nuestra función principal queda así:
@Composable private fun DessertClickerApp( dessertViewModel: DessertViewModel = viewModel() ) {
En el SetContent() no pasamos el parámetro, con lo que se ejecuta la inicalización del por defecto con =viewModel()
Que según la ayuda contextual al colocarnos encima dice:
Returns an existing ViewModel or creates a new one in the given owner (usually, a fragment or an activity), defaulting to the owner provided by LocalViewModelStoreOwner.
The created ViewModel is associated with the given viewModelStoreOwner and will be retained as long as the owner is alive (e.g. if it is an activity, until it is finished or process is killed).
Es decir, hay un elemento en Android que se llama LocalViewModelStoreOwner que básicament lo que hace es crear un ViewModel del tipo indicado en la declaración si no está creado ya. Es decir crea una instancia única de un DessertViewModel. Única porque cuando alguien más en otra parte de la aplicación pida un viewModel para un DessertViewModel, nuestro amigo el LocalViewModelStoreOwner devolverá el mismo objeto DessertViewModel que creó la primera vez, por lo que los datos que se ven serán los mismos se llamen desde donde se llamen. Esto veremos que es útil cuando trabajemos con fragments y navegación.
Por toro lado el LocalViewModelStoreOwner asigna ese viewModel al ciclo de vida del propietario al que se vincula en su creación, en nuestro caso a una Activity, puesto que nuestra función DessertClickerApp se está llamando desde el onCreate de una Activity. De forma que nuetro amigo el LocalViewModelStoreOwner guarda el viewModel durante todo el ciclo de vida de la activity hasta que se destruye finalmente, perdurando a los cambios de rotación y asignandoselo de nuevo a la nueva Activity que se crea tras la rotación en el onRestart().