CompositionLocal ile yerel kapsamlı veriler

CompositionLocal, verileri kompozisyon üzerinden dolaylı olarak iletmek için kullanılan bir araçtır. Bu sayfada, CompositionLocal'nin ne olduğunu daha ayrıntılı bir şekilde öğrenecek, kendi CompositionLocal'nizi nasıl oluşturacağınızı öğrenecek ve CompositionLocal'nin kullanım alanınız için iyi bir çözüm olup olmadığını anlayacaksınız.

CompositionLocal ile tanışın

Genellikle Compose'da, her bir birleştirilebilir işlev için parametre olarak kullanıcı arayüzü ağacından veriler aşağı akar. Bu, bir bileşenin bağımlılıklarını açık hale getirir. Ancak bu, renk veya yazı stili 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
    )
}

Renkleri çoğu bileşime açık parametre bağımlılığı olarak iletme ihtiyacını ortadan kaldırmak için Compose, kullanıcı arayüzü ağacında veri akışı sağlamak için açık olmayan bir yöntem olarak kullanılabilecek ağaç kapsamlı adlandırılmış nesneler oluşturmanıza olanak tanıyan CompositionLocal'yi sunar.

CompositionLocal öğeleri genellikle kullanıcı arayüzü ağacının belirli bir düğümünde bir değerle sağlanır. Bu değer, CompositionLocal'nin birleştirilebilir işlevde parametre olarak tanımlanmasına gerek kalmadan birleştirilebilir alt öğeleri tarafından kullanılabilir.

CompositionLocal, Material temasının temelinde kullanılan öğedir. MaterialTheme, colorScheme, typography ve shapes olmak üzere üç CompositionLocal örneği sağlayan bir nesnedir. Bu örnekleri daha sonra bileşimin herhangi bir alt öğesinde alabilirsiniz. Daha açık belirtmek gerekirse, 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üne göre kapsamlandırılır. Böylece, ağacın farklı seviyelerinde farklı değerler sağlayabilirsiniz. Bir CompositionLocal öğesinin current değeri, kompozisyonun ilgili bölümünde bir ata tarafından sağlanan en yakın değere karşılık gelir.

Bir CompositionLocal için yeni bir değer sağlamak istiyorsanız CompositionLocalProvider ve CompositionLocal anahtarını value ile ilişkilendiren provides ara değer işlevini kullanın. CompositionLocalProvider'un content lambdası, CompositionLocal'ın current özelliğine erişirken sağlanan değeri alır. Yeni bir değer sağlandığında, Oluştur işlevi, CompositionLocal değerini okuyan kompozisyonun bölümlerini yeniden oluşturur.

Buna örnek olarak LocalContentColor CompositionLocal, mevcut arka plan rengiyle kontrast oluşturması için metin ve simgelerde kullanılan tercih edilen içerik rengini içerir. Aşağıdaki örnekte, kompozisyonun farklı bölümleri için farklı değerler sağlamak üzere CompositionLocalProvider 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")
}

Şekil 1. CompositionLocalExample bileşeninin önizlemesi.

Son örnekte, CompositionLocal örnekleri Material bileşenleri tarafından dahili olarak kullanılmıştır. Bir CompositionLocal öğesinin mevcut değerine erişmek için current özelliğini kullanın. Aşağıdaki örnekte, Android uygulamalarında yaygın olarak kullanılan LocalContext CompositionLocal öğesinin geçerli Context değeri metni biçimlendirmek için kullanılı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 CompositionLocal

CompositionLocal, kompozisyon aracılığıyla verileri dolaylı olarak aktarmak için kullanılan bir araçtır.

CompositionLocal'un kullanılmasıyla ilgili başka bir önemli sinyal, parametrenin kesişimsel olması ve uygulamanın ara katmanlarının varlığından haberdar olmaması gerektiğidir. Bu ara katmanların haberdar edilmesi, birleştirilebilir öğenin işlevini sınırlayacağından bu durum önemlidir. Örneğin, Android izinleri için sorgu, temelde bir CompositionLocal tarafından sağlanır. Bir medya seçici bileşeni, API'sini değiştirmeden ve medya seçiciyi çağıranların ortamda kullanılan bu ek bağlamdan haberdar 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'nin bazı olumsuz yönleri olduğundan aşırı kullanımını önermeyiz:

CompositionLocal, bir bileşenin davranışını tahmin etmeyi zorlaştırır. Örtülü bağımlılıklar oluşturdukları için, bunları kullanan birleştirilebilir öğelerin arayanlarının her CompositionLocal için bir değer sağladığından emin olması gerekir.

Ayrıca, bu bağımlılık, kompozisyonun herhangi bir yerinde mutasyona uğrayabileceğ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 kompozisyonda yukarı doğru gitmeniz gerektiğinden sorun oluştuğunda uygulamada hata ayıklama daha zor olabilir. IDE'deki Kullanım yerlerini bul veya Düzen oluşturma inceleyici gibi araçlar, bu sorunu azaltmak için yeterli bilgi sağlar.

CompositionLocal kullanmaya karar verme

CompositionLocal'ü kullanım alanınız için 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ştiricilerin CompositionLocal için bir değer sağlamadığı bir duruma düşmesinin son derece zor olduğunu garanti etmeniz gerekir. Varsayılan değer sağlamamak, test oluştururken veya bu özelliği kullanan bir bileşeni önizlerken sorunlara ve can sıkıcı durumlara neden olabilir. CompositionLocal her zaman açıkça sağlanmalıdır.

Ağaç kapsamlı veya alt hiyerarşi kapsamlı olarak düşünülmeyen kavramlar için CompositionLocal kullanmayın. CompositionLocal, birkaçı tarafından değil, tüm alt öğeler tarafından kullanılabilecek durumlarda anlamlıdır.

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

Kötü bir uygulama örneği, belirli bir ekranın ViewModel değerini tutan bir CompositionLocal oluşturmaktır. Böylece, söz konusu ekrandaki tüm derlenebilirler belirli bir mantık yürütmek için ViewModel referansı alabilir. Belirli bir kullanıcı arayüzü ağacının altındaki tüm bileşenlerin ViewModel hakkında bilgi sahibi olması gerekmediğinden bu kötü bir uygulamadır. En iyi uygulama, durum aşağı, etkinlikler yukarı akışı modelini izleyerek yalnızca gerekli bilgileri birleştirilebilir öğelere aktarmaktır. Bu yaklaşım, bileşenlerinizin yeniden kullanılabilirliğini ve test edilebilirliğini artırır.

CompositionLocal oluşturma

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

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

  • staticCompositionLocalOf: compositionLocalOf'ın aksine, staticCompositionLocalOf'un okumaları Compose tarafından izlenmez. Değerin değiştirilmesi, current değerinin Kompozisyon'da okunduğu yerlerin yerine CompositionLocal değerinin sağlandığı content lambda işlevinin tamamının yeniden derlenmesine neden olur.

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

Örneğin, bir uygulamanın tasarım sistemi, kullanıcı arayüzü bileşeni için gölge kullanılarak bileşenlerin öne çıkarılma şekliyle ilgili fikir sahibi olabilir. Uygulamanın farklı yükseklikleri kullanıcı arayüzü ağacı boyunca yayılacağından bir CompositionLocal kullanırız. CompositionLocal değeri, sistem temasına göre koşullu olarak türetildiği için 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ğer sağlama

CompositionLocalProvider bileşeni, değerleri belirli bir hiyerarşi için CompositionLocal örneklerine bağlar. Bir CompositionLocal için yeni bir değer sağlamak üzere, CompositionLocal anahtarını value ile ilişkilendiren provides ara dize işlevini aşağıdaki gibi 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üketme

CompositionLocal.current, ilgili 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ğerlendirebileceğiniz alternatifler

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

Belirli parametreleri iletme

Kompozit'in bağımlılıkları konusunda net olmak iyi bir alışkanlıktır. Kompozitlere yalnızca ihtiyaç duydukları bilgileri iletmenizi öneririz. Birleştirilebilir öğelerin ayrılmasını ve yeniden kullanılmasını teşvik etmek için her bir birleştirilebilir öğe mümkün olan en az miktarda 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 bileşene gereksiz bağımlılıkların aktarılmasını önlemenin bir diğer yolu da kontrolün tersine çevrilmesidir. Alt öğe, bazı mantık yürütmek için bir 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'ün çok fazla sorumluluğu olabilir. Ayrıca, MyViewModel'ü bağımlılık olarak iletmek, artık birlikte bağlandıkları için MyDescendant'ü daha az yeniden kullanılabilir hale getirir. Bağımlılığı alt öğeye aktarmayan ve mantığı yürütmekten üst öğeyi sorumlu tutan kontrol inversiyonu ilkelerini kullanan alternatifi düşünün:

@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ığından bazı kullanım alanları için daha uygun olabilir. Üst öğe derleyiciler, daha esnek alt düzey derleyiciler elde etmek için daha karmaşık hale gelir.

Benzer şekilde, @Composable içerik lambda'ları da aynı avantajlardan yararlanmak 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()
    }
}