CompositionLocal ile yerel kapsamlı veriler

CompositionLocal, verileri Composition'dan aşağıya ö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, her bir composable işlevine parametre olarak kullanıcı arayüzü ağacından aşağı doğru akar. Bu, composable'ın bağımlılıklarını açık hale getirir. Ancak bu, 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 bir parametre bağımlılığı olarak iletilmesine gerek olmaması için Compose, CompositionLocal sunar. Bu, verilerin kullanıcı arayüzü ağacında akmasını sağlamak için örtülü bir yol olarak kullanılabilecek, ağ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ığı öğedir. MaterialTheme, üç CompositionLocal örneği (colorScheme, typography ve shapes) sağlayan bir nesnedir. 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. Böylece, 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")
}

1.şekil 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ılıyor:

@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 oluşturma

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

CompositionLocal kullanmanın bir diğer önemli sinyali, parametrenin kesişen bir parametre olması ve bu parametrenin varlığının ara uygulama katmanları tarafından bilinmemesi gerektiğidir. Çünkü bu ara katmanların parametrenin varlığından haberdar olması, birleştirilebilir öğenin faydasını sınırlar. Örneğin, Android izinleri için sorgu oluşturma, 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ı, bazı dezavantajları olduğu için önermiyoruz:

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ğru 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 bilgiyi sağlar.

CompositionLocal kullanıp kullanmamaya karar verme

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

CompositionLocal iyi bir varsayılan değere sahip 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ğlanmaması, test oluştururken veya bu CompositionLocal'yı 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 kullanılma ihtimali olduğunda mantıklıdır.

Kullanım alanınız bu şartları karşılamıyorsa CompositionLocal oluşturmadan önce Değerlendirilebilecek 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ğı akar ve etkinlikler yukarı akar kalıbını izleyerek composable'lara yalnızca ihtiyaç duydukları bilgileri iletmektir. Bu yaklaşım, composable işlevlerinizi daha fazla yeniden kullanılabilir ve test etmesi daha kolay hale getirir.

CompositionLocal oluşturma

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 değerinin 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ükseltilme şekliyle ilgili belirli tercihler 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 olarak türetildiğ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ğer sağlama

CompositionLocalProvider Composable'ı, belirli bir hiyerarşi için değerleri CompositionLocal örneklerine bağlar. Bir CompositionLocal öğesine yeni bir değer sağlamak için CompositionLocal anahtarını value ile ilişkilendiren provides infix 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üketimi

CompositionLocal.current, CompositionLocal öğesine 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ün tersine çevrilmesidir. 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. Bunun nedeni, bu öğelerin artık birlikte kullanılmasıdır. 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ı şekilde kullanılarak aynı avantajlar elde edilebilir:

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

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