CompositionLocal è uno strumento per passare i dati in modo implicito attraverso la composizione. In questa pagina scoprirai in modo più dettagliato cos'è un CompositionLocal, come crearne uno e se un CompositionLocal è una buona soluzione per il tuo caso d'uso.CompositionLocal
Introduzione a CompositionLocal
In genere, in Compose i dati scorrono verso il basso attraverso l'albero dell'UI come parametri per ogni funzione componibile. In questo modo, le dipendenze di un elemento componibile sono esplicite. Tuttavia, questa operazione può essere complessa per i dati utilizzati molto spesso e ampiamente, come i colori o gli stili di tipo. Vedi l'esempio seguente:
@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 degli elementi componibili, Compose offre CompositionLocal, che consente di creare oggetti denominati con ambito albero che possono essere utilizzati come modo implicito per far fluire i dati attraverso l'albero dell'UI.
In genere, gli elementi CompositionLocal vengono forniti con un valore in un determinato nodo dell'albero dell'UI. Questo valore può essere utilizzato dai relativi elementi componibili discendenti senza dichiarare CompositionLocal come parametro nella funzione componibile.
Termini chiave: in questa guida utilizziamo i termini composizione, albero dell'UI e gerarchia dell'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 dell'UI o la gerarchia dell'UI è l'albero di
LayoutNodecostruito, aggiornato e gestito dal processo di composizione.
CompositionLocal è ciò che il tema Material utilizza in background.
MaterialTheme è un oggetto che fornisce tre istanze CompositionLocal: colorScheme, typography e shapes, che ti consentono 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 colorScheme, shapes e typography di MaterialTheme.
@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 current valore
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
content lambda 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 del contenuto 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") }
CompositionLocalExample.Nell'ultimo esempio, le istanze CompositionLocal sono state utilizzate internamente dagli elementi componibili Material. Per accedere al valore corrente di un CompositionLocal,
utilizza la relativa current
proprietà. Nell'esempio seguente, il valore Context corrente di LocalContext CompositionLocal di uso comune nelle app 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 proprio CompositionLocal
CompositionLocal è uno strumento per passare i dati in modo implicito attraverso 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é rendere consapevoli questi livelli intermedi limiterebbe l'
utilità dell'elemento componibile. Ad esempio, l'esecuzione di query per le autorizzazioni Android è
consentita da un CompositionLocal in background. Un elemento componibile di selezione dei contenuti multimediali
può aggiungere nuove funzionalità per accedere ai contenuti protetti da autorizzazioni sul
dispositivo senza modificare la relativa 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 elemento componibile. Poiché creano dipendenze implicite, i chiamanti degli elementi componibili che li utilizzano devono assicurarsi che venga fornito un valore per ogni CompositionLocal.
Inoltre, potrebbe non esistere un'unica fonte di verità per questa dipendenza, poiché può subire mutazioni in qualsiasi parte della composizione. Pertanto, il debug dell'app in caso di problemi può essere più difficile , poiché devi spostarti verso l'alto 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 alcune condizioni che possono rendere CompositionLocal una buona soluzione per il tuo caso d'uso:
Un CompositionLocal deve avere un buon valore predefinito. 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.
Se non fornisci un valore predefinito, possono verificarsi problemi e frustrazioni quando crei test o visualizzi in anteprima un elemento componibile che utilizza CompositionLocal, poiché sarà sempre necessario fornirlo in modo esplicito.
Evita CompositionLocal per i concetti che non sono considerati con ambito albero o
con ambito 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 considerare prima di creare un
CompositionLocal.
Un esempio di pratica errata è la creazione di un CompositionLocal che contiene il ViewModel di una schermata specifica, in modo che tutti gli elementi componibili in quella schermata possano ottenere un riferimento a ViewModel per eseguire una logica. Questa è una pratica errata perché non tutti gli elementi componibili sotto un determinato albero dell'UI devono conoscere un ViewModel. La best practice consiste nel passare agli elementi componibili solo le informazioni
di cui hanno bisogno seguendo il pattern in cui
lo stato scorre verso il basso e gli eventi scorrono verso l'alto.
Questo approccio renderà gli elementi componibili più riutilizzabili e più facili da testare.
Creare un CompositionLocal
Esistono due API per creare un CompositionLocal:
compositionLocalOf: la modifica del valore fornito durante la ricomposizione invalida solo il contenuto che legge il relativo valorecurrent.staticCompositionLocalOf: a differenza dicompositionLocalOf, le letture di unstaticCompositionLocalOfnon vengono monitorate da Compose. La modifica del valore comporta la ricomposizione dell'intera lambdacontentin cui viene fornitoCompositionLocal, anziché solo i punti in cui il valorecurrentviene 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 prestazioni.
Ad esempio, il sistema di progettazione di un'app potrebbe essere orientato al modo in cui gli elementi componibili vengono elevati utilizzando un'ombra per il componente UI. Poiché le diverse elevazioni dell'app devono propagarsi in tutto l'albero dell'UI, utilizziamo un 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() }
Fornire valori a un CompositionLocal
L'elemento componibile CompositionLocalProvider
associa i valori alle istanze CompositionLocal per la gerarchia specificata. Per fornire un nuovo valore a un CompositionLocal, utilizza la
provides
funzione infissa 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 } } } }
Utilizzare 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
Essere espliciti sulle dipendenze degli elementi componibili è una buona abitudine. Ti consigliamo di passare agli elementi componibili solo ciò di cui hanno bisogno. Per incoraggiare il disaccoppiamento e il riutilizzo degli elementi componibili, ogni elemento componibile deve contenere la minor quantità possibile di informazioni.
@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 elemento componibile è utilizzare l'inversione del controllo. Anziché che il discendente accetti una dipendenza per eseguire una logica, lo fa il genitore.
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. 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 ad alcuni casi d'uso, in quanto disaccoppia il figlio dai relativi antenati immediati. Gli elementi componibili antenati tendono a diventare più complessi a favore di elementi componibili di livello inferiore più flessibili.
Allo stesso modo, le lambda di 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
- Anatomia di un tema in Compose
- Utilizzare le visualizzazioni in Compose
- Kotlin per Jetpack Compose