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 composable. In questo modo, le dipendenze di un composable diventano esplicite. Tuttavia, questo 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 esplicita del parametro alla maggior parte dei componibili, Compose offre CompositionLocal
, che consente di creare oggetti con nome con ambito ad albero che possono essere utilizzati come un modo implicito per far passare i dati attraverso l'albero dell'UI.
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 dichiarare CompositionLocal
come parametro nella funzione componibile.
CompositionLocal
è ciò che viene utilizzato dal tema Material.
MaterialTheme
è
un oggetto che fornisce tre istanze CompositionLocal
: colorScheme
,
typography
e shapes
, che ti consente di recuperarle in un secondo momento in qualsiasi parte
discendente della composizione.
Nello specifico, queste sono le 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 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
. Il
lambda content
di CompositionLocalProvider
riceverà il valore
fornito quando accede alla proprietà current
di CompositionLocal
. Quando
viene fornito un nuovo valore, Compose ricompone 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 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, per formattare il testo viene usato l'attuale valore Context
di LocalContext
CompositionLocal
, comunemente usato nelle app Android:
@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 selettore media componibile può aggiungere nuove funzionalità per accedere a contenuti protetti da autorizzazioni sul dispositivo senza modificare la relativa API e richiedere ai chiamanti del selettore media di essere a conoscenza 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. Quando
creano dipendenze implicite, i chiamanti dei componibili che le usano devono
verificare che sia 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 entrare in una situazione in cui non viene fornito un valore per CompositionLocal
.
Non fornire un valore predefinito può causare problemi e frustrazioni durante la creazione di test o l'anteprima di un componibile che utilizza sempre CompositionLocal
, che dovrà sempre 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 pratica è la creazione di un CompositionLocal
che contenga il ViewModel
di una determinata schermata in modo che tutti i composabili in quella schermata possano ottenere 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
. È buona norma passare ai componibili solo le informazioni di cui hanno bisogno seguendo lo schema secondo cui lo stato scorrono verso il basso e gli eventi scorrono verso l'alto. Questo approccio renderà i componibili 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 ricomposizione invalida solo il contenuto che legge il relativo 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 viene letto il valorecurrent
nella composizione.
Se è molto probabile che il valore fornito a CompositionLocal
subisca modifiche o
non cambierà mai, utilizza staticCompositionLocalOf
per ottenere vantaggi in termini di prestazioni.
Ad esempio, il sistema di progettazione di un'app potrebbe essere ottimizzato nel modo in cui gli elementi componibili vengono elevati utilizzando un'ombra per il componente dell'interfaccia utente. Poiché le diverse
elevazioni per l'app dovrebbero propagarsi nell'albero della UI, utilizziamo
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 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 ComposizioneLocal, è probabile che un'altra soluzione sia più adatta al 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,
passare MyViewModel
come dipendenza rende MyDescendant
meno riutilizzabile
dato che ora sono accoppiati. Considera l'alternativa che non passa la dipendenza al discendente e utilizza l'inversione dei principi di controllo che rende 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 ad alcuni casi d'uso, in quanto disaccoppia l'asset secondario dai suoi predecessori immediati. I componenti componibili predecessori tendono a diventare più complessi in favore di componibili più flessibili di livello inferiore.
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() } }
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Struttura di un tema in Scrivi
- Utilizzare le visualizzazioni in Scrittura
- Kotlin per Jetpack Compose