Bermigrasi ke Indication dan Ripple API

Untuk meningkatkan performa komposisi komponen interaktif yang menggunakan Modifier.clickable, kami telah memperkenalkan API baru. API ini memungkinkan lebih banyak implementasi Indication yang efisien, seperti ripple.

androidx.compose.foundation:foundation:1.7.0+ dan androidx.compose.material:material-ripple:1.7.0+ menyertakan API berikut perubahan:

Tidak digunakan lagi

Penggantian

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

ripple() API baru disediakan di library Material.

Catatan: Dalam konteks ini, "Library Material" merujuk ke androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material, dan androidx.wear.compose:compose-material3.

RippleTheme

Yakni:

  • Gunakan API RippleConfiguration library Material, atau
  • Membangun implementasi ripple sistem desain Anda sendiri

Halaman ini menjelaskan dampak perubahan perilaku dan petunjuk untuk bermigrasi ke API yang baru.

Perubahan perilaku

Versi library berikut menyertakan perubahan perilaku ripple:

  • androidx.compose.material:material:1.7.0+
  • androidx.compose.material3:material3:1.3.0+
  • androidx.wear.compose:compose-material:1.4.0+

Versi library Material ini tidak lagi menggunakan rememberRipple(); sebagai gantinya, mereka menggunakan API ripple baru. Hasilnya, kueri tidak mengkueri LocalRippleTheme. Oleh karena itu, jika Anda menetapkan LocalRippleTheme di aplikasi Anda, Material komponen tidak akan menggunakan nilai ini.

Bagian berikut menjelaskan cara beralih kembali ke perilaku lama untuk sementara tanpa melakukan migrasi; tetapi, sebaiknya Anda bermigrasi ke API baru. Sebagai petunjuk migrasi, lihat Bermigrasi dari rememberRipple ke ripple dan bagian-bagian berikutnya.

Mengupgrade versi library Material tanpa melakukan migrasi

Untuk berhenti memblokir upgrade versi library, Anda dapat menggunakan LocalUseFallbackRippleImplementation CompositionLocal API untuk dikonfigurasi Komponen Material untuk kembali ke perilaku lama:

CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
    MaterialTheme {
        App()
    }
}

Pastikan untuk memberikan ini di luar MaterialTheme sehingga ripple lama dapat disediakan melalui LocalIndication.

Bagian berikut menjelaskan cara bermigrasi ke API baru.

Bermigrasi dari rememberRipple ke ripple

Menggunakan library Material

Jika Anda menggunakan library Material, langsung ganti rememberRipple() dengan panggilan ke ripple() dari library yang sesuai. API ini menciptakan ripple menggunakan nilai yang berasal dari API tema Material. Lalu, teruskan objek objek ke Modifier.clickable dan/atau komponen lainnya.

Misalnya, cuplikan berikut menggunakan API yang tidak digunakan lagi:

Box(
    Modifier.clickable(
        onClick = {},
        interactionSource = remember { MutableInteractionSource() },
        indication = rememberRipple()
    )
) {
    // ...
}

Anda harus mengubah cuplikan di atas menjadi:

@Composable
private fun RippleExample() {
    Box(
        Modifier.clickable(
            onClick = {},
            interactionSource = remember { MutableInteractionSource() },
            indication = ripple()
        )
    ) {
        // ...
    }
}

Perhatikan bahwa ripple() tidak lagi menjadi fungsi composable dan tidak perlu diingat. Model ini juga dapat digunakan kembali di beberapa komponen, mirip dengan pengubah, jadi pertimbangkan untuk mengekstrak pembuatan riak ke nilai tingkat teratas untuk menyimpan alokasi.

Menerapkan sistem desain khusus

Jika Anda menerapkan sistem desain Anda sendiri, dan sebelumnya Anda menggunakan rememberRipple() beserta RippleTheme kustom untuk mengonfigurasi ripple, Anda harus menyediakan ripple API Anda sendiri yang didelegasikan ke node ripple API ditampilkan di material-ripple. Kemudian, komponen Anda dapat menggunakan ripple Anda sendiri yang memakai nilai tema Anda secara langsung. Untuk informasi selengkapnya, lihat Memigrasikan dari RippleTheme.

Bermigrasi dari RippleTheme

Memilih tidak ikut perubahan perilaku untuk sementara

Library Material memiliki CompositionLocal sementara, LocalUseFallbackRippleImplementation, yang dapat Anda gunakan untuk mengonfigurasi semua Komponen Material untuk kembali menggunakan rememberRipple. Dengan begini, rememberRipple terus mengkueri LocalRippleTheme.

Cuplikan kode berikut menunjukkan cara menggunakan API LocalUseFallbackRippleImplementation CompositionLocal:

CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
    MaterialTheme {
        App()
    }
}

Jika menggunakan tema aplikasi kustom yang dibuat di atas Material, Anda dapat menyediakan lokal komposisi dengan aman sebagai bagian dari tema aplikasi:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
        MaterialTheme(content = content)
    }
}

Untuk informasi selengkapnya, lihat Mengupgrade versi library Material tanpa bagian migrasi.

Menggunakan RippleTheme guna menonaktifkan ripple untuk komponen tertentu

Library material dan material3 mengekspos RippleConfiguration dan LocalRippleConfiguration, yang memungkinkan Anda mengonfigurasi tampilan riak dalam sebuah subpohon. Perhatikan bahwa RippleConfiguration dan LocalRippleConfiguration bersifat eksperimental, dan hanya ditujukan untuk per komponen dan penyesuaian. Penyesuaian global/seluruh tema tidak didukung oleh API; lihat Menggunakan RippleTheme untuk mengubah semua ripple secara global dalam untuk informasi selengkapnya tentang kasus penggunaan tersebut.

Misalnya, cuplikan berikut menggunakan API yang tidak digunakan lagi:

private object DisabledRippleTheme : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Transparent

    @Composable
    override fun rippleAlpha(): RippleAlpha = RippleAlpha(0f, 0f, 0f, 0f)
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleTheme) {
        Button {
            // ...
        }
    }

Anda harus mengubah cuplikan di atas menjadi:

CompositionLocalProvider(LocalRippleConfiguration provides null) {
    Button {
        // ...
    }
}

Menggunakan RippleTheme untuk mengubah warna/alfa ripple untuk komponen tertentu

Seperti yang dijelaskan di bagian sebelumnya, RippleConfiguration dan LocalRippleConfiguration adalah API eksperimental dan hanya ditujukan untuk penyesuaian per komponen.

Misalnya, cuplikan berikut menggunakan API yang tidak digunakan lagi:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Red

    @Composable
    override fun rippleAlpha(): RippleAlpha = MyRippleAlpha
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleThemeColorAndAlpha) {
        Button {
            // ...
        }
    }

Anda harus mengubah cuplikan di atas menjadi:

@OptIn(ExperimentalMaterialApi::class)
private val MyRippleConfiguration =
    RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha)

// ...
    CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) {
        Button {
            // ...
        }
    }

Menggunakan RippleTheme untuk mengubah semua ripple dalam aplikasi secara global

Sebelumnya, Anda dapat menggunakan LocalRippleTheme untuk menentukan perilaku ripple pada tingkat seluruh tema. Pada dasarnya, ini adalah titik integrasi antara lokal dan riak sistem komposisi desain. Alih-alih mengekspos dengan tema primitif, material-ripple kini mengekspos createRippleModifierNode() . Fungsi ini memungkinkan {i>library<i} sistem desain untuk membuat implementasi urutan wrapper, yang mengkueri nilai temanya, lalu mendelegasikan implementasi ripple ke node yang dibuat oleh fungsi ini.

Hal ini memungkinkan sistem desain untuk secara langsung mengkueri apa yang mereka butuhkan, dan mengekspos membutuhkan lapisan tema yang dapat dikonfigurasi pengguna di atasnya tanpa harus mematuhi apa yang disediakan di lapisan material-ripple. Perubahan ini juga membuat lebih banyak secara eksplisit tema/spesifikasi riak sesuai dengan, karena ripple API itu sendiri yang mendefinisikan kontrak tersebut, bukan secara implisit yang berasal dari tema.

Untuk panduan, lihat penerapan API ripple di Material library, dan mengganti panggilan ke lokal komposisi Material sesuai kebutuhan sistem desain Anda sendiri.

Bermigrasi dari Indication ke IndicationNodeFactory

Melewati sekitar Indication

Jika Anda hanya membuat Indication untuk diteruskan, seperti membuat ripple yang akan diteruskan ke Modifier.clickable atau Modifier.indication, Anda tidak perlu melakukan perubahan. IndicationNodeFactory mewarisi dari Indication, sehingga semuanya akan terus mengkompilasi dan berfungsi.

Membuat Indication

Jika Anda membuat implementasi Indication sendiri, migrasi harus sederhana dalam kebanyakan kasus. Misalnya, pertimbangkan Indication yang menerapkan efek skala pada pers:

object ScaleIndication : Indication {
    @Composable
    override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
        // key the remember against interactionSource, so if it changes we create a new instance
        val instance = remember(interactionSource) { ScaleIndicationInstance() }

        LaunchedEffect(interactionSource) {
            interactionSource.interactions.collectLatest { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                    is PressInteraction.Release -> instance.animateToResting()
                    is PressInteraction.Cancel -> instance.animateToResting()
                }
            }
        }

        return instance
    }
}

private class ScaleIndicationInstance : IndicationInstance {
    var currentPressPosition: Offset = Offset.Zero
    val animatedScalePercent = Animatable(1f)

    suspend fun animateToPressed(pressPosition: Offset) {
        currentPressPosition = pressPosition
        animatedScalePercent.animateTo(0.9f, spring())
    }

    suspend fun animateToResting() {
        animatedScalePercent.animateTo(1f, spring())
    }

    override fun ContentDrawScope.drawIndication() {
        scale(
            scale = animatedScalePercent.value,
            pivot = currentPressPosition
        ) {
            this@drawIndication.drawContent()
        }
    }
}

Anda dapat memigrasikannya dalam dua langkah:

  1. Migrasikan ScaleIndicationInstance menjadi DrawModifierNode. Platform API untuk DrawModifierNode sangat mirip dengan IndicationInstance: metode ini mengekspos fungsi ContentDrawScope#draw() yang secara fungsional setara dengan IndicationInstance#drawContent(). Anda perlu mengubah fungsi tersebut, dan kemudian terapkan logika collectLatest di dalam node secara langsung, bukan secara Indication.

    Misalnya, cuplikan berikut menggunakan API yang tidak digunakan lagi:

    private class ScaleIndicationInstance : IndicationInstance {
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun ContentDrawScope.drawIndication() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@drawIndication.drawContent()
            }
        }
    }

    Anda harus mengubah cuplikan di atas menjadi:

    private class ScaleIndicationNode(
        private val interactionSource: InteractionSource
    ) : Modifier.Node(), DrawModifierNode {
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        private suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        private suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun onAttach() {
            coroutineScope.launch {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> animateToResting()
                        is PressInteraction.Cancel -> animateToResting()
                    }
                }
            }
        }
    
        override fun ContentDrawScope.draw() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@draw.drawContent()
            }
        }
    }

  2. Migrasikan ScaleIndication untuk menerapkan IndicationNodeFactory. Karena logika koleksi kini dipindahkan ke node, ini adalah factory yang sangat sederhana yang satu-satunya tanggung jawabnya adalah membuat instance node.

    Misalnya, cuplikan berikut menggunakan API yang tidak digunakan lagi:

    object ScaleIndication : Indication {
        @Composable
        override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
            // key the remember against interactionSource, so if it changes we create a new instance
            val instance = remember(interactionSource) { ScaleIndicationInstance() }
    
            LaunchedEffect(interactionSource) {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> instance.animateToResting()
                        is PressInteraction.Cancel -> instance.animateToResting()
                    }
                }
            }
    
            return instance
        }
    }

    Anda harus mengubah cuplikan di atas menjadi:

    object ScaleIndicationNodeFactory : IndicationNodeFactory {
        override fun create(interactionSource: InteractionSource): DelegatableNode {
            return ScaleIndicationNode(interactionSource)
        }
    
        override fun hashCode(): Int = -1
    
        override fun equals(other: Any?) = other === this
    }

Menggunakan Indication untuk membuat IndicationInstance

Dalam sebagian besar kasus, Anda harus menggunakan Modifier.indication untuk menampilkan Indication untuk komponen. Namun, dalam kasus yang jarang terjadi, Anda membuat IndicationInstance menggunakan rememberUpdatedInstance, Anda perlu mengupdate implementasi untuk memeriksa apakah Indication adalah IndicationNodeFactory sehingga Anda bisa menggunakan implementasi yang lebih ringan. Misalnya, Modifier.indication akan delegasikan secara internal ke node yang dibuat jika node tersebut adalah IndicationNodeFactory. Jika tidak, fungsi Modifier.composed akan digunakan untuk memanggil rememberUpdatedInstance.