บทความนี้จะเรียนรู้การจัดสไตล์และทำ theme ให้แอปพลิเคชันใน Jetpack Compose
Compose เป็น UI framework ใหม่สำหรับ Android (รองรับ Desktop และ Web กำลังพัฒนา) แทนระบบ View แบบ XML เก่า
ขณะเขียนอยู่ใน beta release แต่ส่วนนี้คาดว่าไม่เปลี่ยนแปลงมากสำหรับ stable release
หัวข้อครอบคลุม:
- สรุปวิธีเก่าแบบ XML
- ย้ายจาก colors, themes, typography (font) แบบ XML ไป Kotlin Compose
- ตั้งค่า light และ dark themes ในไม่กี่บรรทัด
- ใช้ข้อมูลสไตล์ Kotlin ใหม่ใน composables
- จัดสไตล์ Text Composables โดยเฉพาะ
ก่อนเริ่ม ต้องเข้าใจว่า composable คืออะไร ไม่จะอธิบายที่นี่ เพราะอธิบายไว้ในบทความอื่นแล้ว
วิธีเก่าจัดสไตล์ Android Apps ด้วย XML Resources
ชอบแบ่งปันแรงจูงใจและประวัติเบื้องหลัง หากไม่สนใจ ข้ามไปส่วนปฏิบัติได้เลย
Android Resources
ระบบ Android app resources น่าชื่นชม แต่ทุก design decision มีข้อดีและข้อเสีย
ความท้าทายใหญ่คือ localized resources คือการสร้างแอปที่:
- แสดงข้อความและกราฟิกหลายภาษาและอักษร
- ดูสัดส่วนเหมาะสมกับ form factors หลากหลาย (dimensions, densities ฯลฯ)
Resource system ให้ที่สำหรับนักพัฒนาให้ localized resources ที่ platform เลือกใช้ตอน compile time ประหยัด boilerplate code
Feature หรือ Flaw?
แม้จะไม่อยากจัดการ boilerplate สำหรับ string resources เอง แต่ก็ไม่ชอบเขียน XML
มีเพียงไม่กี่สิ่งที่ชอบทำใน XML มากกว่า Kotlin หรือ Swift ที่ทันสมัยและขèvent
เหตุผลทางเทคนิค: XML resources ไม่ใช่ ideal เสมอไป
เพื่อรวม XML-based resources เข้า JVM-based app code ต้องมี layers of translation (compilation) และ platform bridges (APIs) สร้างความยากลำบากให้ทั้ง platform และ app developers
ปัญหาสองประการที่พบ:
- ต้องการเข้าถึง resource ที่ไม่ต้องการ tight coupling กับ platform APIs
- ต้องเขียน boilerplate code มากแค่เปลี่ยนลักษณะ View (override resource styles/themes)
Root problem คือ tight coupling กับ View system และ Android resource system (ซึ่ง coupled กันเอง)
Platform developers ต้อง build บน codebase เก่าและใหญ่ พร้อมรองรับ Android OS เก่า งานที่ขอบคุณไม่ได้
ผลกระทบต่อ app developers คือ boilerplate code มาก, workarounds แบบแฮ็ก สำหรับสิ่งที่ควรเป็น one-liners API หลักคือ Context ซึ่งไม่ควร leak memory
เข้าสู่ Jetpack Compose.
ตั้งค่า Themes, Colors, และ Fonts ด้วย Jetpack Compose
หลังทบทวนระบบเก่า มาสำรวจวิธีสวยง่ายและเรียบง่ายกว่า เนื่องจากทำงานใน Kotlin หมายความว่า:
เราและ platform developers ไม่ถูกผูกมัดด้วย translation (compilation) และ API bridges (Android R class และ Context) ระหว่าง XML และ JVM
หมายถึง boilerplate code น้อยลงมาก และ control มากขึ้นตอน runtime
สำหรับส่วนปฏิบัติ แนะนำทำตามลำดับที่อธิบาย โครงสร้างตามลำดับที่ผมเขียนโค้ดในแอปใหม่
แทนที่ Colors.xml Resources ด้วย Kotlin Compose
หากยังไม่เลือก color scheme แนะนำใช้ทรัพยากรจาก Material Design website:
- Color palettes
- Color tool
หากวางแผนรองรับ light/dark themes เลือก color scheme ที่รองรับข้อความสีขาวและสีดำ

สร้างไฟล์ Color.kt (ชื่อไม่สำคัญ) เติมด้วย immutable values:
import androidx.compose.ui.graphics.Color val primaryGreen = Color(0XFF00bc00) val primaryCharcoal = Color(0xFF2b2b2b) val accentAmber = Color(0xFFffe400) val textColorLight = Color(0xDCFFFFFF) val textColorDark = Color(0xFFf3f3f3) val gridLineColorLight = Color.Black //...ใช้ค่า predefined เช่น Color.Black หรือ ARGB Hex ของตัวเอง
ARGB Hex คืออะไร? "0XFF00bc00" แปลว่า:
0xบอก compiler ว่าเป็นเลขฐานสิบหกFFหรือDCแทน Transparency/Alpha ใน Hex- คู่ตัวอักษร 6 ตัวที่เหลือแทน Red, Green, Blue
เพิ่ม Fonts และแทนที่ attribute fontFamily
Typography (fonts) จัดการง่าย Kotlin default arguments มีประโยชน์
สร้างไฟล์ Type.kt สร้าง class Typography:
val typography = Typography( body1 = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp ), button = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 32.sp ), caption = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 12.sp ) ) //...และ TextStyle classes:
//... val mainTitle = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Light, fontSize = 48.sp, textAlign = TextAlign.Center ) fun dropdownText(color: Color) = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 32.sp, textAlign = TextAlign.Center, color = color ) //...ให้ public functions หรือ values (ไม่แนะนำ var) ขึ้นอยู่กับ preference และ requirements
สร้าง App Theme ใน Jetpack Compose
สิ่งสุดท้ายก่อนใช้ theme ใน composables คือ MaterialTheme @Composable ผมมีพร้อม light/dark color palettes ในไฟล์ GraphSudokuTheme:
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.MaterialTheme import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color private val LightColorPalette = lightColors( primary = primaryGreen, secondary = textColorLight, surface = lightGrey, primaryVariant = gridLineColorLight, onPrimary = accentAmber, onSurface = accentAmber ) private val DarkColorPalette = darkColors( // main background color primary = primaryCharcoal, // used for text color secondary = textColorDark, // background of sudoku board surface = lightGreyAlpha, // grid lines of sudoku board primaryVariant = gridLineColorLight, onPrimary = accentAmber, onSurface = accentAmber ) @Composable fun GraphSudokuTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { MaterialTheme( colors = if (darkTheme) DarkColorPalette else LightColorPalette, typography = typography, shapes = shapes, content = content ) } เนื่องจากคุณคุ้นเคย composable แล้ว (เตือนไว้) สิ่งใหม่คือ darkTheme: Boolean = isSystemInDarkTheme()
isSystemInDarkTheme() สอบถามอุปกรณ์ Android ที่รองรับ เกี่ยวกับความชอบ light/dark theme ของผู้ใช้
คืนค่า boolean ใช้ใน Ternary Assignment เช่น colors = if (darkTheme) DarkColorPalette else LightColorPalette
แค่นี้ก็จบ Colors, Fonts, และสอง Themes ในไม่กี่นาที
ใช้ Theme ใน Compose
ถึงเวลาใช้ Theme ในแอป ในแอปนี้มี 2 หน้าจอหลัก ใช้ Activity เป็น container สำหรับ composables:
class NewGameActivity : AppCompatActivity(), NewGameContainer { //... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //... setContent { GraphSudokuTheme { NewGameScreen( onEventHandler = logic::onEvent, viewModel ) } } //... }ที่เรียก setContent {} แนะนำให้วาง Theme composable ทันที ทำให้ style information cascade/inherit ไปยัง composables 巢ที่ซ้อนกัน
เกือบเสร็จแล้ว!
Override Styles และ Themes
พยายามใส่สีที่ต้องการใน light/dark palettes ตอนเรียก MaterialTheme.colors.<Color> ระบบจะจัดการ conditional logic เลือก palette ให้อัตโนมัติ:
@Composable fun NewGameContent( onEventHandler: (NewGameEvent) -> Unit, viewModel: NewGameViewModel ) { Surface( Modifier .wrapContentHeight() .fillMaxWidth() ) { ConstraintLayout(Modifier.background(MaterialTheme.colors.primary)) { //... } //... } } แต่บางครั้งเขียน conditional logic เองดีกว่า... หรือแค่ขี้เกียจ Compose ให้ configurations มากมายเป็น properties:
@Composable fun DoneIcon(onEventHandler: (NewGameEvent) -> Unit) { Icon( imageVector = Icons.Filled.Done, tint = if (MaterialTheme.colors.isLight) textColorLight else textColorDark, contentDescription = null, modifier = Modifier .clickable( //... ) ) }MaterialTheme.Colors.isLight คืนค่า boolean บ่งบอก mode ใช้ Ternary Assignment อีกครั้ง
จัดสไตล์ Text Composable
ตั้งค่า argument style ให้เท่ากับ text style ของคุณ (จาก MaterialTheme หรือ Type.kt):
Text( text = stat.toTime(), style = statsLabel.copy( color = if (isZero) Color.White else MaterialTheme.colors.onPrimary, fontWeight = FontWeight.Normal ), modifier = Modifier .wrapContentSize() .padding(end = 2.dp, bottom = 4.dp), textAlign = TextAlign.End )TextStyle มี copy function พร้อมใช้ หากต้องการ override
เสร็จแล้ว! ตอนนี้คุณรู้จักจัดสไตล์และทำ theme แอปด้วย Jetpack Compose ขอบคุณที่อ่าน :)
Social
หาผมได้ที่ Instagram และ Twitter
บทช่วยสอนและคอร์สของผม
https://youtube.com/wiseass https://www.freecodecamp.org/news/author/ryan-michael-kay/ https://skl.sh/35IdKsj (introduction to Android with Android Studio)