Dati con ambito locale con ComposizioneLocal

CompositionLocal è uno strumento per trasmettere i dati in modo implicito nella composizione. In questa pagina scoprirai in modo più dettagliato che cos'è un CompositionLocal, come crearne uno e se un CompositionLocal è una buona soluzione per il tuo caso d'uso.CompositionLocal

Ti presentiamo CompositionLocal

In genere, in Compose, i dati fluiscono verso il basso attraverso la struttura ad albero dell'interfaccia utente come parametri per ogni funzione componibile. In questo modo, le dipendenze di un composable diventano esplicite. Tuttavia, può essere complicato per i dati molto frequentemente e ampiamente utilizzati, come i colori o gli stili di carattere. 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 non dover passare i colori come dipendenza di parametro esplicita alla maggior parte dei composabili, Compose offre CompositionLocal che consente di creare oggetti denominati con ambito a albero che possono essere utilizzati come modo implicito per far fluire i dati nell'albero dell'interfaccia utente.

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

CompositionLocal è ciò che viene utilizzato dal tema Material. MaterialTheme è un oggetto che fornisce tre istanze di CompositionLocal: colorScheme, typography e shapes, che puoi recuperare in un secondo momento in qualsiasi parte discendente della composizione. Nello specifico, 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 di CompositionLocal è limitata a una parte della composizione, in modo da poter fornire valori diversi a diversi livelli dell'albero. Il valore current di un CompositionLocal corrisponde al valore più vicino fornito da un progenitore in quella parte della composizione.

Per fornire un nuovo valore a un CompositionLocal, utilizza il CompositionLocalProvider e la relativa funzione infix 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, Componi ricompone le parti della composizione che leggono CompositionLocal.

Ad esempio, LocalContentColor CompositionLocal contiene il colore preferito dei contenuti utilizzato per il testo e l'iconografia per garantire che sia in 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")
}

Figura 1. Anteprima del composable CompositionLocalExample.

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

Creare un CompositionLocal personalizzato

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

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

Tuttavia, CompositionLocal non è sempre la soluzione migliore. sconsigliamo di abusare di CompositionLocal perché presenta alcuni svantaggi:

CompositionLocal rende più difficile il ragionamento sul comportamento di un composable. Poiché creano dipendenze implicite, gli utenti dei composabili che li utilizzano devono assicurarsi che venga soddisfatto un valore per ogni CompositionLocal.

Inoltre, potrebbe non esserci una fonte attendibile per questa dipendenza, in quanto può essere modificata in qualsiasi parte della composizione. Pertanto, il debug dell'app in caso di problema può essere più complesso, in quanto devi risalire nella Composizione per vedere dove è stato fornito il valore current. Strumenti come Trova utilizzi nell'IDE o l'ispettore del layout di Compose forniscono informazioni sufficienti per attenuare questo problema.

Decidere se utilizzare CompositionLocal

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

Un CompositionLocal deve avere un buon valore predefinito. Se non è presente 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 indicazione di un valore predefinito può causare problemi e frustrazione durante la creazione di test o l'anteprima di un composable che lo utilizza. CompositionLocal richiederà sempre di essere fornito esplicitamente.

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

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

Un esempio di cattiva prassi è la creazione di un CompositionLocal che contenga il ViewModel di una determinata schermata in modo che tutti i composabili al suo interno possano ricevere un riferimento al ViewModel per eseguire una certa logica. Questa è una cattiva prassi perché non tutti i composabili sotto una determinata struttura ad albero dell'interfaccia utente devono conoscere un ViewModel. La best practice consiste nel passare ai composabili solo le informazioni di cui hanno bisogno seguendo lo schema in cui lo stato scorre verso il basso e gli eventi verso l'alto. Questo approccio renderà i tuoi composabili più riutilizzabili e più facili da testare.

Creazione di un CompositionLocal

Esistono due API per creare un CompositionLocal:

  • compositionLocalOf: la modifica del valore fornito durante la ricompozione invalida soltanto i contenuti che leggono il suo current valore.

  • staticCompositionLocalOf: a differenza di compositionLocalOf, le letture di un staticCompositionLocalOf non vengono monitorate da Compose. La modifica del valore comporta la ricompozione dell'intera lambda content in cui viene fornito CompositionLocal, anziché solo dei punti in cui viene letto il valore current 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 opinabile per il modo in cui i composabili vengono elevati utilizzando un'ombra per il componente dell'interfaccia utente. Poiché le diverse elevazioni dell'app devono propagarsi nell'intera struttura ad albero dell'interfaccia utente, utilizziamo un CompositionLocal. Poiché il valore CompositionLocal viene dedotto 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 composable CompositionLocalProvider associa i valori alle istanze CompositionLocal per la gerarchia data. Per fornire un nuovo valore a un CompositionLocal, utilizza la funzione infix 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
            }
        }
    }
}

Consumare 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.

Passare parametri espliciti

È buona norma dichiarare esplicitamente le dipendenze del composable. Ti consigliamo di passare ai composabili solo ciò di cui hanno bisogno. Per incoraggiare il disaccoppiamento e il riutilizzo dei composabili, ogni componibile deve contenere la minima 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 di controllo

Un altro modo per evitare di passare dipendenze non necessarie a un composable è tramite l'inversione del controllo. Anziché il discendente che accetta una dipendenza per eseguire una logica, è il genitore a farlo.

Guarda 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, se passi MyViewModel come dipendenza, MyDescendant diventa meno riutilizzabile poiché ora sono accoppiati. Prendi in considerazione 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 scollega il figlio dai suoi antenati immediati. I composabili di primo livello tendono a diventare più complessi a favore di composabili di livello inferiore più flessibili.

Analogamente, i lambda dei contenuti @Composable possono essere utilizzati 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()
    }
}