CompositionLocal
è uno strumento per
trasferire i dati in modo implicito tramite la composizione. In questa pagina scoprirai
che cos'è un CompositionLocal
in modo più dettagliato, come creare il tuo
CompositionLocal
e se un CompositionLocal
è una buona soluzione per
il tuo caso d'uso.
Ti presentiamo CompositionLocal
In genere in Compose, i dati scorrono verso il basso attraverso l'albero dell'interfaccia utente come parametri di ogni funzione componibile. In questo modo le dipendenze di un composable sono esplicite. Tuttavia, questa operazione può essere complessa per i dati utilizzati molto spesso e ampiamente, come colori o stili di carattere. Vedi il seguente esempio:
@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 necessità di non trasmettere i colori come dipendenza esplicita dei parametri alla maggior parte dei composable, Compose offre CompositionLocal
, che ti consente di creare oggetti denominati con ambito ad albero che possono essere utilizzati come modo implicito per far fluire i dati attraverso l'albero dell'interfaccia utente.
Gli elementi CompositionLocal
vengono solitamente forniti con un valore in un determinato nodo
dell'albero della UI. Questo valore può essere utilizzato dai relativi discendenti componibili senza
dichiarare CompositionLocal
come parametro nella funzione componibile.
CompositionLocal
è ciò che utilizza il tema Material.
MaterialTheme
è
un oggetto che fornisce tre istanze di CompositionLocal
: colorScheme
,
typography
e shapes
, che ti consentono di recuperarle 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 CompositionLocal
è limitata a 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 antenato in quella parte della composizione.
Per fornire un nuovo valore a un CompositionLocal
, utilizza la
funzione infissa CompositionLocalProvider
e il relativo provides
che associa una chiave CompositionLocal
a un value
. La
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
le icone per garantire il contrasto con il colore di sfondo corrente. Nell'esempio
seguente, CompositionLocalProvider
viene utilizzato per fornire valori diversi
per le diverse parti 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 composable Material. Per accedere al valore attuale di un CompositionLocal
,
utilizza la 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 il tuo CompositionLocal
CompositionLocal
è uno strumento per passare i dati attraverso la composizione
in modo implicito.
Un altro segnale chiave per l'utilizzo di CompositionLocal
è quando il parametro è
trasversale e i livelli di implementazione intermedi non devono essere a conoscenza
della sua esistenza, perché rendere consapevoli questi livelli intermedi limiterebbe l'utilità
del componente componibile. Ad esempio, l'interrogazione delle autorizzazioni Android è
consentita da un CompositionLocal
. Un composable selettore di contenuti multimediali
può aggiungere nuove funzionalità per accedere a contenuti protetti da autorizzazioni sul
dispositivo senza modificarne l'API e richiedere ai chiamanti del selettore di contenuti multimediali di
essere a conoscenza di questo contesto aggiunto utilizzato dall'ambiente.
Tuttavia, CompositionLocal
non è sempre la soluzione migliore. Sconsigliamo di utilizzare eccessivamente CompositionLocal
perché presenta alcuni svantaggi:
CompositionLocal
rende più difficile ragionare sul comportamento di un composable. Poiché
creano dipendenze implicite, i chiamanti dei composable che li utilizzano devono
assicurarsi che venga fornito un valore per ogni CompositionLocal
.
Inoltre, potrebbe non esserci una fonte di verità chiara per questa dipendenza, in quanto
può mutare in qualsiasi parte della composizione. Pertanto, il debug dell'app quando si verifica un problema può essere più difficile, in quanto devi navigare nella composizione per vedere dove è stato fornito il valore current
. Strumenti come Trova
utilizzi nell'IDE o lo strumento di ispezione del layout di Compose forniscono informazioni sufficienti per
mitigare 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
.
Se non fornisci un valore predefinito, possono verificarsi problemi e frustrazioni durante la creazione
di test o l'anteprima di un componente componibile che utilizza 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 a gerarchia secondaria. Un CompositionLocal
ha senso quando può essere
potenzialmente utilizzato da qualsiasi discendente, non da alcuni.
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 prassi scorretta è la creazione di un CompositionLocal
che contiene il
ViewModel
di una determinata schermata in modo che tutti i componenti combinabili in quella schermata possano
ottenere un riferimento al ViewModel
per eseguire una logica. Si tratta di una pratica sconsigliata
perché non tutti i composable sotto un determinato albero dell'interfaccia utente devono conoscere un
ViewModel
. La best practice consiste nel passare ai composable solo le informazioni
di cui hanno bisogno seguendo il pattern stato verso il basso ed eventi verso l'alto. Questo approccio renderà i tuoi composable più
riutilizzabili e più facili da testare.
Creazione di un CompositionLocal
in corso…
Esistono due API per creare un CompositionLocal
:
compositionLocalOf
: La modifica del valore fornito durante la ricomposizione invalida solo il contenuto che legge il suo valorecurrent
.staticCompositionLocalOf
: A differenza dicompositionLocalOf
, le letture di unstaticCompositionLocalOf
non vengono monitorate da Compose. La modifica del valore comporta la ricomposizione dell'intera lambdacontent
in cui viene fornitoCompositionLocal
, anziché solo dei punti in cui il valorecurrent
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 orientato al modo in cui i componenti componibili
vengono elevati utilizzando un'ombra per il componente UI. Poiché le diverse
elevazioni per l'app devono propagarsi in tutto l'albero della UI, utilizziamo un
CompositionLocal
. Poiché il valore di 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() }
Fornire valori a un CompositionLocal
Il composable 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
nel seguente modo:
// 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 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, un'altra soluzione potrebbe essere più adatta
al tuo caso d'uso.
Trasmettere parametri espliciti
Essere espliciti sulle dipendenze di un composable è una buona abitudine. Ti consigliamo di passare ai composable solo ciò di cui hanno bisogno. Per favorire il disaccoppiamento e il riutilizzo dei composable, ogni composable 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 composable è tramite l'inversione del controllo. Anziché che il discendente acquisisca una dipendenza per eseguire una logica, è il genitore a farlo.
Vedi 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. Prendi in considerazione l'alternativa che non passa la
dipendenza nel 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 ad alcuni casi d'uso, in quanto disaccoppia il figlio dai suoi antenati immediati. I composable padre tendono a diventare più complessi a favore di composable di livello inferiore più flessibili.
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() } }
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Struttura di un tema in Compose
- Utilizzare le visualizzazioni in Scrivi
- Kotlin per Jetpack Compose