Compose - Efecte

  • Durant la composició de la interfície d'usuari pot ser necessari executar accions no relacionades amb la composició i que han d'executar-se fora del procés de composició.

    Introducció

    Entorn de treball

    Crea un projecte effect.

    SideEffect

    src/happy/happy.kt

    SideEffect és una funció componible que executa un bloc de codi cada vegada que el seu component pare es recompon, i que s’executa de manera independent al final del procés de composició.

    Per exemple, a continuació tens un codi que no utilitza un SideEffect, i que registra que la cosa va 🥳 quan en realitat ha anat 👺 perquè no espera que hagi finalitzat tot el procés de recomposició:

    @Composable
    fun App() {
    log()
    Text("Hello Word!")
    throw Exception("Evil 👺")
    }
    fun log() {
    println("Everything Went Well 🥳")
    }

    Si executes el codi pots veure que s’imprimeix el log 🥳, encara que la composició ha estat 👺

    Terminal window
    Everything Went Well 🥳
    Exception in thread "main" java.lang.Exception: Evil ?

    Això passa perquè el procés de composició ha executat la crida a log() quan tu li has dit que havia d’executar-ho.

    En canvi, si utilitzes un SideEffect com en aquest exemple, la funció componible només s’executa al final de la composició si la composició s’ha pogut realitzar.

    @Composable
    fun App() {
    SideEffect {
    log()
    }
    Text("Hello Word!")
    throw Exception("Evil 👺")
    }

    Si executes el codi segueix anant 👺, però almenys no queda registrat que la composició ha anat 🥳:

    Terminal window
    Exception in thread "main" java.lang.Exception: Evil ?

    Recomposició

    src/dot/dot.kt

    Quan es recompon un Composable, es torna a executar tot el codi dins de la funció Composable, inclosos els efectes secundaris.

    També saps de Recomposició, que només es recompon aquella part de la interfície afectada per la variable d’estat que ha estat modificada.

    Per tant, només s’executen els SideEffects que formin part dels componibles que es tornen a recompondre en veure’s afectats per la modificació de la variable d’estat.

    No obstant això, la interfície d’usuari només s’actualitzarà amb els canvis que s’hagin realitzat en l’estat o propietats del Composable.

    @Composable
    fun App() {
    var count by remember { mutableStateOf(0) }
    Column {
    Text("DOT DOT DOT 🥰 🥰 🥰")
    SideEffect {
    println("$count ++++++++++")
    }
    Button(onClick = { count++ }) {
    SideEffect {
    println("$count ##########")
    }
    Text("Count")
    }
    }
    }

    Si executes el codi pots veure que els SideEffects només s’executen una vegada:

    Terminal window
    0 ++++++++++
    0 ##########

    No importa les vegades que premis el botó i la variable d’estat count incrementi el seu valor: a cap composable li interessa el seu estat (pobreta, ningú li fa cas 😥) i cap es recompon.

    Modifica el codi perquè el text del botó mostri el valor de la variable d’estat count:

    @Composable
    fun App() {
    val count = remember { mutableStateOf(0) }
    Column {
    Text("DOT DOT DOT 🥰 🥰 🥰")
    SideEffect {
    println("${count.value} ++++++++++")
    }
    Button(onClick = { count.value++ }) {
    SideEffect {
    println("${count.value} ##########")
    }
    Text("Count ${count.value} ♥️")
    }
    }
    }

    Ara si prems el botó, el botó s’ha de recompondre perquè està lligat a l’estat de la variable count en mostrar el seu valor:

    0 ++++++++++
    0 ##########
    1 ##########
    2 ##########
    3 ##########
    ...

    I en recompondre’s el Button també s’executa el seu SideEffect … Quina bonica història d’amor 💞 🌚

    Perquè després diguin que escriure codi no és creatiu i poètic 😂

    LaunchedEffect

    src/song/song.kt

    Encara que les històries de SideEffect poden ser interessants i romàntiques, moltes altres s’han d’executar en una altra coroutine independent perquè el procés principal no pot esperar que els SideEffects acabin el que volen que facin els SideEffect, que en la majoria dels casos és esperar molt, moltíssim temps en temps d’ordinador sense fer absolutament res.

    Les aplicacions multiplataforma es caracteritzen per l’ús intensiu d’operacions E/S com l’accés a un fitxer, una base de dades o un servei remot, que bàsicament consisteix a esperar 🥱.

    Per tant, les operacions E/S s’han d’executar de manera asíncrona en un àmbit de corutina independent perquè la UI no es “congeli” mentre s’espera el resultat.

    Modifica el codi:

    @Composable
    fun App() {
    val text = io("Hello", 3000)
    Text(text)
    }
    suspend fun <T>io(data:T, time: Long): T {
    delay(time)
    return data
    }

    Aquest codi no es pot executar perquè no compila:

    Terminal window
    Suspend function 'suspend fun <T> io(data: T, time: Long): T' should be called only from a coroutine or another suspend function.

    Com era d’esperar, Kotlin detecta que estàs cridant una funció “suspend” que bloqueja la corutina que renderitza la UI, i no ho permet.

    Per aquests casos està LaunchedEffect, una funció componible que executa un efecte secundari en un àmbit de corutina independent.

    @Composable
    fun App() {
    val text = "♥️ Best Love Song ? ♥️"
    LaunchedEffect(true) {
    val song = io("You Make My Dreams", 3000)
    }
    Text(text)
    }
    suspend fun <T> io(data: T, time: Long): T {
    delay(time)
    return data
    }

    De funcionar, funciona, no congela la UI, però com utilitzem el resultat a la UI 🧐?

    Amb una variable d’estat 😀:

    @Composable
    fun App() {
    var text by remember { mutableStateOf("♥️ Best Love Song ? ♥️") }
    LaunchedEffect(true) {
    val song = io("You Make My Dreams", 5000)
    text = song
    }
    Text(text)
    }

    Ara quan l’operació io retorna la cançó després de 5 segons, modifiquem la variable d’estat text a la corutina independent, i com s’ha modificat una variable d’estat, la UI es torna a recompondre.

    Informar a l’usuari

    src/flower/flower.kt

    Quan s’efectua una operació asíncrona s’ha d’indicar a l’usuari que s’està esperant una resposta.

    Per exemple, aquesta funció tarda una estona a rebre la llista de flors:

    suspend fun fetchData(): List<String> {
    delay(4000)
    return listOf("Rose", "Tulip", "Sunflower", "Daisy", "Lily", "Orchid")
    }

    El codi que es mostra a continuació informa a l’usuari:

    @Composable
    fun App() {
    var isLoading by remember { mutableStateOf(false) }
    var data by remember { mutableStateOf(listOf<String>()) }
    LaunchedEffect(isLoading) {
    if (isLoading) {
    data = fetchData()
    isLoading = false
    }
    }
    Column {
    Button(onClick = { isLoading = true }) {
    Text("Fetch Data")
    }
    if (isLoading) {
    CircularProgressIndicator()
    } else {
    // Mostra les dades
    LazyColumn {
    items(data) { item ->
    Text(item)
    }
    }
    }
    }
    }

    En aquest exemple, la funció LaunchedEffect executa una crida de xarxa per obtenir dades d’una API quan la variable d’estat isLoading té el valor true. La funció s’executa en un àmbit de corutina independent, cosa que permet que la interfície d’usuari continuï responent mentre s’efectua l’operació.

    La funció LaunchedEffect pren dos paràmetres: key, que s’estableix a isLoading, i block que és una lambda que defineix l’efecte secundari a executar.

    En aquest cas, el bloc lambda invoca la funció fetchData(), que simula una crida de xarxa que suspèn la corutina durant 4 segons.

    Una vegada que s’obtenen les dades, actualitza la variable d’estat data i es posa isLoading a false, amagant l’indicador de càrrega i mostrant les dades obtingudes.

    Quina és la lògica darrere del paràmetre key?

    El paràmetre key a LaunchedEffect s’utilitza per identificar la instància LaunchedEffect i evitar que es recompongui innecessàriament.

    Quan es recompon un Composable, Compose determina si s’ha de tornar a dibuixar. Si l’estat o les propietats d’un Composable han canviat, o si un Composable ha invocat invalidate, Compose torna a dibuixar el Composable. Redibuixar un Composable pot ser una operació costosa, sobretot si el Composable conté operacions de llarga durada o efectes secundaris que no és necessari que es tornin a executar cada vegada que es recompon el Composable.

    En proporcionar un paràmetre key a LaunchedEffect, pots especificar un valor que identifiqui de forma única la instància LaunchedEffect. Si el valor del paràmetre key canvia, Compose considerarà la instància LaunchedEffect com una nova instància i tornarà a executar l’efecte secundari. Si el valor del paràmetre key continua sent el mateix, Compose saltarà l’execució de l’efecte secundari i reutilitzarà el resultat anterior, evitant recomposicions innecessàries.

    També pots utilitzar diverses “keys” per a LaunchedEffect:

    // Utilitza un UUID aleatori com a clau per a LaunchedEffect
    val key = remember { UUID.randomUUID().toString() }
    LaunchedEffect(key, isLoading) {
    ...
    }

    DisposableEffect

    src/timer/timer.kt

    DisposableEffect és una funció Composable que executa un efecte secundari quan es representa per primera vegada el Composable principal i elimina l’efecte quan el Composable s’elimina de la jerarquia de UI.

    Aquesta funció és útil per gestionar els recursos que han de netejar-se quan un Composable ja no s’utilitza, com ara:

    • Afegir i eliminar oients d’esdeveniments (“event listeners”)
    • Iniciar i aturar animacions
    • Vincular i desvincular recursos de sensors com ara Camera, LocationManager, etc
    • Gestió de connexions de bases de dades

    A continuació tens un exemple:

    @Composable
    fun TimerScreen() {
    var elapsedTime by remember { mutableStateOf(0) }
    DisposableEffect(Unit) {
    val scope = CoroutineScope(Dispatchers.Default)
    val job = scope.launch {
    while(true) {
    delay(1_000)
    elapsedTime +=1
    println("Timer is still working $elapsedTime")
    }
    }
    onDispose {
    job.cancel()
    }
    }
    Text(
    text = "Elapsed Time: $elapsedTime",
    modifier = Modifier.padding(16.dp),
    fontSize = 24.sp
    )
    }

    En aquest codi, utilitzem DisposableEffect per llançar una corutina que augmenta el valor de l’estat elapsedTime cada segon. També utilitzem DisposableEffect per garantir que la corutina es cancel·li i els recursos utilitzats per la corutina es netegin quan el Composable ja no s’utilitza.

    A la funció de neteja del DisposableEffect, cancel·lem la corutina utilitzant el mètode cancel() de la instància Job emmagatzemada a job.

    La funció onDispose es crida quan el Composable s’elimina de la jerarquia de la interfície d’usuari i proporciona una manera de netejar qualsevol recurs utilitzat per Composable. En aquest cas, utilitzem onDispose per cancel·lar la corutina i assegurar-nos que es neteja tots els recursos utilitzats per la corutina.

    Per comprovar com funciona DisposableEffect, executa aquest codi per veure el resultat:

    @Composable
    fun RunTimerScreen() {
    var isVisible by remember { mutableStateOf(true) }
    Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Bottom
    ) {
    Spacer(modifier = Modifier.height(10.dp))
    Button(onClick = { isVisible = false }) {
    Text("Hide the timer")
    }
    if (isVisible)
    TimerScreen()
    }
    }

    Has afegit un nou Composable RunTimerScreen que permet a l’usuari canviar la visibilitat de TimerScreen.

    Quan l’usuari fa clic al botó “Hide the timer”, el Composable TimerScreen s’elimina de la jerarquia de la interfície d’usuari i la corutina es cancel·la i neteja.

    Si elimines la invocació a job.cancel() de la funció onDispose la corutina continuarà executant-se fins i tot quan el Composable TimerScreen ja no estigui en ús, cosa que pot provocar fuites i altres problemes de rendiment.

    Mitjançant l’ús conjunt de DisposableEffect i CoroutineScope t’assegures que la corutina llançada per CoroutineScope es cancel·la i es netegen els recursos quan el TimerScreen ja no està en ús.

    Activitat

    Elimina la invocació a job.cancel() i verifica que la corutina continua executant-se imprimint per pantalla el temps que ha passat.

    Terminal window
    Timer is still working 1
    Timer is still working 2
    Timer is still working 3
    Timer is still working 4
    ...

    Actvitats

    Star Wars API

    En aquesta activitat utilitzarem Ktor - Client

    A SWAPI tens una API de Star Wars.

    Per exemple https://swapi.dev/api/people/1/?format=json

    Construeix una aplicació que et mostri la informació de starwars i que guardi les dades en cache a la base de dades local.

    SpaceX

    En aquest enllaç tens un tutorial per Android: Create a multiplatform app using Ktor and SQLDelight

    Fes la mateixa aplicació, però “jvm/app” amb Room (enlloc de SQLDelight).

    TODO