CompositionLocal ile yerel kapsamlı veriler

CompositionLocal, verileri Composition üzerinden örtülü olarak aktarmak için kullanılan bir araçtır. Bu sayfada, CompositionLocal hakkında daha ayrıntılı bilgi edinecek, kendi CompositionLocal öğenizi nasıl oluşturacağınızı öğrenecek ve CompositionLocal öğesinin kullanım alanınız için iyi bir çözüm olup olmadığını anlayacaksınız.

CompositionLocal ile tanışın

Genellikle Compose'da veriler, kullanıcı arayüzü ağacında her bir composable işlevine parametre olarak aşağı doğru akar. Bu, composable'ın bağımlılıklarını açık hale getirir. Ancak bu durum, renkler veya yazı tipi stilleri gibi çok sık ve yaygın olarak kullanılan veriler için zahmetli olabilir. Aşağıdaki örneğe bakın:

@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
    )
}

Renklerin çoğu composable'a açık parametre bağımlılığı olarak iletilmesine gerek olmaması için Compose, CompositionLocal sunar. Bu, verilerin kullanıcı arayüzü ağında akmasını sağlamak için örtülü bir yol olarak kullanılabilecek ağ kapsamlı adlandırılmış nesneler oluşturmanıza olanak tanır.

CompositionLocal öğeleri genellikle kullanıcı arayüzü ağacının belirli bir düğümünde bir değerle birlikte sağlanır. Bu değer, CompositionLocal parametresi composable işlevinde bildirilmeden composable öğenin alt öğeleri tarafından kullanılabilir.

CompositionLocal, Material temasının arka planda kullandığı teknolojidir. MaterialTheme, üç CompositionLocal örneği sağlayan bir nesnedir: colorScheme, typography ve shapes. Bu örnekleri daha sonra Kompozisyon'un herhangi bir alt bölümünde alabilirsiniz. Bunlar, MaterialTheme colorScheme, shapes ve typography özellikleri aracılığıyla erişebileceğiniz LocalColorScheme, LocalShapes ve LocalTypography özellikleridir.

@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
    )
}

CompositionLocal örneği, Kompozisyonun bir bölümüyle sınırlıdır. Bu nedenle, ağacın farklı seviyelerinde farklı değerler sağlayabilirsiniz. Bir CompositionLocal öğesinin current değeri, Kompozisyonun o bölümündeki bir üst öğe tarafından sağlanan en yakın değere karşılık gelir.

Bir CompositionLocal için yeni bir değer sağlamak üzere CompositionLocalProvider ve provides bir CompositionLocal anahtarını bir value ile ilişkilendiren infix işlevini kullanın. CompositionLocal öğesinin current özelliğine erişilirken CompositionLocalProvider öğesinin content lambda'sı sağlanan değeri alır. Yeni bir değer sağlandığında Compose, CompositionLocal okuyan Kompozisyon'un bölümlerini yeniden oluşturur.

Buna örnek olarak, LocalContentColor CompositionLocal, metin ve ikonografide kullanılan tercih edilen içerik rengini içerir. Bu renk, mevcut arka plan rengiyle kontrast oluşturur. Aşağıdaki örnekte, CompositionLocalProvider, kompozisyonun farklı bölümleri için farklı değerler sağlamak üzere kullanılır.

@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 composable'ın önizlemesi.
Şekil 1. CompositionLocalExample composable'ının önizlemesi.

Son örnekte, CompositionLocal örnekleri Material composable'ları tarafından dahili olarak kullanıldı. Bir CompositionLocal öğesinin geçerli değerine erişmek için current özelliğini kullanın. Aşağıdaki örnekte, metni biçimlendirmek için Android uygulamalarında yaygın olarak kullanılan LocalContext CompositionLocal öğesinin mevcut Context değeri kullanılmaktadır:

@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)
}

Kendi kitlenizi oluşturma CompositionLocal

CompositionLocal, verileri Composition üzerinden örtülü olarak aktarmak için kullanılan bir araçtır.

CompositionLocal kullanmanın bir diğer önemli sinyali, parametrenin kesişen olduğu ve uygulama ara katmanlarının bunun varlığından haberdar olmaması gerektiği durumlardır. Çünkü bu ara katmanların haberdar olması, birleştirilebilir öğenin faydasını sınırlar. Örneğin, Android izinleri için sorgu oluşturma işlemi, arka planda CompositionLocal tarafından sağlanır. Medya seçici composable'ı, API'sini değiştirmeden ve medya seçicinin arayanlarının ortamdan kullanılan bu ek bağlamın farkında olmasını gerektirmeden cihazdaki izin korumalı içeriğe erişmek için yeni işlevler ekleyebilir.

Ancak CompositionLocal her zaman en iyi çözüm değildir. CompositionLocal aşırı kullanımının bazı dezavantajları olduğundan bu durum önerilmez:

CompositionLocal, composable'ın davranışını anlamayı zorlaştırır. Bunlar örtülü bağımlılıklar oluşturduğundan, bunları kullanan composable'ların arayanları, her CompositionLocal için bir değerin karşılandığından emin olmalıdır.

Ayrıca, Kompozisyonun herhangi bir bölümünde değişebileceğinden bu bağımlılık için net bir doğruluk kaynağı olmayabilir. Bu nedenle, current değerinin nerede sağlandığını görmek için Composition'da yukarı gitmeniz gerektiğinden bir sorun oluştuğunda uygulamada hata ayıklamak daha zor olabilir. IDE'deki Kullanım yerlerini bul veya Compose düzen inceleyici gibi araçlar, bu sorunu azaltmak için yeterli bilgi sağlar.

CompositionLocal kullanıp kullanmayacağınıza karar verme

Kullanım alanınız için CompositionLocal'ı iyi bir çözüm haline getirebilecek belirli koşullar vardır:

CompositionLocal için iyi bir varsayılan değer olmalıdır. Varsayılan değer yoksa geliştiricinin CompositionLocal için değer sağlanmayan bir duruma girmesinin son derece zor olacağını garanti etmeniz gerekir. Varsayılan değer sağlamamak, test oluştururken veya bu CompositionLocal öğesini kullanan bir composable'ı önizlerken sorunlara ve hayal kırıklığına neden olabilir. Bu durumda, değerin her zaman açıkça sağlanması gerekir.

Ağaç kapsamlı veya alt hiyerarşi kapsamlı olarak düşünülmeyen kavramlar için CompositionLocal kullanmayın. CompositionLocal, birkaç torun tarafından değil, herhangi bir torun tarafından potansiyel olarak kullanılabildiğinde mantıklıdır.

Kullanım alanınız bu şartları karşılamıyorsa CompositionLocal oluşturmadan önce Dikkate alınabilecek alternatifler bölümüne göz atın.

Kötü bir uygulama örneği, belirli bir ekranın CompositionLocal değerini tutan bir ViewModel oluşturmaktır. Böylece, bu ekrandaki tüm composable'lar, bazı mantık işlemlerini gerçekleştirmek için ViewModel değerine referans alabilir. Bu, kötü bir uygulamadır. Çünkü belirli bir kullanıcı arayüzü ağacının altındaki tüm composable'ların ViewModel hakkında bilgi sahibi olması gerekmez. İyi uygulama, durum aşağı, etkinlikler yukarı akar kalıbını izleyerek yalnızca composable'lara ihtiyaç duydukları bilgileri iletmektir. Bu yaklaşım, composable işlevlerinizi daha fazla yeniden kullanılabilir ve test etmeyi kolaylaştırır.

CompositionLocal oluştur

CompositionLocal oluşturmak için iki API vardır:

  • compositionLocalOf: Yeniden oluşturma sırasında sağlanan değerin değiştirilmesi yalnızca current değerini okuyan içeriği geçersiz kılar.

  • staticCompositionLocalOf: compositionLocalOf'den farklı olarak, staticCompositionLocalOf'nin okunma sayısı Compose tarafından izlenmez. Değerin değiştirilmesi, CompositionLocal öğesinin sağlandığı content lambda'nın tamamının, yalnızca current değerinin Composition'da okunduğu yerler yerine yeniden oluşturulmasına neden olur.

CompositionLocal için sağlanan değerin değişme olasılığı çok düşükse veya hiç değişmeyecekse performans avantajlarından yararlanmak için staticCompositionLocalOf kullanın.

Örneğin, bir uygulamanın tasarım sisteminde, kullanıcı arayüzü bileşeni için gölge kullanılarak composable'ların yükseltilmesi konusunda belirli bir görüş olabilir. Uygulamanın farklı yükseklikleri kullanıcı arayüzü ağacına yayılması gerektiğinden CompositionLocal kullanıyoruz. CompositionLocal değeri, sistem temasına bağlı olarak koşullu şekilde elde edildiğinden compositionLocalOf API'sini kullanırız:

// 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() }

CompositionLocal için değerler sağlama

CompositionLocalProvider Composable'ı, belirli bir hiyerarşi için değerleri CompositionLocal örneklerine bağlar. CompositionLocal öğesine yeni bir değer sağlamak için CompositionLocal anahtarını value ile ilişkilendiren provides infix işlevini aşağıdaki şekilde kullanın:

// 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
            }
        }
    }
}

CompositionLocal tüketimi

CompositionLocal.current, CompositionLocal için değer sağlayan en yakın CompositionLocalProvider tarafından sağlanan değeri döndürür:

@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
    }
}

Değerlendirilebilecek alternatifler

CompositionLocal bazı kullanım alanları için aşırı bir çözüm olabilir. Kullanım alanınız, Deciding whether to use CompositionLocal (CompositionLocal'ı kullanıp kullanmayacağınıza karar verme) bölümünde belirtilen ölçütleri karşılamıyorsa kullanım alanınız için başka bir çözüm daha uygun olabilir.

Açık parametreleri iletme

Composable'ın bağımlılıkları hakkında açık olmak iyi bir alışkanlıktır. Composable işlevlere yalnızca ihtiyaç duydukları parametreleri iletmenizi öneririz. Birleştirilebilir işlevlerin ayrılmasını ve yeniden kullanılmasını teşvik etmek için her birleştirilebilir işlev mümkün olduğunca az bilgi içermelidir.

@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
}

Kontrolün tersine çevrilmesi

Bir composable'a gereksiz bağımlılıklar aktarmayı önlemenin bir diğer yolu da kontrolü tersine çevirme kullanmaktır. Alt öğe, bazı mantıkları yürütmek için bağımlılık almak yerine bunu üst öğe yapar.

Bir alt öğenin bazı verileri yükleme isteğini tetiklemesi gereken aşağıdaki örneğe bakın:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel)
}

@Composable
fun MyDescendant(myViewModel: MyViewModel) {
    Button(onClick = { myViewModel.loadData() }) {
        Text("Load data")
    }
}

Duruma bağlı olarak MyDescendant çok fazla sorumluluk sahibi olabilir. Ayrıca, MyViewModel öğesini bağımlılık olarak iletmek, MyDescendant öğesini daha az yeniden kullanılabilir hale getirir. Çünkü artık birlikte kullanılırlar. Bağımlılığı alt öğeye geçirmeyen ve kontrolü tersine çevirme ilkelerini kullanarak mantığı yürütmekten üst öğeyi sorumlu kılan alternatifi göz önünde bulundurun:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusableLoadDataButton(
        onLoadClick = {
            myViewModel.loadData()
        }
    )
}

@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
    Button(onClick = onLoadClick) {
        Text("Load data")
    }
}

Bu yaklaşım, alt öğeyi doğrudan üst öğelerinden ayırdığı için bazı kullanım alanlarına daha uygun olabilir. Üst öğe composable'lar, daha esnek alt düzey composable'lar kullanmak için daha karmaşık hale gelir.

Benzer şekilde, @Composable içerik lambdaları da aynı avantajları elde etmek için aynı şekilde kullanılabilir:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(
        content = {
            Button(
                onClick = {
                    myViewModel.loadData()
                }
            ) {
                Text("Confirm")
            }
        }
    )
}

@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}