Sovrapposizione dell'architettura per Jetpack Compose

Questa pagina fornisce una panoramica generale dei livelli architetturali di Jetpack Compose, nonché dei principi fondamentali su cui si basa questa progettazione.

Jetpack Compose non è un singolo progetto monolitico, ma viene creato a partire da una serie di moduli assemblati per formare uno stack completo. Comprendere i diversi moduli che compongono Jetpack Compose ti consente di:

  • Usa il livello di astrazione appropriato per creare la tua app o la tua libreria
  • Cerca di capire quando puoi passare a un livello inferiore per avere un maggiore controllo o una personalizzazione
  • Riduci al minimo le dipendenze

Livelli

I livelli principali di Jetpack Compose sono:

Figura 1. I livelli principali di Jetpack Compose.

Ogni livello è basato sui livelli inferiori, combinando le funzionalità per creare componenti di livello superiore. Ogni livello si basa sulle API pubbliche dei livelli inferiori per verificare i limiti dei moduli e consentirti di sostituire qualsiasi livello in caso di necessità. Esaminiamo questi livelli dal basso verso l'alto.

Durata
Questo modulo fornisce le nozioni di base del runtime di Compose, come remember, mutableStateOf, l'annotazione @Composable e SideEffect. Puoi scegliere di utilizzare direttamente questo livello se hai bisogno solo delle funzionalità di gestione degli alberi di Compose, non della sua UI.
Interfaccia utente
Il livello UI è composto da più moduli (ui-text, ui-graphics, ui-tooling e così via). Questi moduli implementano le nozioni di base del toolkit dell'interfaccia utente, come LayoutNode, Modifier, gestori di input, layout personalizzati e disegno. Prova a basarti su questo livello se ti servono solo i concetti fondamentali di un toolkit dell'interfaccia utente.
Nozioni di base
Questo modulo fornisce componenti di base indipendenti dal sistema di progettazione per l'UI di Compose, come Row e Column, LazyColumn, il riconoscimento di particolari gesti e così via. Potresti considerare l'utilizzo del livello di base per creare il tuo sistema di progettazione personalizzato.
Materiale
Questo modulo fornisce un'implementazione del sistema di Material Design per l'interfaccia utente di Compose, fornendo un sistema di temi, componenti con stili, indicazioni a onde e icone. Sfrutta questo livello quando utilizzi Material Design nella tua app.

Principi di progettazione

Uno dei principi cardine di Jetpack Compose è la fornitura di piccoli elementi mirati che possano essere assemblati (o composti) insieme, piuttosto che di pochi componenti monolitici. Questo approccio presenta una serie di vantaggi.

Controlli

I componenti di livello superiore tendono a produrre di più, ma limitano il tuo livello di controllo diretto. Se hai bisogno di un maggiore controllo, puoi utilizzare il menu a discesa per utilizzare un componente di livello inferiore.

Ad esempio, se vuoi animare il colore di un componente, puoi usare l'API animateColorAsState:

val color = animateColorAsState(if (condition) Color.Green else Color.Red)

Tuttavia, se avevi bisogno che il componente iniziasse sempre in grigio, non puoi farlo con questa API. In alternativa, puoi utilizzare il menu a discesa per utilizzare l'API Animatable di livello inferiore:

val color = remember { Animatable(Color.Gray) }
LaunchedEffect(condition) {
    color.animateTo(if (condition) Color.Green else Color.Red)
}

L'API animateColorAsState di livello superiore si basa a sua volta sull'API Animatable di livello inferiore. Utilizzare l'API di livello inferiore è più complesso, ma offre un maggiore controllo. Scegli il livello di astrazione più adatto alle tue esigenze.

Funzionalità di

L'assemblaggio di componenti di livello superiore da componenti di base più piccoli rende molto più facile personalizzare i componenti. Ad esempio, considera l'implementazione di Button fornita dal livello Material:

@Composable
fun Button(
    // …
    content: @Composable RowScope.() -> Unit
) {
    Surface(/* … */) {
        CompositionLocalProvider(/* … */) { // set LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                Row(
                    // …
                    content = content
                )
            }
        }
    }
}

Un elemento Button viene assemblato a partire da 4 componenti:

  1. Un materiale Surface che fornisce lo sfondo, la forma, la gestione dei clic e così via.

  2. Un elemento CompositionLocalProvider che modifica la versione alpha dei contenuti quando il pulsante viene attivato o disattivato

  3. Un elemento ProvideTextStyle imposta lo stile di testo predefinito da utilizzare

  4. Un Row fornisce il criterio di layout predefinito per i contenuti del pulsante

Abbiamo omesso alcuni parametri e commenti per rendere più chiara la struttura, ma l'intero componente contiene solo circa 40 righe di codice perché combina semplicemente questi quattro componenti per implementare il pulsante. I componenti come Button sono valutati in merito ai parametri che espongono, bilanciando le personalizzazioni comuni con un'esplosione di parametri che possono rendere un componente più difficile da usare. I componenti Material, ad esempio, offrono personalizzazioni specificate nel sistema Material Design, semplificando il rispetto dei principi di progettazione dei materiali.

Tuttavia, se vuoi apportare una personalizzazione oltre i parametri di un componente, puoi eseguire il "menu a discesa" di un livello ed eseguire il fork di un componente. Ad esempio, Material Design specifica che i pulsanti devono avere uno sfondo in tinta unita. Se hai bisogno di uno sfondo sfumato, questa opzione non è supportata dai parametri Button. In questo caso, puoi utilizzare l'implementazione Material Button come riferimento e creare il tuo componente:

@Composable
fun GradientButton(
    // …
    background: List<Color>,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(
                Brush.horizontalGradient(background)
            )
    ) {
        CompositionLocalProvider(/* … */) { // set material LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                content()
            }
        }
    }
}

L'implementazione precedente continua a utilizzare i componenti del livello Material, ad esempio i concetti di contenuti alpha dei contenuti attuali di Material e lo stile di testo corrente. Tuttavia, sostituisce il materiale Surface con un Row e lo stile per ottenere l'aspetto desiderato.

Se non vuoi usare i concetti di base di Material, ad esempio se stai creando un tuo sistema di progettazione personalizzato, puoi scegliere di usare solo i componenti del livello di base:

@Composable
fun BespokeButton(
    // …
    backgroundColor: Color,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(backgroundColor)
    ) {
        // No Material components used
        content()
    }
}

Jetpack Compose riserva i nomi più semplici per i componenti di livello più elevato. Ad esempio, androidx.compose.material.Text si basa su androidx.compose.foundation.text.BasicText. In questo modo, puoi fornire alla tua implementazione il nome più rilevabile se vuoi sostituire i livelli superiori.

Scegliere la giusta astrazione

La filosofia di Compose per creare componenti a più livelli e riutilizzabili implica che non sempre dovresti raggiungere i componenti di base di livello inferiore. Molti componenti di livello superiore non solo offrono più funzionalità, ma spesso implementano best practice come il supporto dell'accessibilità.

Ad esempio, se vuoi aggiungere il supporto dei gesti al tuo componente personalizzato, puoi crearlo da zero utilizzando Modifier.pointerInput, ma esistono altri componenti di livello superiore basati su questo strumento che potrebbero offrire un punto di partenza migliore, ad esempio Modifier.draggable, Modifier.scrollable o Modifier.swipeable.

In linea di massima, è preferibile utilizzare il componente di livello superiore che offre le funzionalità necessarie per usufruire delle best practice incluse.

Scopri di più

Vedi l'esempio di Jetsnack per un esempio di creazione di un sistema di progettazione personalizzato.