Composició

  • Compose està construït al voltant de funcions componibles.

    Introducció

    Compose utilitza un model d’interfície d’usuari (UI) declaratiu que utilitza funcions que es componen d’altres funcions que prenen dades i emeten elements d’UI.

    Aquestes funcions et permeten definir la UI de la teva aplicació mitjançant codi, descrivint quin aspecte hauria de tenir i proporcionant les dependències respecte a les dades, en lloc d’haver de centrar-te en la construcció de la UI (inicialitzant un element, afegint aquest a un element superior, etc.).

    Crea un projecte “JVM GUI Application (Compose Multiplatform)” amb Amper.

    Funcions componibles

    Per crear una funció componible només has d’afegir l’anotació @Composable al nom de la funció:

    @Composable
    fun Greeting(name: String) {
        Text("Hello $name")
    }

    Algunes coses destacables sobre aquesta funció:

    • La funció està anotada amb l’anotació @Composable. Totes les funcions componibles han de tenir aquesta anotació perquè el compilador Compose sàpiga que aquesta funció està destinada a convertir dades en UI.

    • La funció pren dades. Les funcions Composable poden acceptar paràmetres perquè la funció descrigui els components de la UI en funció d’aquests paràmetres.

    • La funció mostra text a la UI. Ho fa cridant a la funció componible Text() creant d’aquesta manera una jerarquia d’UI cridant a altres funcions componibles.

    • La funció no retorna res. Les funcions de composició que emeten UI no necessiten retornar res perquè només descriuen una part de la UI, en cap cas construeixen widgets d’UI.

    • Aquesta funció és ràpida, idempotent i lliure d’efectes secundaris. La funció es comporta de la mateixa manera quan se la crida diverses vegades amb el mateix argument i no utilitza altres valors com variables globals o crides a random(). La funció descriu la UI sense cap efecte secundari, com modificar propietats o variables globals.

    En general, totes les funcions componibles han d’escriure’s amb aquestes propietats, per les raons que s’explicaran a Recomposición.

    Afegeix un element Text

    Modifica el fitxer main.kt:

    @Composable
    @Preview
    fun App() {
        MaterialTheme {
            Text("Hello World!")
        }
    }

    App és una funció componible que ara utilitza la funció componible Text que mostra el text “Hello World!”:

    App

    Text(Hello World!)

    Defineix una funció componible

    A continuació crearàs una funció componible amb l’anotació @Composable.

    Defineix una funció MessageCard a la qual se li passa un nom que utilitzes per configurar un element de text.

    @Composable
    fun MessageCard(name: String) {
        Text(text = "Hello $name")
    }

    Obtén una vista prèvia de la funció

    L’anotació @Preview et permet obtenir una vista prèvia d’una funció componible amb els paràmetres que tu vulguis sense haver de modificar el codi principal de l’aplicació.

    L’anotació s’ha d’utilitzar en una funció componible que no accepti paràmetres. Per aquest motiu, no pots obtenir una vista prèvia de la funció MessageCard directament.

    En el seu lloc, crea una segona funció anomenada MessageCardPreview, que cridi MessageCard amb un paràmetre apropiat.

    @Preview
    @Composable
    fun MessageCardPreview() {
        MessageCard("Hello, David!")
    }

    Layouts

    Els elements de la interfície d’usuari són jeràrquics i estan continguts en altres elements. A Compose, es crea una jerarquia de la interfície d’usuari cridant a funcions componibles des d’altres funcions componibles.

    Afegir diversos textos

    Fins ara has creat la teva primera funció componible i una vista prèvia! Per descobrir més funcions de Compose, crearàs una pantalla de missatgeria simple que contingui una llista de missatges que es pot ampliar amb algunes animacions.

    Comença per enriquir el missatge componible mostrant el nom de l’autor i el contingut del missatge. Primer has de canviar el paràmetre componible perquè accepti un objecte Message en lloc d’una String i afegir un altre Text componible dins del componible MessageCard. Assegura’t d’actualitzar també la vista prèvia.

    // ...
    
    @Composable
    fun App() {
        MaterialTheme {
            MessageCard(Message("Eva", "En el pot petit hi ha la bona confitura"))
        }
    }
    
    data class Message(val author: String, val body: String)
    
    @Composable
    fun MessageCard(msg: Message) {
        Text(text = msg.author)
        Text(text = msg.body)
    }

    Aquest codi crea dos elements de text dins de la vista de contingut. No obstant això, atès que no has proporcionat cap informació sobre com organitzar-los, els elements de text es dibuixen un sobre l’altre, el que fa que el text sigui il·legible.

    Utilitzant una columna

    La funció Column permet organitzar elements verticalment.

    Afegeix una Column a la funció MessageCard. Pots utilitzar Row per organitzar elements horitzontalment i Box per apilar elements.

    // ...
    
    @Composable
    fun MessageCard(msg: Message) {
        Column {
            Text(text = msg.author)
            Text(text = msg.body)
        }
    }

    Afegeix una imatge

    Recursos

    A continuació afegeix una foto de perfil del remitent a MessageCard.

    Has d’afegir les imatges al directori composeResources/drawable.

    Has d’executar el projecte per generar la classe Res.

    Afegeix un componible Row per tenir un disseny ben estructurat i un componible Image dins d’ell.

    // ...
    import org.jetbrains.compose.resources.painterResource
    
    // Resources
    import compose_composition.generated.resources.Res
    import compose_composition.generated.resources.eva
    
    @Composable
    fun MessageCard(msg: Message) {
        Row {
            Image(
                painter = painterResource(Res.drawable.eva),
                contentDescription = "Contact profile picture"
    
            )
            Column {
                Text(text = msg.author)
                Text(text = msg.body)
            }
        }
    }

    Configura el layout

    El disseny del teu missatge té l’estructura correcta, però els seus elements no estan ben espaiats i la imatge és massa gran.

    Per decorar o configurar un element componible, Compose utilitza modifiers. Aquests permeten canviar la mida, el disseny i l’aparença de l’element componible o afegir interaccions d’alt nivell, com fer que es pugui fer clic en un element. Pots encadenar-los per crear elements componibles més complets. Utilitzaràs alguns d’ells per millorar el disseny.

    // ...
    import androidx.compose.foundation.layout.Spacer
    import androidx.compose.foundation.layout.height
    import androidx.compose.foundation.layout.padding
    import androidx.compose.foundation.layout.size
    import androidx.compose.foundation.layout.width
    import androidx.compose.foundation.shape.CircleShape
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.draw.clip
    import androidx.compose.ui.unit.dp
    
    @Composable
    fun MessageCard(msg: Message) {
        // Add padding around our message
        Row(modifier = Modifier.padding(all = 16.dp)) {
            Image(
                painter = painterResource(R.drawable.eva),
                contentDescription = "Contact profile picture",
                modifier = Modifier
                    // Set image size to 80 dp
                    .size(80.dp)
                    // Clip image to be shaped as a circle
                    .clip(CircleShape)
            )
    
            // Add a horizontal space between the image and the column
            Spacer(modifier = Modifier.width(16.dp))
    
            Column {
                Text(text = msg.author)
                // Add a vertical space between the author and message texts
                Spacer(modifier = Modifier.height(8.dp))
                Text(text = msg.body)
            }
        }
    }

    Material Design

    Compose està dissenyat per admetre els principis de Material Design. Molts dels seus elements d’interfície d’usuari implementen Material Design de manera predeterminada. En aquesta lliçó, dissenyaràs la teva aplicació amb widgets de Material Design.

    Compose está diseñado para admitir los principios de Material Design. Muchos de sus elementos de interfaz de usuario implementan Material Design de manera predeterminada. En esta lección, diseñarás tu aplicación con widgets de Material Design.

    TODO Revisar si cal modificar la configuració d’Amper.

    Compose ofereix una implementació de Material Design 3 i els seus elements d’interfície d’usuari llestos per utilitzar. Milloraràs l’aparença de componible MessageCard mitjançant l’estil de Material Design.

    Per començar, embolica la funció MessageCard amb el tema Material creat en el teu projecte, MaterialTheme, així com un Surface.

    // ...
    
    @Composable
    fun App() {
        MaterialTheme {
            Surface(modifier = Modifier.fillMaxSize()) {
                MessageCard(Message("Eva", "En el pot petit hi ha la bona confitura"))
            }
        }
    }

    Material Design es basa en tres pilars: Color, Typography, i Shape. Els aniràs afegint un per un.

    Color

    Utilitza MaterialTheme.colors per estilitzar amb colors del tema embolcallat. Pots utilitzar aquests valors del tema en qualsevol lloc on es necessiti un color. Aquest exemple utilitza colors de temes dinàmics (definits per les preferències del dispositiu).

    Pots establir dynamicColor a false al fitxer MaterialTheme.kt per canviar això. TODO on?

    Estilitza el títol i afegeix una vora a la imatge.

    // ...
    import androidx.compose.foundation.border
    import androidx.compose.material3.MaterialTheme
    
    @Composable
    fun MessageCard(msg: Message) {
        // Add padding around our message
        Row(modifier = Modifier.padding(all = 16.dp)) {
            Image(
                painter = painterResource(Res.drawable.eva),
                contentDescription = "Contact profile picture",
                modifier = Modifier
                    // Set image size to 80 dp
                    .size(80.dp)
                    // Clip image to be shaped as a circle
                    .clip(CircleShape).border(3.dp, color = MaterialTheme.colors.primary, CircleShape)
            )
    
            // Add a horizontal space between the image and the column
            Spacer(modifier = Modifier.width(16.dp))
    
            Column {
                Text(
                    text = msg.author,
                    color = MaterialTheme.colors.secondary  
                )
                // Add a vertical space between the author and message texts
                Spacer(modifier = Modifier.height(8.dp))
                Text(text = msg.body)
            }
        }
    }

    Typography

    Typography

    Els estils de Material Typography estan disponibles al MaterialTheme, només cal afegir-los als composables Text.

    // ...
    
    @Composable
    fun MessageCard(msg: Message) {
       Row(modifier = Modifier.padding(all = 16.dp)) {
           Image(
               painter = painterResource(R.drawable.profile_picture),
               contentDescription = null,
               modifier = Modifier
                   .size(80.dp)
                   .clip(CircleShape)
                   .border(3.dp, MaterialTheme.colors.primary, CircleShape)
           )
    
           Spacer(modifier = Modifier.width(16.dp))
    
           Column {
               Text(
                   text = msg.author,
                   color = MaterialTheme.colors.secondary,
                   style = MaterialTheme.typography.titleMedium
               )
    
               Spacer(modifier = Modifier.height(8.dp))
               Text(
                   text = msg.body,
                   style = MaterialTheme.typography.bodyLarge
               )
           }
       }
    }

    Shape

    Amb Shape pots afegir els tocs finals. Primer, embolica el text del cos del missatge amb un composable Surface. Això permet personalitzar la forma i l’elevació del cos del missatge. També s’afegeix padding al missatge per a un millor disseny.

    // ...
    import androidx.compose.material3.Surface
    
    @Composable
    fun MessageCard(msg: Message) {
        Row(modifier = Modifier.padding(all = 16.dp)) {
            Image(
                painter = painterResource(R.drawable.coyote),
                contentDescription = "Contact profile picture",
                modifier = Modifier
                    .size(80.dp)
                    .clip(CircleShape)
                    .border(
                        3.dp, MaterialTheme.colors.primary,
                        CircleShape
                    )
            )
            Spacer(modifier = Modifier.width(16.dp))
            Column {
                Text(
                    text = msg.author, color = MaterialTheme.colors.secondary,
                    style = MaterialTheme.typography.titleMedium
                )
                Spacer(modifier = Modifier.height(8.dp))
                Surface(shape = MaterialTheme.shapes.large, shadowElevation = 2.dp) {
                    Text(
                        text = msg.body,
                        modifier = Modifier.padding(all = 4.dp),
                        style = MaterialTheme.typography.bodyLarge
                    )
                }
            }
        }
    }

    Llistes i animacions

    Les llistes i animacions són presents a tot arreu en les aplicacions. En aquesta lliçó, aprendràs com Compose fa que sigui fàcil crear llistes i divertit afegir animacions.

    Crear una llista de missatges

    Un xat amb un missatge sembla una mica solitari, així que canviarem la conversa per tenir més d’un missatge..

    val messages = listOf(
        Message(
            "Josep Pla",
            "Antigament, el viatjar era un privilegi de gran senyor. Generalment, era la coronació normal dels estudis d'un home. Ara el viatjar s'ha generalitzat i democratitzat considerablement. Viatja molta gent"
        ),
        Message(
            "Josep Pla",
            "Però, potser, les persones que viatgen per arrodonir i afermar la seva visió del món i dels homes són més rares avui que fa cent anys. "
        ),
        Message(
            "Josep Pla",
            "En el nostre país hi ha tres pretextos essencials per a passar la frontera: la peregrinació a Lourdes, la lluna de mel i els negocis. Hom no pot tenir idea de la quantitat de gent del nostre país que ha estat a Lourdes. És incomptable. "
        ),
        Message(
            "Josep Pla",
            "Fa trenta anys, les persones riques de Catalunya feien el viatge de noces a Madrid. Avui van a París o a Niça i de vegades a Itàlia. La lluna de mel, però, és un mal temps per veure res i per formar-se. No es poden pas fer dues coses importants a la vegada. El pitjor temps, potser, per a viatjar, de la vida, és la temporada de la lluna de mel."
        ),
    )

    Necessitaràs crear una funció Conversation que mostrarà múltiples missatges. Per a aquest cas d’ús, utilitza els LazyColumn i LazyRow de Compose. Aquests composables renditzen només els elements que són visibles a la pantalla, per la qual cosa estan dissenyats per ser molt eficients per a llistes llargues.

    En aquest fragment de codi, pots veure que LazyColumn té un fill items. Pren una List com a paràmetre i la seva lambda rep un paràmetre que hem anomenat message (podríem haver-lo anomenat com volguéssim) que és una instància de Message. En resum, aquesta lambda es crida per a cada element de la List proporcionada.

    // ...
    import androidx.compose.foundation.lazy.LazyColumn
    import androidx.compose.foundation.lazy.items
    
    @Composable
    fun App() {
        MaterialTheme {
            Surface(modifier = Modifier.fillMaxSize()) {
                Conversation(messages)
            }
        }
    }
    
    @Composable
    fun Conversation(messages: List<Message>) {
        LazyColumn {
            items(messages) { message -> MessageCard(message) }
        }
    }

    Animar missatges mentre s’expandeixen

    La conversa s’està tornant més interessant. És hora de jugar amb les animacions! Afegiràs la capacitat d’expandir un missatge per mostrar-ne un de més llarg, animant tant la mida del contingut com el color de fons. Per emmagatzemar aquest estat d’UI local, has de mantenir un registre de si un missatge s’ha expandit o no. Per mantenir un registre d’aquest canvi d’estat, has d’utilitzar les funcions remember i mutableStateOf.

    Les funcions composables poden emmagatzemar estat local en memòria utilitzant remember, i fer un seguiment dels canvis al valor passat a mutableStateOf. Els composables (i els seus fills) que utilitzen aquest estat es tornaran a dibuixar automàticament quan el valor s’actualitzi. Això s’anomena recomposició.

    Utilitzant les APIs d’estat de Compose com remember i mutableStateOf, qualsevol canvi en l’estat actualitza automàticament la UI.

    // ...
    import androidx.compose.foundation.clickable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    
    @Composable
    fun MessageCard(msg: Message) {
    
            // ...    
    
            // We keep track if the message is expanded or not in this
            // variable
            var isExpanded by remember { mutableStateOf(false) }
    
            Column(modifier = Modifier.clickable{isExpanded = !isExpanded}) {
                
                Text(
                    text = msg.author,
                    color = MaterialTheme.colors.secondary,
                    style = MaterialTheme.typography.subtitle2
                )
                // Add a vertical space between the author and message texts
                Spacer(modifier = Modifier.height(8.dp))
                Surface(shape = MaterialTheme.shapes.large, elevation = 2.dp) {
                    Text(
                        text = msg.body,
                        modifier = Modifier.padding(all = 4.dp),
    
                        // If the message is expanded, we display all its content
                        // otherwise we only display the first line
                        maxLines = if (isExpanded) Int.MAX_VALUE else 1,
                        
                        style = MaterialTheme.typography.body1
                    )
                }
            }
        }
    }

    Ara pots canviar el fons del contingut del missatge basat en isExpanded quan fem clic en un missatge. Utilitzaràs el modificador clickable per gestionar els esdeveniments de clic en el composable.

    En lloc de simplement alternar el color de fons del Surface, animaràs el color de fons modificant gradualment el seu valor de MaterialTheme.colors.surface a MaterialTheme.colors.primary i viceversa.

    Per fer-ho, utilitzaràs la funció animateColorAsState.

    Finalment, utilitzaràs el modificador animateContentSize per animar la mida del contenidor del missatge suaument:

    // ...
    import androidx.compose.animation.animateColorAsState
    import androidx.compose.animation.animateContentSize
    
    @Composable
    fun MessageCard(msg: Message) {
        // Add padding around our message
        Row(modifier = Modifier.padding(all = 16.dp)) {
            Image(
                painter = painterResource(Res.drawable.josep),
                contentDescription = "Contact profile picture",
                modifier = Modifier
                    // Set image size to 80 dp
                    .size(80.dp)
                    // Clip image to be shaped as a circle
                    .clip(CircleShape).border(3.dp, color = MaterialTheme.colors.primary, CircleShape)
            )
    
            // Add a horizontal space between the image and the column
            Spacer(modifier = Modifier.width(16.dp))
    
            // We keep track if the message is expanded or not in this
            // variable
            var isExpanded by remember { mutableStateOf(false) }
    
            // surfaceColor will be updated gradually from one color to the other
            val surfaceColor by animateColorAsState(
                if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface
            )
    
            Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
                Text(
                    text = msg.author,
                    color = MaterialTheme.colors.secondary,
                    style = MaterialTheme.typography.subtitle2
                )
                // Add a vertical space between the author and message texts
                Spacer(modifier = Modifier.height(8.dp))
                Surface(
                    shape = MaterialTheme.shapes.large,
                    elevation = 2.dp,
                    // surfaceColor color will be changing gradually from primary to surface
                    color = surfaceColor,
                    // animateContentSize will change the Surface size gradually
                    modifier = Modifier.animateContentSize().padding(2.dp)
                ) {
                    Text(
                        text = msg.body,
                        modifier = Modifier.padding(all = 4.dp),
                        // If the message is expanded, we display all its content
                        // otherwise we only display the first line
                        maxLines = if (isExpanded) Int.MAX_VALUE else 1,
                        style = MaterialTheme.typography.body1
                    )
                }
            }
        }
    }

    TODO