การกำหนดธีมด้วยสไตล์

คุณสามารถสร้างแอปได้หลายวิธีโดยใช้สไตล์ สิ่งที่คุณเลือกจะขึ้นอยู่กับตำแหน่งของแอปเมื่อเทียบกับการนำ Material Design ไปใช้ ดังนี้

  1. ระบบการออกแบบที่กำหนดเองทั้งหมด ไม่ได้ใช้ Material Design
    • คำแนะนำ: กำหนดสไตล์คอมโพเนนต์ที่ใช้ค่าจาก ธีม และแสดงพารามิเตอร์สไตล์ในคอมโพเนนต์ระบบการออกแบบ
  2. ใช้ Material Design
    • คำแนะนำ: รอการนำ Material ไปใช้เพื่อผสานรวมกับสไตล์ ใช้สไตล์กับคอมโพเนนต์ของคุณเองเมื่อทำได้

เลเยอร์สไตล์

ในโมเดล Compose แบบดั้งเดิม การปรับแต่งมักจะขึ้นอยู่กับการลบล้างโทเค็นส่วนกลาง (สีและตัวอักษร) ที่ MaterialTheme ให้มา หรือการห่อและลบล้างพร็อพเพอร์ตี้ของคอมโพสได้ของระบบการออกแบบเมื่อทำได้ บางครั้งอาจมีพร็อพเพอร์ตี้ภายในเลเยอร์ Material ที่ไม่ได้แสดงผ่านระบบย่อยหรือพารามิเตอร์ แต่เป็นค่าเริ่มต้นที่ฮาร์ดโค้ดในคอมโพเนนต์เอง

Styles API มีเลเยอร์การแยกย่อยใหม่ที่เป็นสะพานเชื่อมระหว่างระบบย่อยกับคอมโพเนนต์ นั่นคือ สไตล์

เลเยอร์ ความรับผิดชอบ ตัวอย่าง
ค่าระบบย่อย ค่าที่มีชื่อ val Primary = Color(0xFF34A85E)
สไตล์ย่อย สไตล์ที่เปลี่ยนพร็อพเพอร์ตี้เพียงรายการเดียว val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then largeSize then interactiveShadowAtomic
สไตล์คอมโพเนนต์ การกำหนดค่าเฉพาะคอมโพเนนต์ ปุ่มที่มีพื้นหลังสีหลักและระยะห่างจากขอบ 16dp val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }
คอมโพเนนต์ องค์ประกอบ UI ที่ใช้งานได้ซึ่งใช้สไตล์ Button(style = buttonStyle) { ... }
แผนภาพแสดงการกำหนดธีมด้วยสไตล์พร้อมการเปิดตัวเลเยอร์ใหม่
รูปที่ 1 ตัวอย่างคอมโพเนนต์และวิธีเข้าถึงสไตล์จากธีม

สไตล์ย่อยเทียบกับสไตล์แบบรวม

Styles API ช่วยให้คุณแยกสไตล์ออกเป็นสไตล์ย่อยๆ ได้ นอกจากจะกำหนดสไตล์ที่ซับซ้อนและเฉพาะคอมโพเนนต์ เช่น baseButtonStyle แล้ว คุณยังสร้างสไตล์ยูทิลิตี้ขนาดเล็กที่มีวัตถุประสงค์เดียวได้ด้วย ซึ่งจะทำหน้าที่เป็น "อะตอม" ของคุณ

// Define single-purpose "atomic" styles
val paddingAtomic = Style {
    contentPadding(16.dp)
}
val roundedCornerShapeAtomic = Style {
    shape(RoundedCornerShape(8.dp))
}
val primaryBackgroundAtomic = Style {
    background(Color.Blue)
}
val largeSizeAtomic = Style {
    size(100.dp, 40.dp)
}
val interactiveShadowAtomic = Style {
    hovered {
        animate {
            dropShadow(
                Shadow(
                    offset = DpOffset(
                        0.dp,
                        0.dp
                    ),
                    radius = 2.dp,
                    spread = 0.dp,
                    color = Color.Blue,
                )
            )
        }
    }
}

การคอมโพสโดยใช้ "then"

ฟีเจอร์ที่มีประสิทธิภาพอย่างหนึ่งของ Styles API ใหม่คือโอเปอเรเตอร์ then ซึ่งช่วยให้คุณผสานออบเจ็กต์ Style หลายรายการได้ ซึ่งจะช่วยให้คุณสร้างคอมโพเนนต์โดยใช้คลาสยูทิลิตี้ย่อยได้

แบบดั้งเดิม (ไม่ใช่แบบย่อย):

// One large monolithic style
val buttonStyle = Style {
    contentPadding(16.dp)
    shape(RoundedCornerShape(8.dp))
    background(Color.Blue)
}

การปรับโครงสร้างแบบย่อย:

// Combine atoms to create the final appearance
val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic

นำสไตล์ไปใช้ในระบบการออกแบบ

พิจารณาตัวเลือกต่อไปนี้เมื่อนำสไตล์ไปใช้ในระบบการออกแบบ โดยขึ้นอยู่กับตำแหน่งของระบบการออกแบบในสเปกตรัม

ระบบการออกแบบที่กำหนดเองพร้อมสไตล์

พิจารณาเมื่อ: คุณได้รับคู่มือแบรนด์ที่ครอบคลุมซึ่งไม่ได้ อิงตาม Material Design และคุณไม่ได้วางแผนที่จะใช้ Material Design

กลยุทธ์: ใช้ระบบการออกแบบที่กำหนดเองทั้งหมด และแสดงสไตล์เป็นส่วนหนึ่ง ของธีม.

ตัวเลือกนี้เป็นเส้นทางที่กำหนดเองหากคุณไม่ได้ใช้ Material เป็นภาษาหลักของระบบการออกแบบ คุณข้าม MaterialTheme ทั้งหมดสำหรับการกำหนดภาพ และ สร้างธีมที่กำหนดเองแล้ว คุณสร้าง CompanyTheme ที่ทำหน้าที่เป็นคอนเทนเนอร์สำหรับสไตล์

  • วิธีการทำงาน: สร้างออบเจ็กต์ CompanyTheme ที่เก็บออบเจ็กต์ Style สำหรับทุกคอมโพเนนต์ในระบบ คอมโพเนนต์ของคุณ (ทั้ง Wrapper รอบตรรกะ Material หรือการใช้งาน Box หรือ Layout ที่กำหนดเอง) ใช้สไตล์เหล่านี้โดยตรง และแสดงพารามิเตอร์ Style สำหรับผู้ใช้ระบบการออกแบบ
  • เลเยอร์สไตล์: สไตล์คือคำจำกัดความหลักของระบบการออกแบบ โทเค็นคือตัวแปรที่มีชื่อซึ่งป้อนลงในสไตล์เหล่านี้ ซึ่งช่วยให้ปรับแต่งได้อย่างละเอียด เช่น การกำหนดภาพเคลื่อนไหวที่ไม่ซ้ำกันสำหรับการเปลี่ยนแปลงสถานะ (เช่น การปรับขนาดและสีเมื่อกด)

หากคุณกำลังสร้างธีมที่กำหนดเองโดยไม่ได้ใช้ Material และ ต้องการนำสไตล์ไปใช้ ให้เพิ่มรายการสไตล์ลงในธีม ซึ่งจะช่วยให้คุณเข้าถึงสไตล์พื้นฐานได้จากทุกที่ในโปรเจ็กต์

  1. สร้างคลาส Styles ที่จัดเก็บสไตล์ต่างๆ ในแอปพลิเคชันและสร้างค่าเริ่มต้น เช่น ในแอป Jetsnack คลาสนี้ชื่อ JetsnackStyles

    object JetsnackStyles{
        val buttonStyle: Style = Style {
            shape(shapes.medium)
            background(colors.brand)
            contentColor(colors.textPrimary)
            contentPaddingVertical(8.dp)
            contentPaddingHorizontal(24.dp)
            textStyle(typography.labelLarge)
            disabled {
                animate {
                    background(colors.brandSecondary)
                }
            }
        }
        val cardStyle: Style = Style {
            shape(shapes.medium)
            background(colors.uiBackground)
            contentColor(colors.textPrimary)
        }
    }

  2. ระบุ Styles เป็นส่วนหนึ่งของธีมโดยรวม และแสดงฟังก์ชันส่วนขยายตัวช่วยใน StyleScope เพื่อเข้าถึงระบบย่อย

    @Immutable
    class JetsnackTheme(
        val colors: JetsnackColors = LightJetsnackColors,
        val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(),
        val shapes: Shapes = Shapes()
    ) {
        companion object {
            val colors: JetsnackColors
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.colors
    
            val typography: androidx.compose.material3.Typography
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.typography
    
            val shapes: Shapes
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.shapes
    
            val styles: JetsnackStyles = JetsnackStyles
    
            val LocalJetsnackTheme: ProvidableCompositionLocal<JetsnackTheme>
                get() = LocalJetsnackThemeInstance
        }
    }
    
    val StyleScope.colors: JetsnackColors
        get() = LocalJetsnackTheme.currentValue.colors
    
    val StyleScope.typography: androidx.compose.material3.Typography
        get() = LocalJetsnackTheme.currentValue.typography
    
    val StyleScope.shapes: Shapes
        get() = LocalJetsnackTheme.currentValue.shapes
    
    internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() }
    
    @Composable
    fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
        val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors
        val theme = JetsnackTheme(colors = colors)
    
        CompositionLocalProvider(
            LocalJetsnackTheme provides theme,
        ) {
            MaterialTheme(
                typography = LocalJetsnackTheme.current.typography,
                shapes = LocalJetsnackTheme.current.shapes,
                content = content,
            )
        }
    }

  3. เข้าถึง JetsnackStyles ภายในคอมโพสได้

    @Composable
    fun CustomButton(modifier: Modifier,
                     style: Style = Style,
                     text: String) {
        val interactionSource = remember { MutableInteractionSource() }
        val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    
        // Apply style to top level container in combination with incoming style from parameter.
        Box(modifier = modifier
            .clickable(
                interactionSource = interactionSource,
                indication = null,
                enabled = true,
                role = Role.Button,
                onClick = {
    
                },
            )
            .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) {
            Text(text)
        }
    }

นอกเหนือจากการนำธีมส่วนกลางไปใช้แล้ว ยังมีกลยุทธ์ทางเลือกอื่นๆ สำหรับการรวม Styles ไว้ในแอปด้วย คุณสามารถใช้ประโยชน์จาก Styles แบบอินไลน์สำหรับตำแหน่งที่เรียกใช้ที่เฉพาะเจาะจง หรือใช้คำจำกัดความแบบคงที่เมื่อไม่จำเป็นต้องใช้ความสามารถในการกำหนดธีมทั้งหมด ไม่ควรสลับ Styles ตามเงื่อนไข เว้นแต่สไตล์ทั้งหมดจะแตกต่างกันโดยสิ้นเชิง คุณควรเลือกเข้าถึงโทเค็นแบบไดนามิกภายในคำจำกัดความภาพแทนการสลับระหว่างออบเจ็กต์สไตล์ที่แตกต่างกัน