CompositionLocal ile yerel kapsamlı veriler

CompositionLocal, besteden dolaylı olarak veri aktarmak için kullanılan bir araçtır. Bu sayfada CompositionLocal öğesinin ne olduğunu, kendi CompositionLocal öğenizi nasıl oluşturacağınızı ve CompositionLocal öğesinin kullanım alanınız için iyi bir çözüm olup olmadığını öğreneceksiniz.

CompositionLocal ile tanışın

Genellikle Compose'da veriler, kullanıcı arayüzü ağacından her bir composable işlevin parametre olarak aşağıya aktarılır. Bu, bir composable'ın bağımlılıklarını açık hale getirir. Ancak bu, renkler ve tür stilleri gibi çok sık ve yaygın bir şekilde kullanılan veriler için külfetli 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 composable'a açık bir parametre olarak iletme ihtiyacını ortadan kaldırmak için Compose, kullanıcı arayüzü ağacında veri akışı sağlamanın dolaylı bir yolu olarak kullanılabilecek ağaç kapsamlı adlandırılmış nesneler oluşturmanıza olanak tanıyan CompositionLocal sunar.

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

Materyal teması, arka planda CompositionLocal kullanır. MaterialTheme, üç CompositionLocal örneği (renkler, tipografi ve şekiller) sağlayan bir nesnedir. Böylece, bu örnekleri daha sonra Beste'nin herhangi bir alt bölümünde alabilirsiniz. MaterialTheme colors, shapes ve typography özellikleri üzerinden erişebileceğiniz LocalColors, LocalShapes ve LocalTypography özellikleri şunlardır.

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

Ağacın farklı düzeylerinde farklı değerler sağlayabilmeniz için CompositionLocal örneği Beste'nin bir bölümüne dahil edilir. CompositionLocal öğesinin current değeri, Beste'nin söz konusu bölümündeki bir üst öğe tarafından sağlanan en yakın değere karşılık gelir.

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

Buna örnek olarak LocalContentAlpha CompositionLocal, kullanıcı arayüzünün farklı bölümlerini vurgulamak veya vurgulamak için metin ve ikonografide kullanılan, tercih edilen alfa içeriğini içerir. Aşağıdaki örnekte, Beste'nin farklı bölümleri için farklı değerler sağlamak amacıyla CompositionLocalProvider kullanılmıştır.

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

Şekil 1. CompositionLocalExample composable'ın önizlemesi.

Yukarıdaki tüm örneklerde CompositionLocal örnekleri, Material composable'ları 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, metni biçimlendirmek için Android uygulamalarında yaygın olarak kullanılan LocalContext CompositionLocal özelliğinin 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 CompositionLocal oluşturma

CompositionLocal, Beste'den dolaylı olarak veri aktarmak için kullanılan bir araçtır.

CompositionLocal kullanmak için bir diğer önemli sinyal, parametrenin çapraz kesiştiği ve uygulamanın ara katmanlarının varlığından haberdar olmamasıdır. Çünkü bu ara katmanların farkında olması, composable'ın faydasını kısıtlayacaktır. Örneğin, Android izinlerini sorgulama, arka planda bir CompositionLocal tarafından sağlanır. Bir medya seçici composable, API'sini değiştirmeden ve medya seçme aracını çağıran kişilerin ortamdan kullanılan bu ek bağlamı bilmelerini gerektirmeden cihazdaki izne korunan içeriğe erişmek için yeni işlevler ekleyebilir.

Ancak, CompositionLocal her zaman en iyi çözüm değildir. Bazı dezavantajları olduğu için CompositionLocal ürününün aşırı kullanılmamasını öneriyoruz:

CompositionLocal, bir composable'ın davranışını akıl yürütmeyi zorlaştırır. Dolaylı bağımlılıklar oluşturdukları için, composable'ları kullanan composable'ları arayanların, her CompositionLocal için bir değerin memnun olduğundan emin olmaları gerekir.

Dahası, bu bağımlılığın bestenin herhangi bir bölümünde değişebileceği için açık ve doğru bir kaynak olmayabilir. Dolayısıyla, current değerinin nerede sağlandığını görmek için Beste'de yukarı gitmeniz gerekeceğinden, bir sorun oluştuğunda uygulamadaki hataları ayıklamak daha zor olabilir. IDE'deki Kullanımları bul veya Oluşturma düzen denetleyicisi gibi araçlar bu sorunu azaltmak için yeterli bilgi sağlar.

CompositionLocal kullanılıp kullanılmayacağına karar veriliyor

CompositionLocal ürününü kullanım alanınız için 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 bir değerin sağlanmadığı bir duruma düşmesinin çok zor olacağını garanti etmeniz gerekir. Varsayılan bir değer sağlanmaması, test oluştururken veya CompositionLocal öğesini kullanan bir composable'ın önizlenirken her zaman açıkça sağlanmasını gerektirirken sorun ve hayal kırıklığına yol açabilir.

Ağaç kapsamlı veya alt hiyerarşi kapsamlı olarak düşünülmeyen kavramlar için CompositionLocal kullanmaktan kaçının. CompositionLocal, birkaç alt öğe tarafından değil, herhangi bir alt öğe tarafından kullanılabildiğinde anlamlıdır.

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

Belirli bir ekranın ViewModel öğesini içeren bir CompositionLocal oluşturmak kötü uygulamaya örnek olarak verilebilir. Böylece söz konusu ekrandaki tüm composable'lar, mantıksal hareket etmek için ViewModel öğesine referans verebilir. Belirli bir kullanıcı arayüzü ağacının altındaki tüm composable'ların ViewModel hakkında bilgi sahibi olması gerekmediğinden bu kötü bir uygulamadır. İyi bir uygulama, composable'lara yalnızca ihtiyaç duydukları bilgileri durum aşağı, etkinliklerin yukarı aktığı kalıbı izlenerek iletmektir. Bu yaklaşım, composable'larınızı daha çok kullanılabilir ve test edilmesini kolaylaştıracak.

CompositionLocal oluşturuluyor

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 öğesinin aksine, staticCompositionLocalOf okumaları Compose tarafından takip edilmez. Değerin değiştirilmesi, yalnızca current değerinin Beste'de okunduğu yerler yerine, CompositionLocal öğesinin sağlandığı content lambdasının tamamının yeniden oluşturulmasına neden olur.

CompositionLocal için sağlanan değerin değişme olasılığı yoksa veya hiçbir zaman değişmeyecekse performans avantajlarından yararlanmak için staticCompositionLocalOf değerini kullanın.

Örneğin, bir uygulamanın tasarım sistemi, kullanıcı arayüzü bileşeni için bir gölge kullanılarak composable'ların yükseltilme biçiminde değerlendirilebilir. Uygulamanın farklı yükseltmelerinin kullanıcı arayüzü ağacına yayılması gerektiği için CompositionLocal kullanılır. CompositionLocal değeri sistem temasına göre koşullu olarak türetildiğinden compositionLocalOf API'yi 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, değerleri belirli bir hiyerarşi için CompositionLocal örneklerine bağlar. Bir CompositionLocal öğesine yeni bir değer sağlamak için CompositionLocal anahtarını aşağıdaki şekilde bir value ile ilişkilendiren provides infix işlevini 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 kullanımı

CompositionLocal.current, söz konusu CompositionLocal için bir 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
    Card(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 CompositionLocal kullanılıp kullanılmayacağına karar verme bölümünde belirtilen ölçütleri karşılamıyorsa başka bir çözüm muhtemelen kullanım alanınız için daha uygun olabilir.

Açık parametreleri iletme

composable'ın bağımlılıklarını açıkça belirtmek iyi bir alışkanlıktır. composable'ları yalnızca ihtiyaç duydukları bilgileri iletmenizi öneririz. composable'ların ayrıştırılmasını ve tekrar kullanılmasını teşvik etmek için her composable, mümkün olan en az miktarda bilgiyi 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ü ters çevirme

Gereksiz bağımlılıkları bir composable'a iletmekten kaçınmanın bir başka yolu da kontrolü ters çevirmektir. Alt öğe bir mantık yürütmek için bağımlılığı almak yerine bunu yapar.

Bir alt öğenin, bazı verileri yüklemek için isteği 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 adlı kuruluşun büyük sorumluluğu olabilir. Ayrıca, bağımlılık olarak MyViewModel'ın iletilmesi, artık birbiriyle ilişkili oldukları için MyDescendant ürününün yeniden kullanılamaz hale gelmesini sağlar. Bağımlılığı alt öğeye iletmeyen ve üst öğeyi mantığı yürütmekten sorumlu yapan kontrol ilkelerinin tersini kullanan bir alternatif 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 kendi atalarından ayırdığı için bazı kullanım alanları için daha uygun olabilir. Üst composable'lar, daha esnek ve düşük seviyeli composable'lara sahip oldukları için daha karmaşık hâle gelme eğilimindedir.

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