Computer >> Hướng Dẫn Máy Tính >  >> Hệ Thống >> Android

Xây dựng ứng dụng Android với Kotlin & Jetpack Compose: Tạo app Sudoku

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ó property value: Double cho 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ùng Long.
  • 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). Override hashCode() = x*100 + y để unique. Implement Serializable để ghi file.
  • SudokuPuzzle (data class): boundary, difficulty, graph: LinkedHashMap<Int, LinkedList<SudokuNode>> (adjacency list), elapsedTime. Factory buildNewSudoku() gọi chuỗi thuật toán.
  • Interfaces repository: IGameRepository, IGameDataStorage, ISettingsStorage, IStatisticsRepository. Dùng suspend functions + 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 (function onEvent(event: EVENT), Job cho 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ấp Dispatchers.Main (UI) và Dispatchers.IO cho production; test thì dùng Dispatchers.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: message GameSettings (boundary: int32, difficulty: enum).
  • user_statistics.proto: message UserStatistics (các field int64 cho thời gian ms).
  • DataStores.kt: Tạo 2 DataStore<T> với serializer tự sinh từ proto.
  • LocalGameStorageImpl: CRUD file dùng ObjectInput/OutputStream. Helper updateGameData(), 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: Object Color với giá trị hex ARGB (0xAARRGGBB). Thay thế colors.xml.
  • Shape.kt: RoundedCornerShape dùng .dp extension (density-independent pixels).
  • Type.kt: TextStyle + Typography object. Thay thế styles.xml.
  • GraphSudokuTheme.kt: Composable function nhận darkTheme = isSystemInDarkTheme(), content. Tạo 2 palette LightColors/DarkColors, wrap MaterialTheme. Demo: đổi system theme → app đổi ngay lập tức.

UI Components (Reusable)

  • AppToolbar: Composable nhận Modifier (parent quyết định size/position), title, icon: @Composable () -> Unit (parent truyền icon + click handler). Dùng TopAppBar, MaterialTheme.colors.primary tự thích ứng light/dark. Action icon dùng RowScope.
  • LoadingScreen: Surface (fill 80% height), Column center: logo (painterResource), CircularProgressIndicator, text. Dùng copy() để 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ùng inline + crossinline lambda, 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). Implement ActiveGameContainer. 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) } + mutableTransitionState cho 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.
  • BoxWithConstraints lấy max width → tính size ô vuông, margin theo density.
  • ConstraintLayout (Compose): Tạo reference (createRefs()), constrain composable qua constrainAs(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). remember với neverEqualPolicy() để 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!