Bermigrasi ke Indication dan Ripple API

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

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

Tidak digunakan lagi

Penggantian

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

Sebagai gantinya, ripple() API baru disediakan di library Material.

Catatan: Dalam konteks ini, "Library Material" mengacu pada 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 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, library tersebut menggunakan API ripple baru. Akibatnya, mereka tidak membuat kueri LocalRippleTheme. Oleh karena itu, jika Anda menetapkan LocalRippleTheme di aplikasi, komponen Material tidak akan menggunakan nilai ini.

Bagian berikut menjelaskan cara beralih kembali ke perilaku lama untuk sementara tanpa melakukan migrasi; namun, sebaiknya lakukan migrasi ke API baru. Untuk petunjuk migrasi, lihat Bermigrasi dari rememberRipple ke ripple dan bagian berikutnya.

Mengupgrade versi library Material tanpa bermigrasi

Untuk berhenti memblokir upgrade versi library, Anda dapat menggunakan LocalUseFallbackRippleImplementation CompositionLocal API sementara untuk mengonfigurasi komponen Material agar kembali ke perilaku lama:

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

Pastikan untuk menyediakannya di luar MaterialTheme sehingga ripple lama dapat diberikan melalui LocalIndication.

Bagian berikut menjelaskan cara melakukan migrasi ke API baru.

Bermigrasi dari rememberRipple ke ripple

Menggunakan library Material

Jika Anda menggunakan library Material, ganti langsung rememberRipple() dengan panggilan ke ripple() dari library yang sesuai. API ini membuat ripple menggunakan nilai yang berasal dari API tema Material. Kemudian, teruskan objek yang ditampilkan 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 memodifikasi cuplikan di atas untuk:

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

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

Mengimplementasikan sistem desain kustom

Jika Anda mengimplementasikan sistem desain sendiri, dan sebelumnya menggunakan rememberRipple() bersama dengan RippleTheme kustom untuk mengonfigurasi ripple, Anda harus menyediakan API ripple Anda sendiri yang didelegasikan ke API node ripple yang diekspos dalam material-ripple. Kemudian, komponen Anda dapat menggunakan ripple Anda sendiri yang memakai nilai tema secara langsung. Untuk mengetahui informasi selengkapnya, lihat Bermigrasi dariRippleTheme.

Bermigrasi dari RippleTheme

Memilih tidak mengikuti perubahan perilaku untuk sementara

Library Material memiliki CompositionLocal sementara, LocalUseFallbackRippleImplementation, yang dapat Anda gunakan untuk mengonfigurasi semua komponen Material agar dapat kembali menggunakan rememberRipple. Dengan cara ini, rememberRipple akan terus mengkueri LocalRippleTheme.

Cuplikan kode berikut menunjukkan cara menggunakan API LocalUseFallbackRippleImplementation CompositionLocal:

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

Jika menggunakan tema aplikasi kustom yang di-build di atas Material, Anda dapat menyediakan komposisi lokal 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 bagian Mengupgrade versi library Material tanpa memigrasikan.

Menggunakan RippleTheme untuk menonaktifkan ripple untuk komponen tertentu

Library material dan material3 mengekspos RippleConfiguration dan LocalRippleConfiguration, yang memungkinkan Anda mengonfigurasi tampilan ripple dalam sub-hierarki. Perlu diperhatikan bahwa RippleConfiguration dan LocalRippleConfiguration bersifat eksperimental, dan hanya dimaksudkan untuk penyesuaian per komponen. Penyesuaian tingkat global/tema tidak didukung dengan API ini; lihat Menggunakan RippleTheme untuk mengubah semua ripple secara global dalam aplikasi untuk mengetahui 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 memodifikasi cuplikan di atas untuk:

@OptIn(ExperimentalMaterialApi::class)
private val DisabledRippleConfiguration =
    RippleConfiguration(isEnabled = false)

// ...
    CompositionLocalProvider(LocalRippleConfiguration provides DisabledRippleConfiguration) {
        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 memodifikasi cuplikan di atas untuk:

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

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

Menggunakan RippleTheme untuk mengubah semua ripple secara global di aplikasi

Sebelumnya, Anda dapat menggunakan LocalRippleTheme untuk menentukan perilaku ripple pada tingkat seluruh tema. Ini pada dasarnya adalah titik integrasi antara lokal komposisi sistem desain khusus dan ripple. Bukannya mengekspos primitif tema generik, material-ripple kini mengekspos fungsi createRippleModifierNode(). Fungsi ini memungkinkan library sistem desain membuat implementasi wrapper tingkat tinggi, yang mengkueri nilai temanya, lalu mendelegasikan implementasi ripple ke node yang dibuat oleh fungsi ini.

Hal ini memungkinkan sistem desain untuk langsung membuat kueri apa yang diperlukan, dan mengekspos lapisan tema yang dapat dikonfigurasi pengguna di bagian atas tanpa harus sesuai dengan yang disediakan pada lapisan material-ripple. Perubahan ini juga menjadikan tema/spesifikasi yang sesuai dengan ripple secara lebih eksplisit, karena API ripple itu sendirilah yang mendefinisikan kontrak tersebut, bukan diambil secara implisit dari tema.

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

Bermigrasi dari Indication ke IndicationNodeFactory

Melewati sekitar Indication

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

Membuat Indication

Jika Anda membuat implementasi Indication sendiri, dalam sebagian besar kasus, migrasi akan menjadi sederhana. 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: platform ini menampilkan fungsi ContentDrawScope#draw() yang secara fungsional setara dengan IndicationInstance#drawContent(). Anda perlu mengubah fungsi tersebut, lalu menerapkan logika collectLatest di dalam node secara langsung, bukan 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 memodifikasi cuplikan di atas untuk:

    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 mengimplementasikan IndicationNodeFactory. Karena logika koleksi sekarang dipindahkan ke dalam node, ini adalah objek factory sangat sederhana yang satu-satunya tanggung jawab 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 memodifikasi cuplikan di atas untuk:

    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

Pada umumnya, Anda harus menggunakan Modifier.indication untuk menampilkan Indication untuk komponen. Namun, dalam kasus yang jarang terjadi saat Anda membuat IndicationInstance secara manual menggunakan rememberUpdatedInstance, Anda perlu mengupdate implementasi untuk memeriksa apakah Indication adalah IndicationNodeFactory sehingga Anda dapat menggunakan implementasi yang lebih ringan. Misalnya, Modifier.indication akan didelegasikan secara internal ke node yang dibuat jika node tersebut adalah IndicationNodeFactory. Jika tidak, Modifier.composed akan digunakan untuk memanggil rememberUpdatedInstance.