Sistem desain kustom di Compose

Meskipun Material adalah sistem desain yang kami rekomendasikan dan Jetpack Compose menghadirkan implementasi Material, Anda tidak diharuskan untuk menggunakannya. Material dibuat sepenuhnya di API publik, sehingga Anda dapat membuat sistem desain sendiri dengan cara yang sama.

Ada beberapa pendekatan yang dapat Anda lakukan:

Anda mungkin juga ingin terus menggunakan komponen Material dengan sistem desain kustom. Hal ini dapat dilakukan, tetapi ada beberapa hal yang perlu diingat agar sesuai dengan pendekatan yang Anda lakukan.

Untuk mempelajari konstruksi dan API tingkat lebih rendah yang digunakan oleh MaterialTheme dan sistem desain kustom, lihat panduan Anatomi tema di Compose.

Memperluas Penerapan Tema Material

Compose Material sangat mirip dengan Penerapan Tema Material untuk membuatnya mudah dan aman dalam mengikuti panduan Material. Namun, Anda dapat memperluas set warna, tipografi, dan bentuk dengan nilai tambahan. Pendekatan yang paling sederhana adalah menambahkan properti ekstensi:

// Use with MaterialTheme.colorScheme.snackbarAction
val ColorScheme.snackbarAction: Color
    @Composable
    get() = if (isSystemInDarkTheme()) Red300 else Red700

// Use with MaterialTheme.typography.textFieldInput
val Typography.textFieldInput: TextStyle
    get() = TextStyle(/* ... */)

// Use with MaterialTheme.shapes.card
val Shapes.card: Shape
    get() = RoundedCornerShape(size = 20.dp)

Hal ini memberikan konsistensi dengan API penggunaan MaterialTheme. Contohnya, yang ditentukan oleh Compose sendiri adalah surfaceColorAtElevation, yang menentukan warna platform yang harus digunakan bergantung pada elevasi.

Pendekatan lain adalah menentukan tema yang diperluas yang "menggabungkan" MaterialTheme dan nilainya.

Misalnya, Anda ingin menambahkan dua warna tambahan — caution dan onCaution, warna kuning yang digunakan untuk tindakan yang semi-berbahaya — sambil mempertahankan warna Material yang ada:

@Immutable
data class ExtendedColors(
    val caution: Color,
    val onCaution: Color
)

val LocalExtendedColors = staticCompositionLocalOf {
    ExtendedColors(
        caution = Color.Unspecified,
        onCaution = Color.Unspecified
    )
}

@Composable
fun ExtendedTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val extendedColors = ExtendedColors(
        caution = Color(0xFFFFCC02),
        onCaution = Color(0xFF2C2D30)
    )
    CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
        MaterialTheme(
            /* colors = ..., typography = ..., shapes = ... */
            content = content
        )
    }
}

// Use with eg. ExtendedTheme.colors.caution
object ExtendedTheme {
    val colors: ExtendedColors
        @Composable
        get() = LocalExtendedColors.current
}

Ini mirip dengan API penggunaan MaterialTheme. Ini juga mendukung beberapa tema karena Anda dapat menyarangkan ExtendedTheme dengan cara yang sama seperti MaterialTheme.

Menggunakan komponen Material

Saat memperluas Tema Material, nilai MaterialTheme yang ada akan dipertahankan dan komponen Material masih memiliki default yang wajar.

Jika ingin menggunakan nilai yang diperluas dalam komponen, gabungkan ke dalam fungsi composable, langsung tetapkan nilai yang ingin Anda ubah, dan tampilkan nilai lain sebagai parameter ke composable tersebut:

@Composable
fun ExtendedButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = ExtendedTheme.colors.caution,
            contentColor = ExtendedTheme.colors.onCaution
            /* Other colors use values from MaterialTheme */
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

Kemudian, Anda akan mengganti penggunaan Button dengan ExtendedButton jika sesuai.

@Composable
fun ExtendedApp() {
    ExtendedTheme {
        /*...*/
        ExtendedButton(onClick = { /* ... */ }) {
            /* ... */
        }
    }
}

Mengganti subsistem Material

Daripada memperluas Penerapan Tema Material, Anda mungkin ingin mengganti satu atau beberapa sistem — ColorScheme, Typography, atau Shapes — dengan implementasi kustom, sambil mempertahankan yang lain.

Misalnya, Anda ingin mengganti sistem jenis dan bentuk sambil mempertahankan sistem warna:

@Immutable
data class ReplacementTypography(
    val body: TextStyle,
    val title: TextStyle
)

@Immutable
data class ReplacementShapes(
    val component: Shape,
    val surface: Shape
)

val LocalReplacementTypography = staticCompositionLocalOf {
    ReplacementTypography(
        body = TextStyle.Default,
        title = TextStyle.Default
    )
}
val LocalReplacementShapes = staticCompositionLocalOf {
    ReplacementShapes(
        component = RoundedCornerShape(ZeroCornerSize),
        surface = RoundedCornerShape(ZeroCornerSize)
    )
}

@Composable
fun ReplacementTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val replacementTypography = ReplacementTypography(
        body = TextStyle(fontSize = 16.sp),
        title = TextStyle(fontSize = 32.sp)
    )
    val replacementShapes = ReplacementShapes(
        component = RoundedCornerShape(percent = 50),
        surface = RoundedCornerShape(size = 40.dp)
    )
    CompositionLocalProvider(
        LocalReplacementTypography provides replacementTypography,
        LocalReplacementShapes provides replacementShapes
    ) {
        MaterialTheme(
            /* colors = ... */
            content = content
        )
    }
}

// Use with eg. ReplacementTheme.typography.body
object ReplacementTheme {
    val typography: ReplacementTypography
        @Composable
        get() = LocalReplacementTypography.current
    val shapes: ReplacementShapes
        @Composable
        get() = LocalReplacementShapes.current
}

Menggunakan komponen Material

Saat satu atau beberapa sistem MaterialTheme telah diganti, penggunaan komponen Material sebagaimana adanya dapat mengakibatkan warna Material, jenis, atau nilai bentuk yang tidak diinginkan.

Jika ingin menggunakan nilai pengganti dalam komponen, gabungkan ke dalam fungsi composable, langsung tetapkan nilai untuk sistem yang relevan, dan tampilkan nilai lain sebagai parameter ke composable tersebut.

@Composable
fun ReplacementButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        shape = ReplacementTheme.shapes.component,
        onClick = onClick,
        modifier = modifier,
        content = {
            ProvideTextStyle(
                value = ReplacementTheme.typography.body
            ) {
                content()
            }
        }
    )
}

Kemudian, Anda akan mengganti penggunaan Button dengan ReplacementButton jika sesuai.

@Composable
fun ReplacementApp() {
    ReplacementTheme {
        /*...*/
        ReplacementButton(onClick = { /* ... */ }) {
            /* ... */
        }
    }
}

Menerapkan sistem desain yang sepenuhnya kustom

Anda mungkin ingin mengganti Penerapan Tema Material dengan sistem desain yang sepenuhnya kustom. Perhatikan bahwa MaterialTheme menyediakan sistem berikut:

  • ColorScheme, Typography, dan Shapes: Sistem Tema Material
  • TextSelectionColors: Warna yang digunakan untuk pemilihan teks oleh Text dan TextField
  • ripple, disediakan sebagai LocalIndication: Penerapan Material dari Indication

Jika ingin terus menggunakan komponen Material, Anda harus mengganti beberapa sistem ini dalam tema kustom atau menangani sistem dalam komponen untuk menghindari perilaku yang tidak diinginkan.

Namun, sistem desain tidak terbatas pada konsep yang diandalkan Material. Anda dapat mengubah sistem yang ada dan memperkenalkan sistem yang benar-benar baru — dengan class dan jenis baru — untuk membuat konsep lain kompatibel dengan tema.

Dalam kode berikut, kami membuat model sistem warna kustom yang mencakup gradien (List<Color>), menyertakan sistem jenis, memperkenalkan sistem ketinggian baru, dan mengecualikan sistem lain yang disediakan oleh MaterialTheme:

Screenshot UI aplikasi seluler yang menunjukkan sistem desain kustom dengan elemen yang menggunakan gradien untuk warna, tipografi kustom, dan elevasi.

@Immutable
data class CustomColors(
    val content: Color,
    val component: Color,
    val background: List<Color>
)

@Immutable
data class CustomTypography(
    val body: TextStyle,
    val title: TextStyle
)

@Immutable
data class CustomElevation(
    val default: Dp,
    val pressed: Dp
)

val LocalCustomColors = staticCompositionLocalOf {
    CustomColors(
        content = Color.Unspecified,
        component = Color.Unspecified,
        background = emptyList()
    )
}
val LocalCustomTypography = staticCompositionLocalOf {
    CustomTypography(
        body = TextStyle.Default,
        title = TextStyle.Default
    )
}
val LocalCustomElevation = staticCompositionLocalOf {
    CustomElevation(
        default = Dp.Unspecified,
        pressed = Dp.Unspecified
    )
}

@Composable
fun CustomTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val customColors = CustomColors(
        content = Color(0xFFDD0D3C),
        component = Color(0xFFC20029),
        background = listOf(Color.White, Color(0xFFF8BBD0))
    )
    val customTypography = CustomTypography(
        body = TextStyle(fontSize = 16.sp),
        title = TextStyle(fontSize = 32.sp)
    )
    val customElevation = CustomElevation(
        default = 4.dp,
        pressed = 8.dp
    )
    CompositionLocalProvider(
        LocalCustomColors provides customColors,
        LocalCustomTypography provides customTypography,
        LocalCustomElevation provides customElevation,
        content = content
    )
}

// Use with eg. CustomTheme.elevation.small
object CustomTheme {
    val colors: CustomColors
        @Composable
        get() = LocalCustomColors.current
    val typography: CustomTypography
        @Composable
        get() = LocalCustomTypography.current
    val elevation: CustomElevation
        @Composable
        get() = LocalCustomElevation.current
}

Menggunakan komponen Material

Jika tidak ada MaterialTheme, penggunaan komponen Material apa adanya akan menghasilkan warn, jenis, nilai bentuk, serta perilaku indikasi Material yang tidak diinginkan.

Jika ingin menggunakan nilai kustom dalam komponen, gabungkan ke dalam fungsi composable, langsung tetapkan nilai untuk sistem yang relevan, dan ekspos nilai lain sebagai parameter ke composable tersebut.

Sebaiknya akses nilai yang Anda tetapkan dari tema kustom. Atau, jika tema Anda tidak menyediakan Color, TextStyle, Shape, atau sistem lainnya, Anda dapat melakukan hardcode.

@Composable
fun CustomButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = CustomTheme.colors.component,
            contentColor = CustomTheme.colors.content,
            disabledContainerColor = CustomTheme.colors.content
                .copy(alpha = 0.12f)
                .compositeOver(CustomTheme.colors.component),
            disabledContentColor = CustomTheme.colors.content
                .copy(alpha = 0.38f)

        ),
        shape = ButtonShape,
        elevation = ButtonDefaults.elevatedButtonElevation(
            defaultElevation = CustomTheme.elevation.default,
            pressedElevation = CustomTheme.elevation.pressed
            /* disabledElevation = 0.dp */
        ),
        onClick = onClick,
        modifier = modifier,
        content = {
            ProvideTextStyle(
                value = CustomTheme.typography.body
            ) {
                content()
            }
        }
    )
}

val ButtonShape = RoundedCornerShape(percent = 50)

Jika Anda telah memperkenalkan jenis class baru — seperti List<Color> untuk merepresentasikan gradien — sebaiknya implementasikan komponen dari awal, bukan menggabungkannya. Misalnya, lihat JetsnackButton dari contoh Jetsnack.