Tutorial dạy vài khái niệm và thuật ngữ cơ bản liên quan Jetpack Compose UI Library trên Android.
Dù là beginner's guide Compose, không phải beginner's guide Android - nên đã build ít nhất 1-2 app (dù không nhất thiết Compose).
Trước khi bắt đầu, ban đầu plan viết follow-up cho senior devs cho đến khi đọc series 2 phần của Leland Richardson. Leland không chỉ Software Engineer team Jetpack Compose, mà còn writer giỏi.
Mặc dù bài này đứng riêng như intro Compose basics, mạnh mẽ khuyên đọc bài Leland sau khi có kinh nghiệm thực tế Compose (hoặc ngay nếu thích học cách đó).
Key Terms/Concepts Giải Thích Trong Bài:
- Review ngắn hệ thống View và Hierarchy cũ
- Composables và mối quan hệ với Views
- Recomposition và cách tránh làm nó rất tệ!
Composable Là Gì?
Phần này thảo luận phần cơ bản nhất của Jetpack Compose. Nếu là Android developer giàu kinh nghiệm, có thể skip đến sub-section "Are Composables Views?"
Nếu chưa quen View system, đọc phần sau cần thiết để hiểu Composable.
View Hierarchy
Trong bối cảnh Android SDK (thư viện làm UI trên platform), View là gì dùng để cấu trúc và style app.
Đây là building block/element cơ bản nhất của UI, mỗi block chứa các loại info (ngoài những thứ khác):
- Vị trí X, Y start/end cho máy biết vẽ view ở đâu trên màn hình
- Color và alpha (transparency) values
- Font info, text, symbols, images
- Behaviour dựa trên events: user interaction (clicks) hoặc thay đổi data app (chi tiết sau)
Quan trọng hiểu: View có thể là button (thường gọi "widget"), nhưng cũng có thể là container toàn màn hình, phần màn hình, hoặc cho child Views khác.
Container này thường gọi Layouts hoặc Viewgroups tùy context. Chúng share hầu hết info như widget, nhưng cũng chứa info cách arrange/display Views nested bên trong.
Với đó, đến phần quan trọng: View Hierarchy. Với Web Developers, View Hierarchy về cơ bản là version Android của Document Object Model (DOM).
Với Android Developers, View Hierarchy là virtual representation của tất cả Views định nghĩa trong XML hoặc programmatically Java/Kotlin.
Minh họa: xem XML file (không cần nghiên cứu kỹ, chỉ note names). Dùng debugger/stepper tool xem memory space của Fragment inflate file này:
fragment_hour_view.xml:
<?xml version=”1.0" encoding=”utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android=”https://schemas.android.com/apk/res/android" android:layout_width=”match_parent” android:layout_height=”match_parent” android:id=”@+id/root_hour_view_fragment” xmlns:app=”https://schemas.android.com/apk/res-auto" > <androidx.compose.ui.platform.ComposeView android:id=”@+id/tlb_hour_view” //... /> <com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleView android:id=”@+id/vqht_one” //... /> <com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleView android:id=”@+id/vqht_two” //... /> <com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleView android:id=”@+id/vqht_three” //... /> <com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleView android:id=”@+id/vqht_four” //... /> </androidx.constraintlayout.widget.ConstraintLayout>Memory Space của (Fragment)HourView.kt:

Debugger và stepper tools là cách yêu thích học về under the hood của code từ các thư viện. Thử đôi khi!
Mục đích show XML file và runtime View Hierarchy: demo nested Views trong XML translate thành nested View Hierarchy.
Hy vọng với model đơn giản nhưng concrete hệ thống cũ, có thể so sánh hệ thống mới.
Composables Có Phải Views?
Câu hỏi đầu tiên khi làm Compose, câu trả lời: có và không.
Có, vì Composable fulfill cùng conceptual role như View hệ thống cũ. Composable có thể là widget như button, hoặc container như ConstraintLayout (có Composable implementation của ConstraintLayout).
Không, vì UI không còn represented virtually trong View Hierarchy (trừ interoperability). Có nói vậy, Compose không dùng magic để virtually represent và track UI. Nghĩa là phải có thing riêng conceptually giống View Hierarchy.
Xem nhanh thing này. Activity dùng setContent {…} bind Composable:
ActiveGameActivity.kt:
class ActiveGameActivity : AppCompatActivity(), ActiveGameContainer { private lateinit var logic: ActiveGameLogic override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val viewModel = ActiveGameViewModel() setContent { ActiveGameScreen( onEventHandler = { logic.onEvent(it) }, viewModel ) } logic = buildActiveGameLogic(this, viewModel, applicationContext) } //… }ActiveGameScreen.kt:
@Composable fun ActiveGameScreen( onEventHandler: ((ActiveGameEvent) -> Unit), viewModel: ActiveGameViewModel ) { //... GraphSudokuTheme { Column( Modifier .background(MaterialTheme.colors.primary) .fillMaxHeight() ) { ActiveGameToolbar( clickHandler = { onEventHandler.invoke( ActiveGameEvent.OnNewGameClicked ) } ) Box { //content } } } }Trong Compose, View Hierarchy được thay bằng thing ta locate nếu dig deep vào field mWindow của Activity. Trong field đó là conceptual replacement: The Composer và its slotTable.

Nếu muốn overview chi tiết Composer và slotTable, lại khuyên đọc bài Leland (phần 2 đi sâu). Compose Hierarchy còn nhiều hơn Composer và slotTable, nhưng đủ để bắt đầu.
Nói chung, Jetpack Compose dùng Compose Hierarchy (làm từ, managed bởi Composer và slotTable).
Lại là cùng conceptual idea như View hierarchy - bunch of objects in memory space collectively represent UI - nhưng implement rất khác.
Có different quan trọng, tricky technically nhưng dễ hiểu nguyên tắc. Cách Compose handle updates Compose Hierarchy: Recomposition.
Recomposition: Cách Update Compose UI
Cho bạn ESL, Compose từ latin componere, nghĩa xấp xỉ "put together". Người viết nhạc gọi "Composer", có thể nghĩ là người put together notes từ instruments thành composition (bài hát).
Put together ngụ ý có individual pieces. Quan trọng hiểu: hầu hết good software developer làm effort chia code thành phần nhỏ nhất hợp lý.
Nhắc hợp lý, vì nghĩ principles như DRY (Don't Repeat Yourself) nên follow chỉ đến mức solve nhiều problems hơn tạo ra.
Nhiều benefits áp dụng concept này, thường gọi modularity (hoặc như mình thích, Separation of Concerns - SOC). Biết một số bạn nghĩ mình copy Leland, nhưng mình nói SOC là Golden Principle Of Software Architecture nhiều năm rồi.
Nơi SOC chơi vào Compose: cùng principle với React JS library. Khi done properly, Compose chỉ "recompose" (redraw, re-render, update) Composables (parts/elements UI) cần recompose.
ENORMOUSLY quan trọng cho performance app. Vì redraw UI, dù hệ thống View cũ hay Compose, tốn system resources.
Nếu không biết, mục đích RecyclerView cũ (điều đầu tiên mình làm tutorial 2016!) là employ ViewHolder pattern cho list data. Tránh inflate (tạo) Views mới cho mỗi list item.
Mục tiêu bài này focus theory, sẽ viết practical content tháng sau. Kết thúc với story từ trải nghiệm trực tiếp, giúp hiểu recomposition và cách tránh làm nó rất tệ!
Ví Dụ Stopwatch
App Compose đầu tiên full, quyết định build Sudoku. Lý do: project không UI phức tạp, muốn deep dive Graph DS/Algos, phù hợp Sudoku puzzles.
Muốn Stopwatch track thời gian user hoàn thành puzzle:

Như thường lệ nghề nghiệp, dự đoán timer dễ thêm hơn thực tế. Xài Android Chronometer class và Java Timer class, cả hai đều gây vấn đề break app.
Lùi lại, nhận ra đang viết Kotlin. Setup Coroutine-based timer trong presentation logic class (có lý nhất đặt ở đó), update viewmodel mỗi giây:
Class ActiveGameLogic(…):…{ //… inline fun startCoroutineTimer( delayMillis: Long = 0, repeatMillis: Long = 1000, crossinline action: () -> Unit ) = launch { delay(delayMillis) if (repeatMillis > 0) { while (true) { action() delay(repeatMillis) } } else { action() } } private fun onStart() = launch { gameRepo.getCurrentGame( { puzzle, isComplete -> viewModel.initializeBoardState( puzzle, isComplete ) if (!isComplete) timerTracker = startCoroutineTimer { viewModel.updateTimerState() } },{ container?.onNewGameClick() }) } //… }ViewModel (không phải AAC - tự viết VMs. Nhưng Compose đã có interoperability tốt với AAC VMs) expose references đến callback functions, dùng để update Composables:
class ActiveGameViewModel { //… internal var subTimerState: ((Long) -> Unit)? = null internal var timerState: Long = 0L //… internal fun updateTimerState(){ timerState++ subTimerState?.invoke(timerState) } //… }Đây là phần quan trọng! Trigger recomposition Compose Hierarchy dùng features như remember function:
var timerState by remember { mutableStateOf(“”) }Nếu phải biết, features này store state của whatever bạn remember trong slotTable. Tóm lại, state ở đây nghĩa "state" hiện tại của data, bắt đầu là empty String.
Đây là chỗ mình làm sai. Đã tách timer composable đơn giản thành function riêng (applied SOC), truyền timerState as parameter cho composable đó.
Tuy nhiên, snippet trên ngồi trong parent composable của timer, là container cho phần UI phức tạp nhất (Sudoku 9x9 cần nhiều widgets):
@Composable fun GameContent( onEventHandler: (ActiveGameEvent) -> Unit, viewModel: ActiveGameViewModel ) { Surface( Modifier .wrapContentHeight() .fillMaxWidth() ) { BoxWithConstraints(Modifier.background(MaterialTheme.colors.primary)) { //… ConstraintLayout { val (board, timer, diff, inputs) = createRefs() var isComplete by remember { mutableStateOf(false) } var timerState by remember { mutableStateOf("") } viewModel.subTimerState = { timerState = it.toTime() } viewModel.subIsCompleteState = { isComplete = it } //…Sudoku board //Timer Box(Modifier .wrapContentSize() .constrainAs(timer) { top.linkTo(board.bottom) start.linkTo(parent.start) } .padding(start = 16.dp)) { TimerText(timerState) } //…difficulty display //…Input buttons } } } } @Composable fun TimerText(timerState: String) { Text( text = timerState, style = activeGameSubtitle.copy(color = MaterialTheme.colors.secondary) ) }Gây lag và unresponsive đáng kể. Dùng debugger heavily, tìm ra tại sao. Vì timerState variable tạo và update trong parent Composable, trigger recomposition toàn bộ portion UI đó. Mỗi. Tick. Đơn. Lẻ.
Sau khi move code thích hợp vào TimerText composable, mọi thứ smooth:
@Composable fun TimerText(viewModel: ActiveGameViewModel) { var timerState by remember { mutableStateOf("") } viewModel.subTimerState = { timerState = it.toTime() } Text( text = timerState, style = activeGameSubtitle.copy(color = MaterialTheme.colors.secondary) ) }Hy vọng đã cho working understanding recomposition và một cách lớn làm nó sai.
Tránh unnecessary recompositions cực quan trọng cho performance. Cho tới nay, apply SOC rigorously, thậm chí keep remember state trong separate composables, nên thành standard practice.
Resources & Support
Nếu thích bài, share social media và check bài khác trên freeCodeCamp. Có YouTube channel hàng trăm tutorials, active writer nhiều platform.
Connect Social Media
Tìm mình trên Instagram và Twitter.
Cũng chỉ ra single resource dùng bắt đầu Jetpack Compose: Working code samples từ good developers.