CompositionLocal
è uno strumento per trasmettere dati
in modo implicito alla composizione. In questa pagina scoprirai
cos'è un CompositionLocal
in maggiore dettaglio, come crearne uno tuo
CompositionLocal
e scoprirai se un CompositionLocal
è una buona soluzione
per il tuo caso d'uso.
Ti presentiamo CompositionLocal
Di solito, in Compose, i dati scorrono verso il basso nell'albero dell'interfaccia utente come parametri per ogni funzione componibile. Ciò rende esplicite le dipendenze di un componibile. Tuttavia, questa operazione può risultare difficile per dati molto frequenti e ampiamente utilizzati, come colori o stili di caratteri. Vedi l'esempio che segue:
@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 mancata necessità di passare i colori come dipendenza esplicita dei parametri alla maggior parte dei componibili, Compose offre CompositionLocal
che consente di creare oggetti denominati con albero ad albero che possono essere utilizzati come modo implicito per avere un flusso di dati attraverso la struttura dell'interfaccia utente.
In genere, agli elementi CompositionLocal
viene fornito un valore in un determinato nodo della struttura dell'interfaccia utente. Questo valore può essere utilizzato dai relativi discendenti componibili senza dichiarare CompositionLocal
come parametro nella funzione componibile.
CompositionLocal
è ciò che viene usato dal tema Material.
MaterialTheme
è un oggetto che fornisce tre istanze CompositionLocal
(colori, tipografia e forme), che ti consentono di recuperarle in un secondo momento in qualsiasi parte discendente della composizione. In particolare, queste sono le proprietà LocalColors
, LocalShapes
e
LocalTypography
a cui puoi accedere tramite gli attributi MaterialTheme
colors
, shapes
e typography
.
@Composable fun MyApp() { // Provides a Theme whose values are propagated down its `content` MaterialTheme { // New values for colors, 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.colors.primary ) }
Un'istanza CompositionLocal
ha come ambito una parte della composizione, così puoi fornire valori diversi a diversi livelli dell'albero. Il valore current
di un valore CompositionLocal
corrisponde al valore più vicino fornito da un
predecessore in quella parte della composizione.
Per fornire un nuovo valore a un valore CompositionLocal
, utilizza CompositionLocalProvider
e la relativa funzione infisso provides
che associa una chiave CompositionLocal
a un value
. Il lambda
content
di CompositionLocalProvider
riceverà il valore
fornito quando accedi alla proprietà current
di CompositionLocal
. Quando viene fornito un nuovo valore, Compose ricompone le parti della composizione che leggono il CompositionLocal
.
Ad esempio, LocalContentAlpha
CompositionLocal
contiene la versione alpha dei contenuti preferiti utilizzata per testo e icone per enfatizzare o ridurre le diverse parti dell'interfaccia utente. Nell'esempio seguente, CompositionLocalProvider
viene utilizzato per fornire valori diversi per parti diverse della composizione.
@Composable fun CompositionLocalExample() { MaterialTheme { // MaterialTheme sets ContentAlpha.high as default Column { Text("Uses MaterialTheme's provided alpha") CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { Text("Medium value provided for LocalContentAlpha") Text("This Text also uses the medium value") CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) { DescendantExample() } } } } } @Composable fun DescendantExample() { // CompositionLocalProviders also work across composable functions Text("This Text uses the disabled alpha now") }
Figura 1. Anteprima del componibile CompositionLocalExample
.
In tutti gli esempi precedenti, le istanze CompositionLocal
sono state utilizzate internamente dai componenti componibili Material. Per accedere al valore corrente di un elemento CompositionLocal
,
utilizza la relativa proprietà current
. Nell'esempio seguente, l'attuale valore Context
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) }
Creazione del tuo CompositionLocal
in corso...
CompositionLocal
è uno strumento per trasmettere dati tramite la composizione
in modo implicito.
Un altro indicatore chiave per l'utilizzo di CompositionLocal
è quando il parametro è trasversale e i livelli di implementazione intermedi non dovrebbero rendersi conto della sua esistenza, perché rendere consapevoli questi livelli intermedi limiterebbe l'utilità del componibile. Ad esempio, l'esecuzione di query sulle autorizzazioni Android
è offerta da un CompositionLocal
in background. Un componibile selettore media
può aggiungere una nuova funzionalità per accedere ai contenuti protetti da autorizzazione sul
dispositivo senza modificare la relativa API e richiedere che i chiamanti del selettore media
conoscano questo contesto aggiuntivo utilizzato dall'ambiente.
Tuttavia, CompositionLocal
non è sempre la soluzione migliore. Sconsigliamo di utilizzare in modo eccessivo CompositionLocal
in quanto presenta alcuni svantaggi:
CompositionLocal
rende più difficile ragionare sul comportamento di un componibile. Poiché creano dipendenze implicite, i chiamanti dei componibili che le utilizzano devono assicurarsi che sia soddisfatto un valore per ogni elemento CompositionLocal
.
Inoltre, potrebbe non esserci una chiara fonte di verità per questa dipendenza, poiché
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 scorrere nella
Composizione per vedere dove è stato fornito il valore current
. Strumenti come Trova utilizzi nell'IDE o Strumento di controllo del layout di Scrivi forniscono informazioni sufficienti per mitigare questo problema.
Decisione sull'utilizzo di CompositionLocal
Esistono determinate condizioni che possono rendere CompositionLocal
una buona soluzione
per il tuo caso d'uso:
Il valore CompositionLocal
dovrebbe avere un buon valore predefinito. In assenza di 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
.
Se non specifichi un valore predefinito, potresti riscontrare problemi e frustrazione durante la creazione dei test o l'anteprima di un componibile in cui CompositionLocal
ne richiede sempre la specifica.
Evita CompositionLocal
per i concetti che non sono considerati con ambito ad albero o gerarchia. Un CompositionLocal
ha senso quando può essere
potenzialmente utilizzato da qualsiasi discendente, non da alcuni di loro.
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 contenga il ViewModel
di una determinata schermata in modo che tutti i componibili nella schermata possano ottenere un riferimento a ViewModel
per eseguire alcune logiche. Questa è una cattiva pratica perché non tutti i componibili sotto un determinato albero dell'interfaccia utente devono conoscere un elemento ViewModel
. È buona norma passare ai componibili solo le informazioni di cui hanno bisogno seguendo il pattern in cui lo stato fluisce e gli eventi scorrono. Questo approccio renderà i componibili più
riutilizzabili e più facili da testare.
Creazione di un CompositionLocal
in corso...
Per creare un CompositionLocal
sono disponibili due API:
compositionLocalOf
: la modifica del valore fornito durante la ricomposizione rende la validità solo del 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'intero lambdacontent
, in cui viene fornito il valoreCompositionLocal
, 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 "guidato" nel modo in cui i componenti componibili vengono elevati usando un'ombreggiatura per il componente dell'interfaccia utente. Poiché le diverse elevazioni dell'app dovrebbero propagarsi nell'albero dell'interfaccia utente, utilizziamo un elemento 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() }
Specificare valori per un CompositionLocal
L'elemento componibile CompositionLocalProvider
associa i valori a CompositionLocal
istanze per la gerarchia specificata. Per fornire un nuovo valore a CompositionLocal
, utilizza la funzione infix di 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 del CompositionLocal
CompositionLocal.current
restituisce il valore fornito dal valore CompositionLocalProvider
più prossimo che fornisce un valore per il valore CompositionLocal
:
@Composable fun SomeComposable() { // Access the globally defined LocalElevations variable to get the // current Elevations in this part of the Composition Card(elevation = LocalElevations.current.card) { // Content } }
Alternative da valutare
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 ComposeLocal, è probabile che un'altra soluzione sia più adatta al tuo caso d'uso.
Trasmettere parametri espliciti
Essere espliciti riguardo alle dipendenze componibili è una buona abitudine. Ti consigliamo di trasmettere i componibili solo ciò di cui hanno bisogno. Per incoraggiare il disaccoppiamento e il riutilizzo dei componibili, ogni componibile dovrebbe contenere la minor 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 del controllo
Un altro modo per evitare di passare dipendenze non necessarie a un componibile è tramite l'inversione del controllo. Il discendente invece di assumere una dipendenza per eseguire una logica,
Vedi 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,
il passaggio di MyViewModel
come dipendenza rende MyDescendant
meno riutilizzabile, poiché
ora vengono accoppiati. Considera l'alternativa che non trasmette la dipendenza al discendente e utilizza l'inversione dei principi di controllo che rendono 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 per alcuni casi d'uso poiché disaccoppia l'asset figlio dai suoi predecessori immediati. Gli elementi componibili precedenti tendono a diventare più complessi in favore di quelli di livello inferiore più flessibili.
Analogamente, @Composable
lambda di contenuti possono essere utilizzati allo 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() } }
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Struttura di un tema in Compose
- Utilizzare le visualizzazioni in Compose
- Kotlin per Jetpack Compose