Jetpack Compose là toolkit UI hiện đại của Android để xây dựng giao diện native. Nó đơn giản hóa và đẩy nhanh phát triển UI trên Android.
Kênh YouTube freeCodeCamp.org vừa phát hành khóa học tạo ứng dụng Sudoku Android với Kotlin và Jetpack Compose. Trong quá trình đó, bạn cũng sẽ học về cấu trúc dữ liệu và thuật toán đồ thị (Graph).
Khóa học do Ryan M. Kay giảng dạy – developer và instructor giàu kinh nghiệm.
Các phần trong khóa học (3.5 giờ)
- App Design Approach: Minimalism thư viện thứ 3 & Kiến trúc MV-Whatever
- Domain package: Repository Pattern, Enum, Data Class, Sealed Class, Hash Code, Interfaces
- Common package: Extension Functions/Variables, Nguyên tắc Open-Closed (OCP), Abstract Class, Singleton
- Persistence package: Clean Architecture Backend với Java File System Storage, Jetpack Proto DataStore
- UI package: Jetpack Compose UI Basics, Styles, Typography, Light & Dark Themes
- UI Components package: Modifiers, Reusable Toolbar & Loading Screens
- UI Active Game Feature: Presentation Logic & ViewModel với Coroutines, Kotlin Function Types
- UI Active Game Feature: Sudoku Game với Jetpack Compose UI & Activity Container
- Computation Logic: Tổng quan, thiết kế, test Graph DS & Algorithms cho Sudoku n×n vuông
Xem khóa học đầy đủ trên YouTube freeCodeCamp.org.
Tổng quan kiến trúc & Quyết định thiết kế
Ít thư viện thứ 3, chống deprecation
Ngoài Compose và Proto DataStore, hầu như không dùng thư viện Jetpack khác (ViewModel, Navigation, Hilt...). Thay vào đó dùng Kotlin/JDK stdlib và Android SDK – ít thay đổi hơn, code bền vững hơn. Một số việc library thường lo (DI, navigation, viewmodel) tự viết cho app nhỏ này – dễ hơn bạn nghĩ.
Model-View-Whatever (MV-Whatever)
Không tuân theo MVVM/MVP chuẩn. Tự thiết kế dựa trên nguyên tắc Separation of Concerns. Compose phù hợp MVVM nhưng thêm Presentation Logic class (Passive View / Humble Object) để tách control flow ra khỏi ViewModel – dễ test, không phụ thuộc Android, tránh ViewModel thành "God object".
Domain Package: Foundation của ứng dụng
Repository Pattern (Facade Pattern)
Interface ẩn chi tiết subsystem (lưu trữ) sau abstraction. Presentation logic giữ reference đến interface, không phải implementation → loose coupling. Lợi ích: build/test độc lập, swap implementation (file ↔ Room DB) không sửa presentation logic.
Các model & interface chính
Difficulty(enum): EASY, MEDIUM, HARD, EXPERT – có propertyvalue: Doublecho thuật toán.Settings(data class):difficulty,boundary(kích thước: 4 hoặc 9).UserStatistics(data class): Thời gian tốt nhất (ms) cho từng cặp difficulty+boundary – dùngLong.SudokuNode(data class): Node trong directed colored graph.x, y(0-based),color: Int(giá trị 1-9/4, 0=ô trống),readOnly: Boolean(clue không cho sửa). OverridehashCode()=x*100 + yđể unique. ImplementSerializableđể ghi file.SudokuPuzzle(data class):boundary,difficulty,graph: LinkedHashMap<Int, LinkedList<SudokuNode>>(adjacency list),elapsedTime. FactorybuildNewSudoku()gọi chuỗi thuật toán.- Interfaces repository:
IGameRepository,IGameDataStorage,ISettingsStorage,IStatisticsRepository. Dùngsuspendfunctions + function types callback (onSuccess/onError) cho coroutine. - Result wrapper:
sealed class Result<T> { data class Success<T>(...) : Result<T>(); data class Error(...) : Result<T>() }– thay vì callback đôi, trả về 1 object biểu diễn nhiều state.
Common Package: Code dùng chung & Kotlin Features
BaseLogic<EVENT>(abstract class): Chia sẻ behavior (functiononEvent(event: EVENT),Jobcho coroutine scope). Dùng generic type. Abstract class cho phép protected variable – interface không được.extensions.kt: Extension functions (VD:Activity.makeToast(msg),Long.toTimeString()), extension property (string resources). Áp dụng OCP: mở rộng không sửa code gốc.DispatcherProvider(interface) &ProductionDispatcherProvider(object Singleton): Cung cấpDispatchers.Main(UI) vàDispatchers.IOcho production; test thì dùngDispatchers.Unconfined. Dễ test, thread-safe.
Persistence Package: Lưu trữ bền vững
Lưu: tiến độ game đang chơi, settings, statistics (records).
Kiến trúc
GameRepositoryImpl điều phối 2 data source:
- Proto DataStore (Protocol Buffers): Settings & Statistics. Nhẹ, hiệu quả, serialization giống JSON nhưng dễ đọc hơn. File
.protođịnh nghĩa message → compiler sinh Java class. - File Storage (Java I/O Streams): Game progress (SudokuPuzzle + SudokuNode implement
Serializable). Mỗi app có private directory.
Chi tiết triển khai
game_settings.proto: messageGameSettings(boundary: int32, difficulty: enum).user_statistics.proto: messageUserStatistics(các field int64 cho thời gian ms).DataStores.kt: Tạo 2DataStore<T>với serializer tự sinh từ proto.LocalGameStorageImpl: CRUD file dùngObjectInput/OutputStream. HelperupdateGameData(),getGame(). Ghi cả node đơn lẻ (updateNode) và cả puzzle.GameRepositoryImpl: Decision maker backend. Xử lý nhiều event stream: first run, active game, game complete, error cases. Tách logic backend phức tạp ra khỏi presentation logic.
UI Package: Jetpack Compose Theming & Components
Theme system (Light/Dark chỉ vài dòng)
Color.kt: ObjectColorvới giá trị hex ARGB (0xAARRGGBB). Thay thế colors.xml.Shape.kt: RoundedCornerShape dùng.dpextension (density-independent pixels).Type.kt: TextStyle + Typography object. Thay thế styles.xml.GraphSudokuTheme.kt: Composable function nhậndarkTheme = isSystemInDarkTheme(),content. Tạo 2 paletteLightColors/DarkColors, wrapMaterialTheme. Demo: đổi system theme → app đổi ngay lập tức.
UI Components (Reusable)
AppToolbar: Composable nhậnModifier(parent quyết định size/position),title,icon: @Composable () -> Unit(parent truyền icon + click handler). DùngTopAppBar,MaterialTheme.colors.primarytự thích ứng light/dark. Action icon dùngRowScope.LoadingScreen:Surface(fill 80% height),Columncenter: logo (painterResource),CircularProgressIndicator, text. Dùngcopy()để tweak style.
Active Game Feature: Presentation Logic & ViewModel
Kiến trúc feature
- ActiveGameContainer (interface): Entry point, wiring. 2 method:
showError(),onNewGameClick(). - ActiveGameEvent (sealed class): Mọi user interaction (InputClicked, NewGameClicked, Start, Stop, TileFocused...). Strategy Pattern: 1 entry point
onEvent(event)thay vì nhiều function. - ActiveGameViewModel: Không extends Jetpack ViewModel. Dùng Function Types làm "poor man's observer pattern": nullable function references (
subBoardState,subContentState,subTimerState...). ViewModel publish data → View observe qua invoke function types. Loose coupling, có thể dùng cho Kotlin Desktop/JS. - ActiveGameLogic (extends
BaseLogic<ActiveGameEvent>): Coordinator giữa Container, ViewModel, Repository. CoroutineScope riêng (Job), tự cancel coroutines.startCoroutineTimer()dùnginline+crossinlinelambda, delay 1s, cancel bằng Job.
Logic chính
onInputClicked: Nếu có tile focused → cập nhật giá trị, lưu backend, check puzzle complete → handle record/new game.onNewGameClicked: Lưu progress game cũ (nếu chưa complete), navigate NewGame.onStart: Load game từ repo. Nếu first run/error → navigate NewGame. Khởi động timer.onStop: Lưu progress, cancel coroutines.onTileFocused: Cập nhật focus state trên ViewModel.
Compose UI: Active Game Screen
Setup Activity & DI đơn giản
ActiveGameActivity(Empty Activity, no layout XML, launcher). ImplementActiveGameContainer.setContent { GraphSudokuTheme { ActiveGameScreen(onEvent = logic::onEvent, viewModel) } }.buildActiveGameLogic()(extension function trên Context): Manual DI – tạo File dir, DataStore, Repository, ViewModel, Logic. Không cần Hilt/Dagger cho app nhỏ.
ActiveGameScreen (Root Composable)
- Enum
ActiveGameScreenState: LOADING, ACTIVE_GAME, GAME_COMPLETE. - State hoisting:
remember { mutableStateOf(LOADING) }+mutableTransitionStatecho animation alpha. - Bind ViewModel function types:
viewModel.subContentState = { state -> contentState = state; transitionState.targetState = state }. - Animation:
AnimatedVisibility/transitionSpec(tween, alpha). - Scaffold tự build:
AppToolbar(icon new game), content area 3 state.
GameContent (Bàn cờ Sudoku 9×9 = 81 Text)
- Tránh "God Composable" – tách helper functions.
BoxWithConstraintslấy max width → tính size ô vuông, margin theo density.ConstraintLayout(Compose): Tạo reference (createRefs()), constrain composable quaconstrainAs(ref).- Render layers: Board container → Sudoku Board (grid lines modulo √boundary cho sub-grid 3×3) → Tile Text (readOnly vs mutable, focused style) → Difficulty icons → Timer → Input buttons (0-9, backspace, clear).
- Tile state từ
viewModel.boardState(Map) – key = hashCode(x,y). remembervớineverEqualPolicy()để re-compose khi focus thay đổi.
GameCompleteContent
2 image stack (emoji celebration), chỉ show record icon nếu là record mới. 2 Text: thời gian, "New Record!". Dùng ColorFilter đổi màu emoji.
Kết luận
Bạn đã xây dựng app Sudoku hoàn chỉnh với:
- Clean Architecture (Domain, Data, Presentation, UI tách biệt)
- Jetpack Compose hiện đại (Theme, Animation, ConstraintLayout, State hoisting)
- Kotlin advanced (Sealed class, Extension, Coroutines, Function Types, Serialization, Proto DataStore)
- Graph algorithms cho Sudoku n×n
Mở rộng: Chia bill, làm tròn số, undo/redo, hint, daily challenge, multiplayer... Hãy dùng project này làm khởi đầu cho hành trình Android developer!