Özel değiştiriciler oluşturma

Compose, yaygın davranışlar için kullanıma hazır birçok değiştirici sunar. Ancak kendi özel değiştiricilerinizi de oluşturabilirsiniz.

Değiştiriciler birden fazla bölümden oluşur:

  • Bir değiştirici fabrikası
    • Bu, Modifier üzerindeki bir uzantı işlevidir. Değiştiriciniz için deyimsel bir API sağlar ve değiştiricilerin birlikte zincirlenmesine olanak tanır. Değiştirici fabrikası, Compose'un kullanıcı arayüzünüzü değiştirmek için kullandığı değiştirici öğeleri üretir.
  • Bir değiştirici öğe
    • Değiştiricinizin davranışını burada uygulayabilirsiniz.

Gerekli işlevselliğe bağlı olarak özel değiştiriciyi uygulamanın birden fazla yolu vardır. Genellikle, özel bir değiştiriciyi uygulamanın en basit yolu, diğer tanımlanmış değiştirici fabrikalarını birleştiren özel bir değiştirici fabrikası uygulamaktır. Daha fazla özel davranışa ihtiyacınız varsa daha düşük düzeyde olan ancak daha fazla esneklik sağlayan Modifier.Node API'lerini kullanarak değiştirici öğesini uygulayın.

Mevcut değiştiricileri birlikte zincirleme

Mevcut değiştiricileri kullanarak özel değiştiriciler oluşturmak genellikle mümkündür. Örneğin, Modifier.clip(), graphicsLayer değiştiricisi kullanılarak uygulanır. Bu strateji, mevcut değiştirici öğeleri kullanır ve kendi özel değiştirici fabrikanızı sağlarsınız.

Kendi özel değiştiricinizi uygulamadan önce aynı stratejiyi kullanıp kullanamayacağınızı kontrol edin.

fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

Aynı değiştirici grubunu sık sık kullandığınızı fark ederseniz bunları kendi değiştiricinizde birleştirebilirsiniz:

fun Modifier.myBackground(color: Color) = padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

Birleştirilebilir değiştirici fabrikası kullanarak özel değiştirici oluşturma

Mevcut bir değiştiriciye değerler iletmek için bir composable işlevi kullanarak özel bir değiştirici de oluşturabilirsiniz. Buna, composable değiştirici fabrikası denir.

Değiştirici oluşturmak için birleştirilebilir değiştirici fabrikası kullanmak, animate*AsState gibi daha üst düzey Compose API'lerini ve diğer Compose durum destekli animasyon API'lerini kullanmanıza da olanak tanır. Örneğin, aşağıdaki snippet'te etkinleştirildiğinde/devre dışı bırakıldığında alfa değişikliğini canlandıran bir değiştirici gösterilmektedir:

@Composable
fun Modifier.fade(enable: Boolean): Modifier {
    val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
    return this then Modifier.graphicsLayer { this.alpha = alpha }
}

Özel değiştiriciniz, CompositionLocal öğesinden varsayılan değerler sağlamak için kullanılan bir kolaylık yöntemi ise bunu uygulamanın en kolay yolu bir composable değiştirici fabrikası kullanmaktır:

@Composable
fun Modifier.fadedBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

Bu yaklaşımın, aşağıdaki bölümlerde ayrıntılı olarak açıklanan bazı uyarıları vardır.

CompositionLocal değerleri, değiştirici fabrikasının çağrı sitesinde çözümlenir.

Composable değiştirici fabrikası kullanarak özel bir değiştirici oluştururken, composable yereller değeri oluşturuldukları beste ağacından alır, kullanıldıkları yerden değil. Bu durum, beklenmedik sonuçlara yol açabilir. Örneğin, daha önce bahsedilen composable işlevi kullanılarak biraz farklı şekilde uygulanan composable yerel değiştirici örneğini ele alalım:

@Composable
fun Modifier.myBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

@Composable
fun MyScreen() {
    CompositionLocalProvider(LocalContentColor provides Color.Green) {
        // Background modifier created with green background
        val backgroundModifier = Modifier.myBackground()

        // LocalContentColor updated to red
        CompositionLocalProvider(LocalContentColor provides Color.Red) {

            // Box will have green background, not red as expected.
            Box(modifier = backgroundModifier)
        }
    }
}

Değiştiricinizin bu şekilde çalışmasını beklemiyorsanız bunun yerine özel bir Modifier.Node kullanın. Çünkü kompozisyon yerelleri, kullanım sitesinde doğru şekilde çözümlenir ve güvenli bir şekilde yükseltilebilir.

Birleştirilebilir işlev değiştiriciler hiçbir zaman atlanmaz.

Döndürülen değerleri olan composable işlevler atlanamadığı için composable fabrika değiştiriciler hiçbir zaman atlanmaz. Bu, değiştirici işlevinizin her yeniden oluşturmada çağrılacağı anlamına gelir. Bu durum, sık sık yeniden oluşturma yapılıyorsa maliyetli olabilir.

Composable işlev değiştiricileri, composable işlev içinde çağrılmalıdır.

Tüm composable işlevler gibi, composable fabrika değiştiricisi de kompozisyonun içinden çağrılmalıdır. Bu, değiştiricinin asla kompozisyonun dışına çıkarılamayacağından, yükseltilebileceği yeri sınırlar. Buna karşılık, birleştirilebilir olmayan değiştirici fabrikaları, daha kolay yeniden kullanıma olanak tanımak ve performansı artırmak için birleştirilebilir işlevlerin dışına çıkarılabilir:

val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations

@Composable
fun Modifier.composableModifier(): Modifier {
    val color = LocalContentColor.current.copy(alpha = 0.5f)
    return this then Modifier.background(color)
}

@Composable
fun MyComposable() {
    val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher
}

Modifier.Node kullanarak özel değiştirici davranışı uygulama

Modifier.Node, Compose'da değiştiriciler oluşturmak için kullanılan daha düşük düzeyli bir API'dir. Bu API, Compose'un kendi değiştiricilerini uyguladığı API ile aynıdır ve özel değiştiriciler oluşturmanın en iyi performanslı yoludur.

Modifier.Node kullanarak özel değiştirici uygulama

Modifier.Node kullanarak özel değiştirici uygulamak üç bölümden oluşur:

  • Değiştiricinizin mantığını ve durumunu içeren bir Modifier.Node uygulaması.
  • Değiştirici düğümü örnekleri oluşturan ve güncelleyen bir ModifierNodeElement.
  • Daha önce ayrıntılı olarak açıklandığı gibi, isteğe bağlı bir değiştirici fabrikası.

ModifierNodeElement sınıfları durumsuzken her yeniden oluşturmada yeni örnekler ayrılır. Modifier.Node sınıfları ise durumlu olabilir, birden çok yeniden oluşturma boyunca varlığını sürdürebilir ve hatta yeniden kullanılabilir.

Aşağıdaki bölümde her bir bölüm açıklanmakta ve daire çizmek için özel bir değiştirici oluşturma örneği gösterilmektedir.

Modifier.Node

Modifier.Node uygulaması (bu örnekte CircleNode), özel değiştiricinizin işlevini uygular.

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

Bu örnekte, değiştirici işlevine iletilen renkle daire çizilir.

Bir düğüm, Modifier.Node ve sıfır veya daha fazla düğüm türü uygular. Değiştiricinizin gerektirdiği işlevselliğe bağlı olarak farklı düğüm türleri vardır. Yukarıdaki örneğin çizim yapabilmesi için DrawModifierNode'ı uygulaması gerekir. Bu, çizim yöntemini geçersiz kılmasını sağlar.

Kullanılabilir türler şunlardır:

Düğüm

Kullanım

Örnek bağlantı

LayoutModifierNode

Sarmalanmış içeriğin nasıl ölçülüp düzenlendiğini değiştiren bir Modifier.Node.

Örnek

DrawModifierNode

Düzen alanına çizilen bir Modifier.Node.

Örnek

CompositionLocalConsumerModifierNode

Bu arayüzü uyguladığınızda Modifier.Node, beste yerellerini okuyabilir.

Örnek

SemanticsModifierNode

Test, erişilebilirlik ve benzer kullanım alanlarında kullanılmak üzere anlambilim anahtarı/değeri ekleyen bir Modifier.Node.

Örnek

PointerInputModifierNode

PointerInputChanges alan bir Modifier.Node.

Örnek

ParentDataModifierNode

Üst yerleşime veri sağlayan bir Modifier.Node.

Örnek

LayoutAwareModifierNode

onMeasured ve onPlaced geri aramalarını alan bir Modifier.Node.

Örnek

GlobalPositionAwareModifierNode

İçeriğin genel konumu değişmiş olabileceği durumlarda, düzenin son LayoutCoordinates ile onGloballyPositioned geri arama alan bir Modifier.Node.

Örnek

ObserverModifierNode

ObserverNode uygulayan Modifier.Node'lar, observeReads bloğunda okunan anlık görüntü nesnelerinde yapılan değişikliklere yanıt olarak çağrılacak onObservedReadsChanged'nin kendi uygulamalarını sağlayabilir.

Örnek

DelegatingNode

İşi diğer Modifier.Node örneklerine delege edebilen bir Modifier.Node.

Bu, birden fazla düğüm uygulamasını tek bir uygulamada birleştirmek için yararlı olabilir.

Örnek

TraversableNode

Modifier.Node sınıflarının, aynı türdeki sınıflar veya belirli bir anahtar için düğüm ağacında yukarı/aşağı hareket etmesine izin verir.

Örnek

Düğümler, ilgili öğelerinde güncelleme çağrıldığında otomatik olarak geçersiz kılınır. Örneğimiz bir DrawModifierNode olduğundan, öğede herhangi bir güncelleme yapıldığında düğüm yeniden çizme işlemini tetikler ve rengi doğru şekilde güncellenir. Düğüm otomatik geçersiz kılmayı devre dışı bırakma bölümünde ayrıntılı olarak açıklandığı gibi, otomatik geçersiz kılmayı devre dışı bırakmak mümkündür.

ModifierNodeElement

ModifierNodeElement, özel değiştiricinizi oluşturmak veya güncellemek için verileri tutan değişmez bir sınıftır:

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

ModifierNodeElement uygulamalarının aşağıdaki yöntemleri geçersiz kılması gerekir:

  1. create: Bu, değiştirici düğümünüzü örnekleyen işlevdir. Bu işlev, değiştiriciniz ilk uygulandığında düğümü oluşturmak için çağrılır. Bu genellikle, düğümü oluşturup değiştirici fabrikasına iletilen parametrelerle yapılandırmak anlamına gelir.
  2. update: Bu işlev, bu değiştirici aynı noktada sağlandığında (bu düğüm zaten mevcut ancak bir özellik değişmişse) çağrılır. Bu, sınıfın equals yöntemiyle belirlenir. Daha önce oluşturulan değiştirici düğümü, update çağrısına parametre olarak gönderilir. Bu noktada, düğümlerin özelliklerini güncellenen parametrelere karşılık gelecek şekilde güncellemeniz gerekir. Düğümlerin bu şekilde yeniden kullanılabilmesi, Modifier.Node ile elde edilen performans artışları için çok önemlidir. Bu nedenle, update yönteminde yeni bir düğüm oluşturmak yerine mevcut düğümü güncellemeniz gerekir. Daire örneğimizde, düğümün rengi güncellenir.

Ayrıca, ModifierNodeElement uygulamalarında equals ve hashCode de uygulanmalıdır. update yalnızca önceki öğeyle yapılan bir eşittir karşılaştırması yanlış değerini döndürürse çağrılır.

Önceki örnekte bu amaçla bir veri sınıfı kullanılıyor. Bu yöntemler, bir düğümün güncellenmesi gerekip gerekmediğini kontrol etmek için kullanılır. Öğenizde bir düğümün güncellenmesi gerekip gerekmediğine katkıda bulunmayan özellikler varsa veya ikili uyumluluk nedenleriyle veri sınıflarından kaçınmak istiyorsanız equals ve hashCode'yi (ör. padding değiştirici öğesi) manuel olarak uygulayabilirsiniz.

Değiştirici fabrikası

Bu, değiştiricinizin genel API yüzeyidir. Çoğu uygulama, değiştirici öğesini oluşturur ve değiştirici zincirine ekler:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

Eksiksiz örnek

Bu üç bölüm, Modifier.Node API'lerini kullanarak daire çizmek için özel değiştiriciyi oluşturmak üzere bir araya gelir:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

Modifier.Node kullanılan yaygın durumlar

Modifier.Node ile özel değiştiriciler oluştururken karşılaşabileceğiniz bazı yaygın durumlar aşağıda açıklanmıştır.

Sıfır parametre

Değiştiricinizin parametresi yoksa hiçbir zaman güncellenmesi gerekmez ve veri sınıfı olması da gerekmez. Aşağıda, bir composable'a sabit miktarda dolgu uygulayan bir değiştiricinin örnek uygulaması verilmiştir:

fun Modifier.fixedPadding() = this then FixedPaddingElement

data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() {
    override fun create() = FixedPaddingNode()
    override fun update(node: FixedPaddingNode) {}
}

class FixedPaddingNode : LayoutModifierNode, Modifier.Node() {
    private val PADDING = 16.dp

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val paddingPx = PADDING.roundToPx()
        val horizontal = paddingPx * 2
        val vertical = paddingPx * 2

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            placeable.place(paddingPx, paddingPx)
        }
    }
}

Referans kompozisyon yerelleri

Modifier.Node değiştiricileri, CompositionLocal gibi Compose durum nesnelerindeki değişiklikleri otomatik olarak gözlemlemez. Modifier.Node değiştiricilerin, yalnızca birleştirilebilir fabrika ile oluşturulan değiştiricilere göre avantajı, currentValueOf kullanarak değiştiricinin ayrıldığı yerden değil, kullanıcı arayüzü ağınızda kullanıldığı yerden kompozisyon yerelinin değerini okuyabilmeleridir.

Ancak değiştirici düğüm örnekleri, durum değişikliklerini otomatik olarak gözlemlemez. Bir kompozisyon yerelinin değişmesine otomatik olarak tepki vermek için kapsam içinde geçerli değerini okuyabilirsiniz:

Bu örnekte, arka planı rengine göre çizmek için LocalContentColor değerine bakılır. ContentDrawScope, anlık görüntü değişikliklerini gözlemlediği için LocalContentColor değeri değiştiğinde otomatik olarak yeniden çizilir:

class BackgroundColorConsumerNode :
    Modifier.Node(),
    DrawModifierNode,
    CompositionLocalConsumerModifierNode {
    override fun ContentDrawScope.draw() {
        val currentColor = currentValueOf(LocalContentColor)
        drawRect(color = currentColor)
        drawContent()
    }
}

Bir kapsamın dışındaki durum değişikliklerine tepki vermek ve değiştiricinizi otomatik olarak güncellemek için ObserverModifierNode kullanın.

Örneğin, Modifier.scrollable, LocalDensity'deki değişiklikleri gözlemlemek için bu tekniği kullanır. Basitleştirilmiş bir örnek aşağıda gösterilmektedir:

class ScrollableNode :
    Modifier.Node(),
    ObserverModifierNode,
    CompositionLocalConsumerModifierNode {

    // Place holder fling behavior, we'll initialize it when the density is available.
    val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity))

    override fun onAttach() {
        updateDefaultFlingBehavior()
        observeReads { currentValueOf(LocalDensity) } // monitor change in Density
    }

    override fun onObservedReadsChanged() {
        // if density changes, update the default fling behavior.
        updateDefaultFlingBehavior()
    }

    private fun updateDefaultFlingBehavior() {
        val density = currentValueOf(LocalDensity)
        defaultFlingBehavior.flingDecay = splineBasedDecay(density)
    }
}

Değiştiriciye animasyon ekleme

Modifier.Node uygulamaları coroutineScope verilerine erişebilir. Bu, Compose Animatable API'lerinin kullanılmasına olanak tanır. Örneğin, bu snippet, daha önce gösterilen CircleNode öğesini tekrar tekrar solup kaybolacak şekilde değiştirir:

class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
    private lateinit var alpha: Animatable<Float, AnimationVector1D>

    override fun ContentDrawScope.draw() {
        drawCircle(color = color, alpha = alpha.value)
        drawContent()
    }

    override fun onAttach() {
        alpha = Animatable(1f)
        coroutineScope.launch {
            alpha.animateTo(
                0f,
                infiniteRepeatable(tween(1000), RepeatMode.Reverse)
            ) {
            }
        }
    }
}

Yetki verme özelliğini kullanarak değiştiriciler arasında durumu paylaşma

Modifier.Node değiştiriciler, diğer düğümlere yetki verebilir. Bunun birçok kullanım alanı vardır. Örneğin, farklı değiştiricilerdeki ortak uygulamaları ayıklamak için kullanılabilir. Ayrıca, değiştiriciler arasında ortak durum paylaşmak için de kullanılabilir.

Örneğin, etkileşim verilerini paylaşan tıklanabilir değiştirici düğümün temel bir uygulaması:

class ClickableNode : DelegatingNode() {
    val interactionData = InteractionData()
    val focusableNode = delegate(
        FocusableNode(interactionData)
    )
    val indicationNode = delegate(
        IndicationNode(interactionData)
    )
}

Düğümün otomatik olarak geçersiz kılınmasını devre dışı bırakma

Modifier.Node düğümleri, ilgili ModifierNodeElement çağrıları güncellendiğinde otomatik olarak geçersiz kılınır. Karmaşık değiştiriciler için, değiştiricinizin aşamaları ne zaman geçersiz kılacağı konusunda daha ayrıntılı kontrol elde etmek amacıyla bu davranışın devre dışı bırakılmasını isteyebilirsiniz.

Bu, özellikle özel değiştiriciniz hem düzeni hem de çizimi değiştiriyorsa yararlıdır. Otomatik geçersiz kılmayı devre dışı bırakmak, yalnızca color gibi çizimle ilgili özellikler değiştiğinde çizimi geçersiz kılmanıza olanak tanır. Bu, düzenin geçersiz kılınmasını önler ve değiştiricinizin performansını artırabilir.

Bununla ilgili varsayıma dayalı bir örnek, özellik olarak color, size ve onClick lambda'sına sahip bir değiştiriciyle aşağıdaki örnekte gösterilmektedir. Bu değiştirici, gereksiz geçersiz kılmaları atlayarak yalnızca gerekenleri geçersiz kılar:

class SampleInvalidatingNode(
    var color: Color,
    var size: IntSize,
    var onClick: () -> Unit
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode {
    override val shouldAutoInvalidate: Boolean
        get() = false

    private val clickableNode = delegate(
        ClickablePointerInputNode(onClick)
    )

    fun update(color: Color, size: IntSize, onClick: () -> Unit) {
        if (this.color != color) {
            this.color = color
            // Only invalidate draw when color changes
            invalidateDraw()
        }

        if (this.size != size) {
            this.size = size
            // Only invalidate layout when size changes
            invalidateMeasurement()
        }

        // If only onClick changes, we don't need to invalidate anything
        clickableNode.update(onClick)
    }

    override fun ContentDrawScope.draw() {
        drawRect(color)
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val size = constraints.constrain(size)
        val placeable = measurable.measure(constraints)
        return layout(size.width, size.height) {
            placeable.place(0, 0)
        }
    }
}