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 Anda 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 mungkin dilakukan, tetapi ada hal-hal yang perlu diingat agar sesuai dengan pendekatan yang Anda ambil.

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

Memperluas Tema Material

Compose Material dengan cermat mengikuti Tema Material untuk membuatnya mudah dan aman untuk mengikuti panduan Material. Namun, Anda dapat memperluas kumpulan 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. Contoh hal ini yang ditentukan oleh Compose itu sendiri adalah surfaceColorAtElevation, yang menentukan warna platform yang harus digunakan, bergantung pada ketinggian.

Pendekatan lainnya adalah menentukan tema tambahan yang "menggabungkan" MaterialTheme dan nilainya.

Misalnya Anda ingin menambahkan dua warna tambahan — caution dan onCaution, warna kuning yang digunakan untuk tindakan yang semi-berbahaya — dengan tetap 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 Anda ingin menggunakan nilai yang diperluas dalam komponen, gabungkan ke dalam fungsi composable Anda sendiri, langsung tetapkan nilai yang ingin Anda ubah, dan tampilkan yang lain sebagai parameter ke composable yang berisi:

@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 Tema Material, Anda dapat mengganti satu atau beberapa sistem — Colors, Typography, atau Shapes — dengan implementasi kustom, sambil mempertahankan sistem lainnya.

Misalkan Anda ingin mengganti jenis dan sistem 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 dapat mengganti Tema Material dengan sistem desain kustom sepenuhnya. Perhatikan bahwa MaterialTheme menyediakan sistem berikut:

  • Colors, Typography, dan Shapes: Sistem Tema Material
  • TextSelectionColors: Warna yang digunakan untuk pemilihan teks oleh Text dan TextField
  • Ripple dan RippleTheme: Implementasi material dari Indication

Jika ingin terus menggunakan komponen Material, Anda harus mengganti beberapa sistem ini di tema atau tema kustom, atau menangani sistem di komponen Anda 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:

@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 lain, Anda dapat meng-hardcode-nya.

@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 — mungkin akan lebih baik jika Anda menerapkan komponen dari awal daripada menggabungkannya. Misalnya, lihat JetsnackButton dari contoh Jetsnack.