Dati con ambito locale con ComposizioneLocal

CompositionLocal è uno strumento per trasmettere dati in modo implicito alla composizione. In questa pagina scoprirai cos'è un CompositionLocal in maggiore dettaglio, come crearne uno tuo CompositionLocal e scoprirai se un CompositionLocal è una buona soluzione per il tuo caso d'uso.

Ti presentiamo CompositionLocal

Di solito, in Compose, i dati scorrono verso il basso nell'albero dell'interfaccia utente come parametri per ogni funzione componibile. Ciò rende esplicite le dipendenze di un componibile. Tuttavia, questa operazione può risultare difficile per dati molto frequenti e ampiamente utilizzati, come colori o stili di caratteri. Vedi l'esempio che segue:

@Composable
fun MyApp() {
    // Theme information tends to be defined near the root of the application
    val colors = colors()
}

// Some composable deep in the hierarchy
@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        color = colors.onPrimary // ← need to access colors here
    )
}

Per supportare la mancata necessità di passare i colori come dipendenza esplicita dei parametri alla maggior parte dei componibili, Compose offre CompositionLocal che consente di creare oggetti denominati con albero ad albero che possono essere utilizzati come modo implicito per avere un flusso di dati attraverso la struttura dell'interfaccia utente.

In genere, agli elementi CompositionLocal viene fornito un valore in un determinato nodo della struttura dell'interfaccia utente. Questo valore può essere utilizzato dai relativi discendenti componibili senza dichiarare CompositionLocal come parametro nella funzione componibile.

CompositionLocal è ciò che viene usato dal tema Material. MaterialTheme è un oggetto che fornisce tre istanze CompositionLocal (colori, tipografia e forme), che ti consentono di recuperarle in un secondo momento in qualsiasi parte discendente della composizione. In particolare, queste sono le proprietà LocalColors, LocalShapes e LocalTypography a cui puoi accedere tramite gli attributi MaterialTheme colors, shapes e typography.

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colors, typography, and shapes are available
        // in MaterialTheme's content lambda.

        // ... content here ...
    }
}

// Some composable deep in the hierarchy of MaterialTheme
@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        // `primary` is obtained from MaterialTheme's
        // LocalColors CompositionLocal
        color = MaterialTheme.colors.primary
    )
}

Un'istanza CompositionLocal ha come ambito una parte della composizione, così puoi fornire valori diversi a diversi livelli dell'albero. Il valore current di un valore CompositionLocal corrisponde al valore più vicino fornito da un predecessore in quella parte della composizione.

Per fornire un nuovo valore a un valore CompositionLocal, utilizza CompositionLocalProvider e la relativa funzione infisso provides che associa una chiave CompositionLocal a un value. Il lambda content di CompositionLocalProvider riceverà il valore fornito quando accedi alla proprietà current di CompositionLocal. Quando viene fornito un nuovo valore, Compose ricompone le parti della composizione che leggono il CompositionLocal.

Ad esempio, LocalContentAlpha CompositionLocal contiene la versione alpha dei contenuti preferiti utilizzata per testo e icone per enfatizzare o ridurre le diverse parti dell'interfaccia utente. Nell'esempio seguente, CompositionLocalProvider viene utilizzato per fornire valori diversi per parti diverse della composizione.

@Composable
fun CompositionLocalExample() {
    MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
        Column {
            Text("Uses MaterialTheme's provided alpha")
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("Medium value provided for LocalContentAlpha")
                Text("This Text also uses the medium value")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                    DescendantExample()
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the disabled alpha now")
}

Figura 1. Anteprima del componibile CompositionLocalExample.

In tutti gli esempi precedenti, le istanze CompositionLocal sono state utilizzate internamente dai componenti componibili Material. Per accedere al valore corrente di un elemento CompositionLocal, utilizza la relativa proprietà current. Nell'esempio seguente, l'attuale valore Context di LocalContext CompositionLocal, comunemente utilizzato nelle app per Android, viene utilizzato per formattare il testo:

@Composable
fun FruitText(fruitSize: Int) {
    // Get `resources` from the current value of LocalContext
    val resources = LocalContext.current.resources
    val fruitText = remember(resources, fruitSize) {
        resources.getQuantityString(R.plurals.fruit_title, fruitSize)
    }
    Text(text = fruitText)
}

Creazione del tuo CompositionLocal in corso...

CompositionLocal è uno strumento per trasmettere dati tramite la composizione in modo implicito.

Un altro indicatore chiave per l'utilizzo di CompositionLocal è quando il parametro è trasversale e i livelli di implementazione intermedi non dovrebbero rendersi conto della sua esistenza, perché rendere consapevoli questi livelli intermedi limiterebbe l'utilità del componibile. Ad esempio, l'esecuzione di query sulle autorizzazioni Android è offerta da un CompositionLocal in background. Un componibile selettore media può aggiungere una nuova funzionalità per accedere ai contenuti protetti da autorizzazione sul dispositivo senza modificare la relativa API e richiedere che i chiamanti del selettore media conoscano questo contesto aggiuntivo utilizzato dall'ambiente.

Tuttavia, CompositionLocal non è sempre la soluzione migliore. Sconsigliamo di utilizzare in modo eccessivo CompositionLocal in quanto presenta alcuni svantaggi:

CompositionLocal rende più difficile ragionare sul comportamento di un componibile. Poiché creano dipendenze implicite, i chiamanti dei componibili che le utilizzano devono assicurarsi che sia soddisfatto un valore per ogni elemento CompositionLocal.

Inoltre, potrebbe non esserci una chiara fonte di verità per questa dipendenza, poiché può mutare in qualsiasi parte della composizione. Di conseguenza, eseguire il debug dell'app quando si verifica un problema può essere più difficile, in quanto devi scorrere nella Composizione per vedere dove è stato fornito il valore current. Strumenti come Trova utilizzi nell'IDE o Strumento di controllo del layout di Scrivi forniscono informazioni sufficienti per mitigare questo problema.

Decisione sull'utilizzo di CompositionLocal

Esistono determinate condizioni che possono rendere CompositionLocal una buona soluzione per il tuo caso d'uso:

Il valore CompositionLocal dovrebbe avere un buon valore predefinito. In assenza di un valore predefinito, devi garantire che sia estremamente difficile per uno sviluppatore entrare in una situazione in cui non viene fornito un valore per CompositionLocal. Se non specifichi un valore predefinito, potresti riscontrare problemi e frustrazione durante la creazione dei test o l'anteprima di un componibile in cui CompositionLocal ne richiede sempre la specifica.

Evita CompositionLocal per i concetti che non sono considerati con ambito ad albero o gerarchia. Un CompositionLocal ha senso quando può essere potenzialmente utilizzato da qualsiasi discendente, non da alcuni di loro.

Se il tuo caso d'uso non soddisfa questi requisiti, consulta la sezione Alternative da considerare prima di creare un CompositionLocal.

Un esempio di prassi scorretta è la creazione di un CompositionLocal che contenga il ViewModel di una determinata schermata in modo che tutti i componibili nella schermata possano ottenere un riferimento a ViewModel per eseguire alcune logiche. Questa è una cattiva pratica perché non tutti i componibili sotto un determinato albero dell'interfaccia utente devono conoscere un elemento ViewModel. È buona norma passare ai componibili solo le informazioni di cui hanno bisogno seguendo il pattern in cui lo stato fluisce e gli eventi scorrono. Questo approccio renderà i componibili più riutilizzabili e più facili da testare.

Creazione di un CompositionLocal in corso...

Per creare un CompositionLocal sono disponibili due API:

  • compositionLocalOf: la modifica del valore fornito durante la ricomposizione rende la validità solo del contenuto che legge il relativo valore current.

  • staticCompositionLocalOf: A differenza di compositionLocalOf, le letture di un staticCompositionLocalOf non vengono monitorate da Compose. La modifica del valore comporta la ricomposizione dell'intero lambda content, in cui viene fornito il valore CompositionLocal, anziché solo dei punti in cui il valore current viene letto nella composizione.

Se è molto improbabile che il valore fornito a CompositionLocal cambi o non cambierà mai, utilizza staticCompositionLocalOf per ottenere vantaggi in termini di rendimento.

Ad esempio, il sistema di progettazione di un'app potrebbe essere "guidato" nel modo in cui i componenti componibili vengono elevati usando un'ombreggiatura per il componente dell'interfaccia utente. Poiché le diverse elevazioni dell'app dovrebbero propagarsi nell'albero dell'interfaccia utente, utilizziamo un elemento CompositionLocal. Poiché il valore CompositionLocal viene derivato in modo condizionale in base al tema del sistema, utilizziamo l'API compositionLocalOf:

// LocalElevations.kt file

data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)

// Define a CompositionLocal global object with a default
// This instance can be accessed by all composables in the app
val LocalElevations = compositionLocalOf { Elevations() }

Specificare valori per un CompositionLocal

L'elemento componibile CompositionLocalProvider associa i valori a CompositionLocal istanze per la gerarchia specificata. Per fornire un nuovo valore a CompositionLocal, utilizza la funzione infix di provides che associa una chiave CompositionLocal a un value come segue:

// MyActivity.kt file

class MyActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // Calculate elevations based on the system theme
            val elevations = if (isSystemInDarkTheme()) {
                Elevations(card = 1.dp, default = 1.dp)
            } else {
                Elevations(card = 0.dp, default = 0.dp)
            }

            // Bind elevation as the value for LocalElevations
            CompositionLocalProvider(LocalElevations provides elevations) {
                // ... Content goes here ...
                // This part of Composition will see the `elevations` instance
                // when accessing LocalElevations.current
            }
        }
    }
}

Utilizzo del CompositionLocal

CompositionLocal.current restituisce il valore fornito dal valore CompositionLocalProvider più prossimo che fornisce un valore per il valore CompositionLocal:

@Composable
fun SomeComposable() {
    // Access the globally defined LocalElevations variable to get the
    // current Elevations in this part of the Composition
    Card(elevation = LocalElevations.current.card) {
        // Content
    }
}

Alternative da valutare

Un CompositionLocal potrebbe essere una soluzione eccessiva per alcuni casi d'uso. Se il tuo caso d'uso non soddisfa i criteri specificati nella sezione Decidere se utilizzare ComposeLocal, è probabile che un'altra soluzione sia più adatta al tuo caso d'uso.

Trasmettere parametri espliciti

Essere espliciti riguardo alle dipendenze componibili è una buona abitudine. Ti consigliamo di trasmettere i componibili solo ciò di cui hanno bisogno. Per incoraggiare il disaccoppiamento e il riutilizzo dei componibili, ogni componibile dovrebbe contenere la minor quantità di informazioni possibile.

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel.data)
}

// Don't pass the whole object! Just what the descendant needs.
// Also, don't  pass the ViewModel as an implicit dependency using
// a CompositionLocal.
@Composable
fun MyDescendant(myViewModel: MyViewModel) { /* ... */ }

// Pass only what the descendant needs
@Composable
fun MyDescendant(data: DataToDisplay) {
    // Display data
}

Inversione del controllo

Un altro modo per evitare di passare dipendenze non necessarie a un componibile è tramite l'inversione del controllo. Il discendente invece di assumere una dipendenza per eseguire una logica,

Vedi l'esempio seguente in cui un discendente deve attivare la richiesta per caricare alcuni dati:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel)
}

@Composable
fun MyDescendant(myViewModel: MyViewModel) {
    Button(onClick = { myViewModel.loadData() }) {
        Text("Load data")
    }
}

A seconda dei casi, MyDescendant potrebbe avere molte responsabilità. Inoltre, il passaggio di MyViewModel come dipendenza rende MyDescendant meno riutilizzabile, poiché ora vengono accoppiati. Considera l'alternativa che non trasmette la dipendenza al discendente e utilizza l'inversione dei principi di controllo che rendono il predecessore responsabile dell'esecuzione della logica:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusableLoadDataButton(
        onLoadClick = {
            myViewModel.loadData()
        }
    )
}

@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
    Button(onClick = onLoadClick) {
        Text("Load data")
    }
}

Questo approccio può essere più adatto per alcuni casi d'uso poiché disaccoppia l'asset figlio dai suoi predecessori immediati. Gli elementi componibili precedenti tendono a diventare più complessi in favore di quelli di livello inferiore più flessibili.

Analogamente, @Composable lambda di contenuti possono essere utilizzati allo stesso modo per ottenere gli stessi vantaggi:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(
        content = {
            Button(
                onClick = {
                    myViewModel.loadData()
                }
            ) {
                Text("Confirm")
            }
        }
    )
}

@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}