Dati con ambito locale con ComposizioneLocal

CompositionLocal è uno strumento per passare i dati in modo implicito attraverso la composizione. In questa pagina scoprirai più nel dettaglio cos'è un CompositionLocal, come creare il tuo CompositionLocal e se un CompositionLocal è una buona soluzione per il tuo caso d'uso.

Introduzione a CompositionLocal

In genere, in Compose, i dati fluiscono verso il basso attraverso l'albero della UI come parametri per ogni funzione componibile. In questo modo, le dipendenze di un composable sono esplicite. Tuttavia, questo può essere complicato per i dati utilizzati molto di frequente e ampiamente, come i colori o gli stili dei caratteri. Vedi l'esempio di seguito:

@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 evitare di dover passare i colori come dipendenza esplicita dei parametri alla maggior parte dei componibili, Compose offre CompositionLocal, che ti consente di creare oggetti con nome con ambito ad albero che possono essere utilizzati come modo implicito per far fluire i dati attraverso l'albero della UI.

Gli elementi CompositionLocal vengono solitamente forniti con un valore in un determinato nodo dell'albero della UI. Questo valore può essere utilizzato dai suoi discendenti componibili senza dichiarare CompositionLocal come parametro nella funzione componibile.

Termini chiave: in questa guida utilizziamo i termini composizione, albero della UI e gerarchia della UI. Sebbene possano essere utilizzati in modo intercambiabile in altre guide, hanno significati diversi:

  • La composizione è il record del grafico delle chiamate delle funzioni componibili.
  • L'albero della UI o la gerarchia della UI è l'albero di LayoutNode creato, aggiornato e gestito dal processo di composizione.

CompositionLocal è ciò che i temi di Material utilizzano in background. MaterialTheme è un oggetto che fornisce tre istanze CompositionLocal: colorScheme, typography e shapes, consentendoti di recuperarle in un secondo momento in qualsiasi parte discendente della composizione. In particolare, si tratta delle proprietà LocalColorScheme, LocalShapes e LocalTypography a cui puoi accedere tramite gli attributi MaterialTheme colorScheme, shapes e typography.

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colorScheme, 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.colorScheme.primary
    )
}

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

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

Ad esempio, LocalContentColor CompositionLocal contiene il colore dei contenuti preferito utilizzato per il testo e l'iconografia per garantire il contrasto con il colore di sfondo corrente. Nell'esempio seguente, CompositionLocalProvider viene utilizzato per fornire valori diversi per parti diverse della composizione.

@Composable
fun CompositionLocalExample() {
    MaterialTheme {
        // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default
        // This is to automatically make text and other content contrast to the background
        // correctly.
        Surface {
            Column {
                Text("Uses Surface's provided content color")
                CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
                    Text("Primary color provided by LocalContentColor")
                    Text("This Text also uses primary as textColor")
                    CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
                        DescendantExample()
                    }
                }
            }
        }
    }
}

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

Anteprima del componibile CompositionLocalExample.
Figura 1. Anteprima del componibile CompositionLocalExample.

Nell'ultimo esempio, le istanze CompositionLocal sono state utilizzate internamente dai componibili Material. Per accedere al valore corrente di un CompositionLocal, utilizza la relativa proprietà current. Nell'esempio seguente, il valore Context corrente di LocalContext CompositionLocal, di uso comune 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)
}

Crea il tuo artefatto: CompositionLocal

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

Un altro indicatore chiave per l'utilizzo di CompositionLocal è quando il parametro è trasversale e gli strati intermedi di implementazione non devono essere consapevoli della sua esistenza, perché renderli consapevoli limiterebbe l'utilità del composable. Ad esempio, l'esecuzione di query per le autorizzazioni Android è supportata da un CompositionLocal in background. Un composable del selettore di contenuti multimediali può aggiungere nuove funzionalità per accedere ai contenuti protetti da autorizzazioni sul dispositivo senza modificare la sua API e richiedere ai chiamanti del selettore di contenuti multimediali di essere a conoscenza di questo contesto aggiuntivo utilizzato dall'ambiente.

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

CompositionLocal rende più difficile comprendere il comportamento di un composable. Poiché creano dipendenze implicite, i chiamanti dei componibili che li utilizzano devono assicurarsi che venga fornito un valore per ogni CompositionLocal.

Inoltre, potrebbe non esserci una fonte di attendibilità chiara per questa dipendenza, in quanto 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 risalire la composizione per vedere dove è stato fornito il valore current. Strumenti come Trova utilizzi nell'IDE o Layout Inspector di Compose forniscono informazioni sufficienti per mitigare questo problema.

Decidi se utilizzare CompositionLocal

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

Un CompositionLocal deve avere un valore predefinito valido. Se non esiste un valore predefinito, devi garantire che sia estremamente difficile per uno sviluppatore trovarsi in una situazione in cui non viene fornito un valore per CompositionLocal. La mancata impostazione di un valore predefinito può causare problemi e frustrazione durante la creazione di test o l'anteprima di un componibile che utilizza quel CompositionLocal, che richiederà sempre di essere fornito in modo esplicito.

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

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 contiene la ViewModel di una determinata schermata in modo che tutti i componibili in quella schermata possano ottenere un riferimento alla ViewModel per eseguire una determinata logica. Questa è una pratica sconsigliata perché non tutti i componibili sotto un determinato albero della UI devono conoscere un ViewModel. La best practice consiste nel passare ai componibili solo le informazioni di cui hanno bisogno seguendo il pattern secondo cui lo stato scende e gli eventi salgono. Questo approccio renderà i tuoi componibili più riutilizzabili e facili da testare.

Crea un CompositionLocal

Esistono due API per creare un CompositionLocal:

  • compositionLocalOf: la modifica del valore fornito durante la ricomposizione invalida solo i contenuti che leggono il suo 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'intera lambda content in cui viene fornito 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 se non cambierà mai, utilizza staticCompositionLocalOf per ottenere vantaggi in termini di prestazioni.

Ad esempio, il sistema di progettazione di un'app potrebbe essere rigido nel modo in cui i componibili vengono elevati utilizzando un'ombra per il componente UI. Poiché le diverse elevazioni dell'app devono propagarsi nell'albero della UI, utilizziamo un CompositionLocal. Poiché il valore CompositionLocal viene ricavato in modo condizionale in base al tema di 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() }

Fornire valori a un CompositionLocal

Il componibile CompositionLocalProvider associa i valori alle istanze CompositionLocal per la gerarchia specificata. Per fornire un nuovo valore a un CompositionLocal, utilizza la funzione infissa 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 di CompositionLocal

CompositionLocal.current restituisce il valore fornito dal CompositionLocalProvider più vicino che fornisce un valore a quel CompositionLocal:

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

Alternative da considerare

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 CompositionLocal, è probabile che un'altra soluzione sia più adatta al tuo caso d'uso.

Passa parametri espliciti

Essere espliciti sulle dipendenze del componibile è una buona abitudine. Ti consigliamo di passare ai componibili solo ciò di cui hanno bisogno. Per incoraggiare il disaccoppiamento e il riutilizzo dei componibili, ogni componibile deve contenere la quantità minima 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 è utilizzare l'inversione del controllo. Invece di far sì che il discendente accetti una dipendenza per eseguire una logica, è il genitore a farlo.

Guarda l'esempio seguente in cui un discendente deve attivare la richiesta di caricamento di alcuni dati:

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

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

A seconda del caso, MyDescendant potrebbe avere molte responsabilità. Inoltre, il passaggio di MyViewModel come dipendenza rende MyDescendant meno riutilizzabile, poiché ora sono accoppiati. Considera l'alternativa che non passa la dipendenza al discendente e utilizza i principi di inversione del controllo che rendono l'antenato 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, in quanto disaccoppia l'elemento secondario dai suoi predecessori immediati. I componibili predecessori tendono a diventare più complessi per consentire una maggiore flessibilità dei componibili di livello inferiore.

Allo stesso modo, le lambda dei contenuti @Composable possono essere utilizzate nello 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()
    }
}