Bài viết học cách styling và theme ứng dụng trong Jetpack Compose.
Compose là UI framework mới cho Android (hỗ trợ Desktop/Web đang phát triển), thay thế hệ thống View dựa trên XML cũ.
Dù vẫn beta tại thời điểm viết, không mong đợi phần này thay đổi nhiều cho stable release.
Chủ đề:
- Tóm tắt nhanh cách tiếp cận XML cũ
- Cách migrate từ hệ thống colors, themes, typography (font) dựa trên XML
- Cách setup light/dark theme chỉ vài dòng code
- Cách dùng style Kotlin-based trong composables
- Cách styling Text Composables cụ thể
Trước khi tiếp tục, quan trọng hiểu composable là gì. Không giải thích khái niệm ở đây, đã làm trong bài khác.
Cách Chúng Ta Trước Kia Style Android App Dùng XML Resources
Như thường lệ, chia sẻ động lực và lịch sử. Nếu không quan tâm, bỏ qua sang phần thực hành.
Android Resources
Hệ thống resource Android app là thứ team Android xứng đáng được khen, theo mình. Nhưng như mọi quyết định thiết kế, feature trong tình huống này thành flaw trong tình huống khác.
Cụ thể, một thách thức lớn cho cả platform và app developer là tạo localized resources. Chỉ việc build app:
- Hiển thị text/graphics nhiều ngôn ngữ, bảng chữ cái khác nhau
- Look/feel tỷ lệ với nhiều form factor (dimensions, densities, v.v.)
Chỉ 2 ví dụ phổ biến - còn nhiều hơn. Resource system cho nơi cung cấp localized resources mà platform chọn tại compile time. Tiết kiệm viết boilerplate code.
Feature Hay Flaw?
Mặc dù không muốn quản lý boilerplate localized string resources, không có nghĩa là thích viết XML.
Thực tế, rất ít việc mình thích làm trong XML hơn Kotlin/Swift hiện đại, idiomatic, elegant. Preference riêng, có lý do kỹ thuật hơn tại sao XML resources không luôn lý tưởng.
Không phải критика platform developers. Chỉ quan sát design decisions luôn có lợi ích và chi phí.
Để tích hợp XML-based resources vào JVM-based app code, bắt buộc có layers of translation (compilation) và platform bridges (APIs). Gây khó khăn cho cả platform và app developers.
2 vấn đề thường gặp:
- Muốn truy cập resource nơi không muốn tight coupling với platform APIs cung cấp resource
- Viết boilerplate code điên rồ chỉ để thay look của View (override resource styles/themes)
Root problem cho mọi người là tight coupling với View system và Android resource system (chúng tự tight coupling với nhau).
Với platform developers: phải build trên hoặc workaround codebase khổng lồ, cũ. Thêm yêu cầu features mới chạy trên Android OS cũ, công việc vô ơn.
Kết quả cho app developers: nhiều boilerplate code, hacky workarounds cho việc 직 quan nên one-liners. API chính lấy resource là Context - class không muốn leak memory.
Jetpack Compose xuất hiện.
Thiết Lập Themes, Colors, Fonts Với Jetpack Compose
Sau review hệ thống cũ, khám phá cách đẹp và đơn giản hơn style/theme Android app. Đã nói giữ thực hành, nhưng cho phép một điểm quan trọng.
Vì làm trong Kotlin, nghĩa là một điều quan trọng: cả chúng ta và platform developers ít bị ràng buộc bởi translation (compilation) và API bridges (Android R class và Context) giữa XML và JVM.
Nói đơn giản: ít boilerplate code hơn, kiểm soát runtime nhiều hơn.
Phần thực hành, gợi ý follow process theo thứ tự giải thích. Cấu trúc theo quy trình mình dùng khi viết code cho app mới.
Thay Thế Colors.xml Resources Bằng Kotlin Compose
Chưa quyết định color scheme, dùng resources trang Material Design chính thức. Thử:
- Color palettes
- Color tool
Nếu plan support light/dark theme (giải thích sau), chọn color scheme hỗ trợ text trắng và scheme hỗ trợ text đen.

Tạo file Color.kt (tên không quan trọng), điền 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 //...Dùng predefined value như Color.Black hoặc tự cung cấp ARGB Hex.
Vì ARGB Hex là jargon mô tả "0XFF00bc00", dịch nghĩa:
- 2 ký tự đầu
0xbáo compiler đây là số hexadecimal - 2 ký tự tiếp "
FF" hoặc "DC" đại diện Transparency/Opaqueness/Alpha Hex - 6 cặp ký tự còn lại đại diện Red, Green, Blue
Thêm Fonts Và Thay Thuộc Tính fontFamily
Typography (fonts) cũng dễ quản lý. Default arguments của Kotlin rất hữu ích.
Tạo file Type.kt (tên vẫn không quan trọng), tạo 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 ) ) //......và một số class TextStyle:
//... 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 ) //...Cung cấp public functions hay values (khuyên không dùng var ở đây) tùy preference và yêu cầu.
Tạo App Theme Trong Jetpack Compose
Điều cuối cùng cấu hình trước khi dùng theme trong composables là MaterialTheme @Composable. Mình để light/dark color palettes trong file 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 ) } Vì đã quen composable (đã cảnh báo), điều mới chỉ có darkTheme: Boolean = isSystemInDarkTheme().
Giản lược, isSystemInDarkTheme() hỏi thiết bị Android tương thích preference light/dark của user.
Trả về boolean dùng trong Ternary Assignment như colors = if (darkTheme) DarkColorPalette else LightColorPalette.
Đó là tất cả. Colors, Fonts, 2 Themes định nghĩa trong vài phút.
Cách Dùng Theme Trong Compose
Giờ dùng Theme trong app. App này chỉ 2 màn hình chính, dùng Activity làm container cho composables:
class NewGameActivity : AppCompatActivity(), NewGameContainer { //... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //... setContent { GraphSudokuTheme { NewGameScreen( onEventHandler = logic::onEvent, viewModel ) } } //... }Ở đâu gọi setContent {}, gợi ý beginner: ngay lập tức đặt Theme composable bên trong. Sẽ khiến style information cascade/inherit cho mỗi composable lồng nhau.
Xong! Gần như.
Cách Override Styles Và Themes
Nếu có thể, hãy include mọi color muốn dùng trong light/dark palettes. Khi gọi MaterialTheme.colors.<Color>, system xử lý conditional logic chọn palette phù hợp:
@Composable fun NewGameContent( onEventHandler: (NewGameEvent) -> Unit, viewModel: NewGameViewModel ) { Surface( Modifier .wrapContentHeight() .fillMaxWidth() ) { ConstraintLayout(Modifier.background(MaterialTheme.colors.primary)) { //... } //... } } Tuy nhiên, đôi khi viết conditional logic riêng... hoặc lười. May mắn Compose cung cấp nhiều config as 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 trả về boolean cho biết mode, dùng Ternary Assignment từ đó.
Cách Style Text Composable
Chỉ set argument style bằng một text style (từ MaterialTheme hoặc style trong 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 có sẵn function copy nếu cần overwrite.
Xong! Bạn đã biết cách style/theme app dùng Jetpack Compose. Cảm ơn đã đọc :)
Social
Tìm mình trên Instagram và Twitter.
Tutorials & Courses
https://youtube.com/wiseass https://www.freecodecamp.org/news/author/ryan-michael-kay/ https://skl.sh/35IdKsj (introduction to Android with Android Studio)