CompositionLocal ile yerel kapsamlı veriler

CompositionLocal, verileri Beste üzerinden örtülü olarak aktarmaya yarayan bir araçtır. Bu sayfada CompositionLocal öğesinin ne olduğunu daha ayrıntılı bir şekilde, kendi CompositionLocal öğenizi nasıl oluşturacağınızı ve CompositionLocal kullanmanın kullanım alanınız için iyi bir çözüm olup olmadığını öğreneceksiniz.

CompositionLocal ile tanışın

Compose'da genellikle her composable işleve parametre olarak kullanıcı arayüzü ağacı üzerinden veri akışı olur. Bu, bir composable'ın bağımlılıklarını açık hale getirir. Ancak bu, renkler veya 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
    )
}

Compose, CompositionLocal ile renklerin çoğu composable'a açık parametre bağımlılığı olarak iletilmesine gerek kalmaz. Bu özellik, kullanıcı arayüzü ağacında veri akışını dolaylı yol olarak kullanabileceğiniz ağaç kapsamlı adlandırılmış nesneler oluşturmanıza olanak tanır.

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

Materyal tema, arka planda CompositionLocal özelliğini kullanır. MaterialTheme üç CompositionLocal örneği (renkler, tipografi ve şekiller) sağlayan bir nesnedir. Böylece bunları daha sonra Beste'nin herhangi bir alt bölümünde alabilirsiniz. Özellikle bunlar MaterialTheme colors, shapes ve typography özellikleri aracılığıyla erişebileceğiniz LocalColors, LocalShapes ve LocalTypography özellikleridir.

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

Bir CompositionLocal örneği, Beste'nin bir bölümüne ayarlanır. Böylece ağacın farklı düzeylerinde farklı değerler sağlayabilirsiniz. CompositionLocal öğesinin current değeri, Bestenin ilgili bölümünde üst öğe tarafından sağlanan en yakın değere karşılık gelir.

Bir CompositionLocal öğesine yeni bir değer sağlamak için CompositionLocalProvider ve bir CompositionLocal anahtarını value ile ilişkilendiren provides infix işlevini kullanın. CompositionLocalProvider öğesinin content lambda'sı, CompositionLocal öğesinin current özelliğine erişilirken sağlanan değeri alır. Yeni bir değer sağlandığında Compose, CompositionLocal öğesini 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 üzerindeki vurguyu azaltmak amacıyla metin ve ikonografi için kullanılan tercih edilen alfa içerik alfasını içerir. Aşağıdaki örnekte CompositionLocalProvider, Bestenin farklı bölümlerine farklı değerler sağlamak için 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ı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 öğenin 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ılmıştı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 öğenizi oluşturma

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

CompositionLocal kullanımıyla ilgili bir diğer önemli sinyal, parametrenin kesişim gösterdiği zamandır ve bu ara katmanların bilinmesinin composable'ın faydasını sınırlayabileceğinden, ara uygulama katmanlarının bunun farkında olmaması gerekir. Örneğin, Android izinleri için sorgulama, arka planda bir CompositionLocal tarafından sağlanır. composable bir medya seçme aracı, API'sini değiştirmeden ve medya seçiciyi arayanların ortamdan kullanılan bu ek bağlamın farkında olmasını gerektirmeden cihazda 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 getirmesi nedeniyle CompositionLocal aşırı kullanılmasını önermiyoruz:

CompositionLocal, bir composable'ın davranışının gerekçelendirilmesini zorlaştırır. Örtülü bağımlılıklar oluşturdukça, bunları kullanan composable'ları kullananların her CompositionLocal için bir değerin karşılandığından emin olması gerekir.

Dahası, bu bağımlılığın Bestenin herhangi bir bölümünde değişebileceği için kesin bir doğruluk kaynağı olmayabilir. Bu nedenle, current değerinin nerede sağlandığını görmek için Beste'de yukarı gitmeniz gerektiğinden, bir sorun oluştuğunda uygulamadaki hataları ayıklamak daha zor olabilir. IDE'deki Kullanımları bulma veya Compose düzen inceleyici gibi araçlar bu sorunu azaltmak için yeterli bilgi sağlar.

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

CompositionLocal özelliğini kullanım alanınız için iyi bir çözüm olabilecek belirli koşullar vardır:

CompositionLocal öğesinin varsayılan değeri iyi olmalıdır. Varsayılan değer yoksa bir geliştiricinin CompositionLocal için bir değer sağlanmadığı bir durumla karşılaşmasının çok zor olacağını garanti etmeniz gerekir. Varsayılan değerin sağlanmaması, test oluştururken veya CompositionLocal öğesini kullanan bir composable'ı önizlerken sorun ve hayal kırıklığı yaratabilir. Bu durum, CompositionLocal özelliğinin her zaman açıkça sağlanmasını gerektirir.

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ılabilecekse anlamlı olur.

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

Belirli bir ekranın ViewModel değerini tutan bir CompositionLocal oluşturarak bu ekrandaki tüm composable'ların bir mantık gerçekleştirmek için ViewModel öğesine referans vermesini sağlayabilirsiniz. Belirli bir kullanıcı arayüzü ağacının altındaki tüm composable'ların bir ViewModel hakkında bilgi sahibi olması gerekmediğinden bu kötü bir uygulamadır. İyi bir uygulama, composable'lara yalnızca ihtiyaç duydukları bilgileri durumun aşağı inen ve etkinliklerin yukarı çıktığı düzene göre iletmektir. Bu yaklaşım composable'larınızı daha tekrar kullanılabilir ve test edilmesini kolaylaştırır.

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'in aksine, staticCompositionLocalOf okumaları Oluşturma tarafından izlenmez. Değerin değiştirilmesi, yalnızca current değerinin Bestede okunduğu yerler yerine CompositionLocal öğesinin sağlandığı content lambda'nın tamamının yeniden derlenmesine neden olur.

CompositionLocal için sağlanan değerin değişme olasılığı çok düşükse 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şeninde bir gölge kullanılarak composable'ların yükseltildiği biçimde özenli bir şekilde düşünülebilir. Uygulama için farklı yüksekliklerin kullanıcı arayüzü ağacı boyunca yayılması gerektiğinden CompositionLocal kullanırız. CompositionLocal değeri, sistem temasına dayalı olarak koşullu olarak türetildiği için 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 öğesine değerler sağlama

CompositionLocalProvidercomposable, belirtilen hiyerarşi için değerleri CompositionLocal örneğine bağlar. Bir CompositionLocal öğesine yeni bir değer sağlamak için CompositionLocal anahtarını bir value ile aşağıdaki şekilde 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 tüketimi

CompositionLocal.current, söz konusu CompositionLocal öğesine 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
    }
}

Göz önünde bulundurulması gereken alternatifler

CompositionLocal, bazı kullanım alanları için aşırı bir çözüm olabilir. Kullanım alanınız, CompositionLocal özelliğinin kullanılıp kullanılmayacağına 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. Yalnızca ihtiyaç duydukları composable'ları iletmenizi öneririz. composable'ların ayrıştırılmasını ve yeniden kullanılmasını teşvik etmek için her composable'ın mümkün olan en az miktarda bilgi içermesi gerekir.

@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ü tersine çevirme

Bir composable'a gereksiz bağımlılıkları geçirmenin önüne geçmenin diğer bir yolu da kontrolü tersine çevirmektir. Alt öğenin bir mantık yürütmek için bağımlılık alması yerine bunu üst öğe yapar.

Bir alt öğenin, bazı verileri yüklemek için isteği tetiklemesi gereken aşağıdaki örneği inceleyin:

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

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

Destek kaydına bağlı olarak, MyDescendant şirketinin büyük sorumlulukları olabilir. Ayrıca, MyViewModel öğesinin bağımlılık olarak geçirilmesi, MyDescendant artık birbirlerine bağlı olduğundan daha az yeniden kullanılabilir olmasını sağlar. Bağımlılığı alt öğeye geçirmeyen ve üst öğeyi mantığı yürütmekten sorumlu yapan kontrol ilkelerinin ters çevrilmesini 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, çocuğu kendi üstlerinden ayırdığı için bazı kullanım alanları için daha uygun olabilir. Üst composable'lar, daha esnek alt seviye composable'lara kıyasla daha karmaşık hale gelme eğilimindedir.

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