Compose'da özel tasarım sistemleri

Önerdiğimiz tasarım sistemimiz Materyal'dir ve Jetpack Compose bir Materyal uygulaması sunsa da bu özelliği kullanmak zorunda kalmazsınız. Materyal tümüyle herkese açık API'ler üzerinde derlendiğinden, aynı şekilde kendi tasarım sisteminizi de oluşturabilirsiniz.

Bu konuda uygulayabileceğiniz çeşitli yaklaşımlar vardır:

Ayrıca, Malzeme bileşenlerini özel bir tasarım sistemiyle kullanmaya devam edebilirsiniz. Bunu yapmak mümkün olsa da benimsediğiniz yaklaşıma uygun olması için aklınızda bulundurmanız gereken noktalar vardır.

MaterialTheme ve özel tasarım sistemleri tarafından kullanılan alt düzey yapılar ve API'ler hakkında daha fazla bilgi edinmek için Compose'da bir temanın anatomisi kılavuzuna göz atın.

Materyal Temasını Genişletme

Oluşturma Materyali, Materyal yönergelerini izlemenin basit ve güvenli olması için Materyal Tema'yı yakından modeller. Ancak renk, tipografi ve şekil gruplarını ek değerlerle genişletmek mümkündür.

En basit yaklaşım uzantı özellikleri eklemektir:

// Use with MaterialTheme.colors.snackbarAction
val Colors.snackbarAction: Color
    get() = if (isLight) 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)

Bu, MaterialTheme kullanım API'leriyle tutarlılık sağlar. Compose'un kendisi tarafından tanımlanan bunun bir örneği, Colors.isLight'ye bağlı olarak primary ile surface arasında bir proxy işlevi gören primarySurface örneğidir.

Diğer bir yaklaşım da MaterialTheme öğesini ve değerlerini "sarlayan" genişletilmiş bir tema tanımlamaktır.

Mevcut Malzeme renklerini korurken tertiary ve onTertiary şeklinde iki renk daha eklemek istediğinizi varsayalım:

@Immutable
data class ExtendedColors(
    val tertiary: Color,
    val onTertiary: Color
)

val LocalExtendedColors = staticCompositionLocalOf {
    ExtendedColors(
        tertiary = Color.Unspecified,
        onTertiary = Color.Unspecified
    )
}

@Composable
fun ExtendedTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val extendedColors = ExtendedColors(
        tertiary = Color(0xFFA8EFF0),
        onTertiary = Color(0xFF002021)
    )
    CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
        MaterialTheme(
            /* colors = ..., typography = ..., shapes = ... */
            content = content
        )
    }
}

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

Bu, MaterialTheme kullanım API'lerine benzer. ExtendedTheme öğelerini MaterialTheme ile aynı şekilde iç içe yerleştirebileceğiniz için birden çok temayı da destekler.

Material bileşenlerini kullanma

Materyal Teması genişletilirken mevcut MaterialTheme değerleri korunur ve Malzeme bileşenlerinin varsayılan değerleri hâlâ makuldür.

Bileşenlerde genişletilmiş değerler kullanmak istiyorsanız bunları kendi composable işlevlerinizde sarmalayın, değiştirmek istediğiniz değerleri doğrudan ayarlayın ve diğerlerini içeren composable'a parametre olarak maruz bırakın:

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

Bu durumda Button kullanımlarını uygun durumlarda ExtendedButton ile değiştirirsiniz.

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

Malzeme sistemlerini değiştirme

Materyal Teması'nı genişletmek yerine, bir veya daha fazla sistemi (Colors, Typography veya Shapes) özel bir uygulamayla değiştirirken diğerlerini koruyabilirsiniz.

Renk sistemini korurken tür ve şekil sistemlerini değiştirmek istediğinizi varsayalım:

@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
}

Material bileşenlerini kullanma

Bir veya daha fazla MaterialTheme sistemi değiştirildiğinde, Malzeme bileşenlerini olduğu gibi kullanmak istenmeyen Malzeme renk, tür veya şekil değerlerine neden olabilir.

Bileşenlerde değiştirme değerleri kullanmak isterseniz bunları kendi composable işlevlerinizde sarmalayın, ilgili sistemin değerlerini doğrudan ayarlayın ve diğerlerini de içeren composable'a parametre olarak gösterin.

@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()
            }
        }
    )
}

Bu durumda Button kullanımlarını uygun durumlarda ReplacementButton ile değiştirirsiniz.

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

Tamamen özel bir tasarım sistemi uygulamak

Materyal Teması'nı tamamen özel bir tasarım sistemiyle değiştirmek isteyebilirsiniz. MaterialTheme ürününün aşağıdaki sistemleri sağladığını göz önünde bulundurun:

  • Colors, Typography ve Shapes: Materyal Tema oluşturma sistemleri
  • ContentAlpha: Text ve Icon ürünlerinde vurguyu aktarmak için opaklık seviyeleri
  • TextSelectionColors: Text ve TextField tarafından metin seçiminde kullanılan renkler
  • Ripple ve RippleTheme: Indication öğesinin materyal uygulaması

Materyal bileşenlerini kullanmaya devam etmek istiyorsanız istenmeyen davranışlardan kaçınmak için özel temanızda veya temalarınızda bu sistemlerden bazılarını değiştirmeniz veya bileşenlerinizdeki sistemleri işlemeniz gerekir.

Ancak tasarım sistemleri, Materyal'in temel aldığı kavramlarla sınırlı değildir. Diğer kavramları temalarla uyumlu hale getirmek için mevcut sistemleri değiştirebilir ve yeni sınıflar ve türlerle tamamen yenilerini kullanıma sunabilirsiniz.

Aşağıdaki kodda, gradyanlar (List<Color>) içeren özel bir renk sistemini modeller, bir tür sistemi ekler, yeni bir yükseklik sistemi ekler ve MaterialTheme tarafından sağlanan diğer sistemleri hariç tutarız:

@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
}

Material bileşenlerini kullanma

MaterialTheme yoksa, Malzeme bileşenlerini olduğu gibi kullanmak istenmeyen Malzeme rengi, türü, şekli değerleri ve gösterge davranışlarına neden olur.

Bileşenlerde özel değerler kullanmak istiyorsanız bunları kendi composable işlevlerinizde sarmalayın, ilgili sistem için değerleri doğrudan ayarlayın ve diğerlerini parametre olarak içeren composable'a gösterin.

Özel temanızdan ayarladığınız değerlere erişmenizi öneririz. Alternatif olarak, temanız Color, TextStyle, Shape veya diğer sistemleri sağlamıyorsa bunları sabit bir şekilde kodlayabilirsiniz.

@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 = ContentAlpha.disabled)
        ),
        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)

Renk geçişlerini göstermek için List<Color> gibi yeni sınıf türleri kullanıma sunduysanız bileşenleri sarmalamak yerine sıfırdan uygulamak daha iyi olabilir. Örneğin, Jetsnack örneğinden JetsnackButton adresine bakalım.