md2link

Ôn phỏng vấn iKame — Android Middle

DraftMay 17, 2026

Ôn phỏng vấn iKame — Android Middle

Tập trung đúng JD iKame: Kotlin, Android Internals, Compose, Coroutines/Flow, MVVM/MVI, Clean Architecture, Modularization, Design Patterns + SOLID. Mỗi câu trả lời gọn, đủ dùng cho phỏng vấn 2-3 năm kinh nghiệm.


Mục lục

  1. Kotlin cốt lõi
  2. OOP & SOLID
  3. Android Internals
  4. Coroutines & Flow
  5. Jetpack Compose
  6. Architecture (MVVM / MVI / Clean)
  7. Modularization
  8. Design Patterns trong Android
  9. Điểm cộng: KMP, DSA cơ bản

1. Kotlin cốt lõi

Q1. val vs var vs const val?

  • val: tham chiếu bất biến — không gán lại được, nhưng nếu object bên trong là mutable thì vẫn thay đổi nội dung được (ví dụ val list = mutableListOf() vẫn .add() bình thường).
  • var: tham chiếu khả biến — gán lại thoải mái bất cứ lúc nào.
  • const val: hằng số tại thời điểm compile. Chỉ dùng với primitive hoặc String, phải đặt ở top-level hoặc trong companion object. Giá trị được inline trực tiếp vào bytecode nên không tạo field thật sự.
val list = mutableListOf(1, 2)
list.add(3)               // OK — nội dung object thay đổi được
// list = mutableListOf() // LỖI — không gán lại reference được

const val API_URL = "https://api.com"  // inline vào bytecode, không tạo field

Q2. Null safety — ?. / ?: / !! / let?

Kotlin thiết kế hệ thống kiểu phân biệt rõ nullable (String?) và non-null (String) ngay từ compile-time, giúp loại bỏ phần lớn NullPointerException.

Toán tử Ý nghĩa Ví dụ
?. Safe call — trả null nếu receiver null user?.name
?: Elvis — giá trị mặc định khi null name ?: "Unknown"
!! Force unwrap — ném NPE nếu null name!!.length
let Chạy block khi non-null name?.let { print(it) }

Bẫy: !! gần như không nên dùng trong production code. Nếu thấy !! xuất hiện nhiều, đó là dấu hiệu thiết kế sai — nên dùng ?., ?:, hoặc redesign cho non-null.

Q3. data class khác class thường thế nào?

data class yêu cầu ít nhất 1 property trong primary constructor, và compiler sẽ tự sinh:

  • equals() / hashCode() — so sánh theo giá trị tất cả property trong primary constructor.
  • toString() — in dạng User(id=1, name="An").
  • componentN() — cho destructuring: val (id, name) = user.
  • copy() — tạo bản sao, có thể đổi 1 vài field: user.copy(name = "Bình").

Bẫy quan trọng: chỉ property trong primary constructor mới được tính vào equals/hashCode. Property khai báo trong body bị bỏ qua hoàn toàn:

data class User(val id: Int) {
    var name: String = ""   // KHÔNG tham gia equals/hashCode
}
User(1).apply { name = "A" } == User(1).apply { name = "B" }  // true!

→ Mọi property dùng để định danh object phải đặt trong primary constructor.

Q4. sealed class / sealed interface — khi nào dùng?

sealed giới hạn tập subtype được phép tồn tại — tất cả subtype phải nằm cùng package. Compiler biết chính xác có bao nhiêu subtype → when exhaustive (không cần else). Nếu sau này thêm subtype mới, mọi when trong project sẽ compile fail → buộc phải cập nhật, an toàn hơn else -> {} (âm thầm bỏ qua case mới).

sealed class Result
class Success(val data: List<User>) : Result()
class Error(val message: String) : Result()
object Loading : Result()

when (result) {
    is Success -> showData(result.data)
    is Error   -> showError(result.message)
    is Loading -> showLoading()
    // không cần else — compiler biết đủ 3 case
}

sealed class vs sealed interface:

  • sealed class: subtype chỉ kế thừa được 1 class (Kotlin không cho đa kế thừa class). Dùng khi cần chia sẻ state hoặc constructor chung giữa các subtype.
  • sealed interface: subtype có thể implement nhiều sealed interface cùng lúc → linh hoạt hơn. Đây là lựa chọn mặc định.
sealed interface Animal
sealed interface Pet
class Dog : Animal, Pet   // OK — 1 type thuộc nhiều sealed hierarchy cùng lúc

Khác enum: enum yêu cầu mọi value cùng khuôn (cùng property), sealed cho phép mỗi subtype có schema riêng biệt — Success mang data, Error mang message, Loading không cần gì.

Khác abstract class: abstract class mở — ai cũng extend được, kể cả module khác → when không exhaustive, vẫn phải có else.

Use case thực tế: UI state (Loading/Success/Error), Result wrapper, Navigation event, MVI Intent/Action.

Q5. Scope functions: let, run, with, apply, also

Kotlin cung cấp 5 scope function, phân biệt theo 2 chiều:

  1. Truy cập object thế nào: it (phải viết rõ it.name) hay this (gọi trực tiếp name như đang ở trong class).
  2. Trả về gì: kết quả dòng cuối (block result) hay chính object gốc (object itself).
Function Truy cập object Trả về Use case
let it kết quả block Null check + transform
run this kết quả block Cấu hình + tính giá trị
with this kết quả block Gọi nhiều method trên cùng object
apply this chính object Builder-style config
also it chính object Side effect (log, validate)

Khi nào chọn it? Khi cần đổi tên cho dễ đọc (user?.let { u -> ... }), hoặc khi nhiều scope lồng nhau — this ngoài bị che bởi this trong, dùng it tránh nhầm lẫn.

Khi nào chọn this? Khi gọi nhiều method/property trên cùng 1 object → bớt lặp it.xxx.

Trả về object itself dùng để chain tiếp (config xong gán vào biến). Trả về block result dùng để transform sang kiểu khác.

Tình huống Chọn
Null check rồi transform ?.let { }
Config object lúc khởi tạo apply { }
Log/validate giữa chain also { }
Tính ra value mới từ object run { }
Gọi nhiều method trên object có sẵn with(obj) { }

Q6. inline function — tại sao cần?

Bình thường mỗi lambda compile thành 1 anonymous class + 1 object instance → tốn allocation trên heap, đặc biệt trong vòng lặp hoặc hàm gọi thường xuyên.

inline bảo compiler copy toàn bộ body của function + body của lambda thẳng vào call site → không tạo class/object nào cả. Tương đương viết code trực tiếp tại chỗ gọi.

Bắt buộc để dùng reified generics — giữ type info ở runtime (bình thường generic bị type erasure, không biết T là gì lúc runtime):

inline fun <reified T> Gson.fromJson(json: String): T =
    fromJson(json, T::class.java)  // T::class.java chỉ dùng được khi reified

Modifier bổ sung: noinline (1 lambda cụ thể không inline — vì cần truyền nó như object), crossinline (lambda không được phép non-local return — dùng khi lambda chạy trong context khác như coroutine).

Khi nào KHÔNG inline? Function body lớn, gọi ở nhiều nơi → mỗi call site copy cả body → code bloat, tăng kích thước APK. inline chỉ đáng cho function nhận lambda + ngắn gọn.

Q7. lateinit vs lazy?

lateinit by lazy
Kiểu var val
Primitive Không hỗ trợ
Thời điểm Bất kỳ lúc nào Lần đầu access
Thread-safe Không Mặc định có (SYNCHRONIZED)
  • lateinit: dùng khi biết chắc sẽ init trước khi dùng, nhưng không thể init ngay lúc khai báo. Ví dụ: DI inject, binding trong onCreate. Truy cập trước khi init → crash UninitializedPropertyAccessException. Có thể check bằng ::property.isInitialized.
  • lazy: dùng khi compute đắt và có thể không bao giờ dùng đến. Chỉ tính 1 lần, cache kết quả. Có 3 mode: SYNCHRONIZED (mặc định, thread-safe), PUBLICATION (nhiều thread tính, chỉ dùng kết quả đầu), NONE (không sync, nhanh nhất — chỉ dùng trong single-thread).

Q8. Generics — in, out, *?

Variance quyết định quan hệ kế thừa giữa generic types:

  • out T (covariance): chỉ sản xuất T (return), không tiêu thụ (không nhận T làm param). List<Cat> là subtype của List<Animal> → an toàn vì chỉ đọc ra Animal.
  • in T (contravariance): chỉ tiêu thụ T (nhận param), không sản xuất. Comparator<Animal> dùng được cho Comparator<Cat> → vì so sánh Animal thì so sánh Cat cũng OK.
  • * (star projection): không biết/không quan tâm type cụ thể. Chỉ đọc được dạng Any?, không ghi được.

Mẹo nhớ: PECS — Producer Extends (out), Consumer Super (in). Hoặc: out = output = đọc ra, in = input = ghi vào.

Q9. == vs ===?

  • ==: structural equality → gọi equals() bên dưới. So sánh giá trị/nội dung.
  • ===: referential equality → so sánh 2 biến có trỏ đến cùng 1 object trong bộ nhớ hay không.

Tương đương Java: == Kotlin = .equals() Java, === Kotlin = == Java. Kotlin đảo ngược ý nghĩa so với Java để cú pháp thông dụng nhất (==) làm điều hữu ích nhất (so sánh giá trị).

Q10. Extension function — bytecode thế nào?

Compile thành static method bình thường, receiver trở thành parameter đầu tiên. → Không có virtual dispatch, compiler resolve tĩnh theo declared type (kiểu khai báo), không phải runtime type.

open class Base
class Derived : Base()
fun Base.hello() = "Base"
fun Derived.hello() = "Derived"

val b: Base = Derived()
b.hello()  // "Base" — vì declared type là Base, dù runtime type là Derived

Hệ quả: extension function không thể override như member function. Nếu cần polymorphism → dùng member function hoặc interface.


2. OOP & SOLID

Q11. 4 tính chất OOP — ví dụ trong Android?

1. Encapsulation (Đóng gói): Giấu chi tiết bên trong, chỉ expose ra ngoài những gì cần thiết. State chỉ được sửa qua method/property có kiểm soát — không cho code ngoài tự ý ghi vào.

Ví dụ kinh điển trong Android: MutableStateFlow private + StateFlow public trong ViewModel. Fragment/Compose chỉ collect được StateFlow (read-only), không thể .value = .... Muốn đổi state → phải gọi method trên ViewModel (như loadUsers()), ViewModel kiểm soát mọi thay đổi.

2. Inheritance (Kế thừa): Class con dùng lại code (property + method) của class cha, có thể override để đổi behavior ở phần khác biệt.

Ví dụ: HomeViewModel : ViewModel() — tự động có viewModelScope, onCleared(), lifecycle binding mà không phải tự viết lại. Chỉ cần override phần riêng. Android SDK xây dựng trên inheritance: AppCompatActivity, Fragment, Service...

Bẫy: Inheritance tạo coupling chặt cha-con. Sửa cha → con có thể vỡ. Kotlin mặc định final để giảm kế thừa bừa bãi.

3. Polymorphism (Đa hình): Cùng 1 interface/method, nhiều cách thực thi khác nhau. Code gọi method không cần biết object thật sự thuộc class nào.

  • Override (đa hình động): class con override method cha, JVM quyết định gọi version nào lúc runtime dựa trên type thật của object (dynamic dispatch).
  • Overload (đa hình tĩnh): cùng tên function nhưng khác param (số lượng/kiểu), compiler chọn version lúc compile.

Ví dụ: UserRepository interface → UserRepositoryImpl (production, gọi API thật) và FakeUserRepository (test, trả data cứng). ViewModel nhận UserRepository qua constructor, không biết đang dùng impl nào → dễ test, dễ thay đổi.

Kotlin thường thay overloading bằng default argument: fun greet(name: String, greeting: String = "Hi") — 1 hàm thay 2 overload.

4. Abstraction (Trừu tượng hoá): Chỉ định nghĩa "làm gì" (contract/interface), giấu "làm thế nào" (implementation). Người dùng class chỉ cần biết signature, không cần hiểu nội bộ.

Khác Encapsulation ở chỗ: Encapsulation giấu state (data — private field), Abstraction giấu logic (implementation — interface vs impl). Encapsulation là "cách giấu", Abstraction là "giấu cái gì".

Ví dụ: repo.getUser(id) — ViewModel không biết bên trong có cache, có gọi API, có map DTO hay không. Mai mốt đổi REST → GraphQL, Room → SQLDelight — ViewModel không động.

Q12. Vì sao Kotlin class mặc định final?

Triết lý "Design for inheritance or prohibit it" (Effective Java, Item 19). Java mặc định open → class dễ bị extend bừa, phá vỡ invariant mà tác giả không lường trước. Kotlin đảo lại: muốn cho kế thừa phải khai báo open tường minh, kể cả từng method.

data class, enum class, object đều không thể open — vì semantics của chúng không phù hợp với kế thừa.

Q13. abstract class vs interface?

abstract class interface
State có backing field Không
Constructor Không
Đa kế thừa Không

Cả hai đều có thể chứa default method implementation. Sự khác biệt cốt lõi: abstract class có state (backing field, constructor), interface thì không.

Dùng abstract class khi cần chia sẻ state hoặc constructor logic giữa các subclass. Dùng interface cho contract/capability thuần tuý hoặc khi cần 1 class implement nhiều "khả năng" cùng lúc (multiple inheritance).

Q14. SOLID — 5 nguyên tắc

S — Single Responsibility: Mỗi class chỉ có 1 lý do để thay đổi. Ôm đồm nhiều việc → khó test, khó tái sử dụng, sửa chỗ này vỡ chỗ kia.

Dấu hiệu vi phạm: tên class có "Manager/Helper/Utils", file >300 dòng, method đụng nhiều domain (UI + network + analytics). Ví dụ: UserViewModel vừa gọi API, vừa format tên user, vừa gửi analytics → 3 lý do thay đổi. Tách: UserRepository (load) + UserFormatter (format) + Analytics (track).

O — Open/Closed: Mở cho mở rộng (thêm tính năng mới), đóng cho sửa đổi (không động code cũ đang chạy ổn). Cách đạt: dùng abstraction + polymorphism — code gốc gọi qua interface, thêm feature = thêm class mới implement interface đó.

Ví dụ: màn thanh toán có Card, MoMo, ZaloPay. Viết when(type) → thêm Apple Pay phải sửa hàm cũ. Theo OCP: interface PaymentMethod, mỗi phương thức là 1 class. Thêm Apple Pay = thêm ApplePayMethod, code cũ không động.

L — Liskov Substitution: Mọi chỗ dùng class cha phải thay thế hoàn toàn bằng class con mà code vẫn đúng — bao gồm cả behavior contract, không chỉ signature.

Dấu hiệu vi phạm: class con override method rồi throw UnsupportedOperationException; class con thắt input hoặc nới output; code phải is check để xử lý riêng cho từng loại con.

Ví dụ kinh điển: Penguin kế thừa Bird nhưng fly() throw exception → code nhận Bird gọi fly() sẽ vỡ. Fix: tách Flyable ra interface riêng, Penguin không implement Flyable.

I — Interface Segregation: Thà có nhiều interface nhỏ chuyên biệt còn hơn 1 "fat interface". Class không nên bị ép implement method nó không dùng.

Dấu hiệu vi phạm: class implement interface nhưng nhiều method để trống/throw, interface có >5-7 method không liên quan chặt. Ví dụ: interface Worker chứa cả work()eat()Robot chỉ cần work() nhưng bị buộc implement eat(). Fix: tách WorkableEatable.

D — Dependency Inversion: Module cấp cao (logic nghiệp vụ) không phụ thuộc module cấp thấp (chi tiết kỹ thuật). Cả hai cùng phụ thuộc abstraction (interface). Đây là cốt lõi của Clean Architecture.

Lợi ích: test dễ (inject fake/mock), thay đổi linh hoạt (đổi Retrofit → Ktor chỉ đổi impl, logic không động), modularization rõ ràng (Domain định nghĩa interface, Data implement).

Đừng nhầm DIP với DI: DIP = nguyên tắc thiết kế (phụ thuộc abstraction). DI = kỹ thuật cài đặt (truyền dependency qua constructor/setter thay vì new bên trong class). DI là cách phổ biến nhất để đạt DIP.

Q15. Composition over Inheritance — tại sao?

Inheritance tạo coupling chặt cha-con: sửa class cha → class con có thể vỡ, class con bị gắn cứng với 1 hierarchy. Composition (giữ reference đến object khác) linh hoạt hơn: dễ thay đổi behavior runtime, dễ tổ hợp nhiều behavior, dễ test từng phần riêng.

Ví dụ: thay vì class Penguin : Bird() (Penguin kế thừa fly() vô nghĩa), dùng class Bird(private val mover: Mover) — inject FlyingMover cho chim bay, WalkingMover cho chim đi bộ. Thay đổi behavior = thay object, không cần sửa class.


3. Android Internals

Q16. Activity lifecycle — 6 callback chính?

onCreateonStartonResume(running)onPauseonStoponDestroy

  • onCreate: khởi tạo view, setContentView(), nhận savedInstanceState để restore state. Chỉ gọi 1 lần trong đời Activity (trừ khi recreate).
  • onStart: Activity visible nhưng chưa ở foreground — user chưa tương tác được.
  • onResume: Activity ở foreground, focused, nhận input từ user. Đây là trạng thái "đang chạy".
  • onPause: mất focus (dialog overlay, multi-window, Activity khác lên partial). Vẫn visible 1 phần. Nên pause animation, release camera.
  • onStop: không visible nữa. Giải phóng tài nguyên nặng. Lưu data cần thiết (DB, SharedPreferences).
  • onDestroy: cleanup cuối cùng. Sau callback này, Activity bị GC thu hồi. Lưu ý: process có thể bị kill bất kỳ lúc nào sau onStop mà không qua onDestroy.

Config change (xoay màn hình, đổi locale, đổi theme): Activity destroy → recreate hoàn toàn. Dùng ViewModel giữ data qua recreation, rememberSaveable/SavedStateHandle cho UI state nhỏ.

onRestart: gọi khi Activity quay lại từ trạng thái stopped (ví dụ user bấm back từ Activity khác) → tiếp tục onStart.

Q17. Fragment lifecycle — khác Activity?

Thêm các callback riêng: onAttachonCreateonCreateViewonViewCreatedonStartonResumeonPauseonStoponDestroyViewonDestroyonDetach.

Bẫy quan trọng nhất: Fragment lifecycle ≠ View lifecycle. Fragment có thể sống lâu hơn View — khi Fragment bị đẩy vào backstack, onDestroyView được gọi (View bị huỷ) nhưng Fragment vẫn alive (không gọi onDestroy). Khi pop lại, onCreateViewonViewCreated chạy lại với View mới.

Hệ quả: khi observe Flow/LiveData từ Fragment, phải dùng viewLifecycleOwner, KHÔNG phải this. Dùng this → observer sống theo Fragment lifecycle → khi View bị huỷ mà Fragment còn sống, observer vẫn nhận data và cập nhật View đã huỷ → crash hoặc leak.

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { render(it) }
    }
}

Q18. ViewModel sống qua config change nhờ gì? SavedStateHandle để làm gì?

Config change (rotate): ViewModelStore được giữ qua NonConfigurationInstances của ComponentActivity. Khi Activity destroy do config change, hệ thống lưu ViewModelStore sang bên, Activity mới recreate xong nhận lại → ViewModel instance cũ được reuse nguyên vẹn.

ViewModel chết khi: Activity finish() thật sự (user bấm back, gọi finish()), Fragment bị pop khỏi backstack hẳn, hoặc process bị kill.

Process death hoàn toàn khác config change — toàn bộ process bị kill, mọi object trên heap mất (bao gồm ViewModel). Khi user quay lại app, Android restore Activity stack nhưng ViewModel khởi tạo lại từ đầu, dữ liệu trong RAM mất sạch.

→ Cần SavedStateHandle: lưu UI state nhỏ vào Bundle (survive cả process death):

  • UI state nhỏ (query text, selected tab, scroll position) → SavedStateHandle.
  • Data từ API → cache vào DB/disk, ViewModel reload khi khởi tạo lại.

Lưu ý: Bundle có giới hạn ~500KB cho toàn bộ transaction. Vượt quá → TransactionTooLargeException. Đừng nhét object lớn (list, bitmap) vào SavedStateHandle.

Q19. Process lifecycle — biết app foreground/background?

ProcessLifecycleOwner cung cấp observer cho lifecycle cấp toàn app (không phải từng Activity):

  • onStart → app vào foreground (có ít nhất 1 Activity visible).
  • onStop → app vào background (không Activity nào visible).

Hữu ích cho: pause/resume analytics tracking, stop/start polling, lock app khi inactive, hiển thị overlay "session expired".

Q20. Memory leak — pattern phổ biến và cách tránh?

Memory leak xảy ra khi object không còn cần nhưng vẫn bị giữ reference → GC không thu hồi được → RAM tăng dần → OOM crash.

Pattern Nguyên nhân Cách fix
Static giữ Context static var context = activity Dùng applicationContext
Inner class non-static Handler/AsyncTask giữ outer Activity static class + WeakReference
Listener không unregister Register onCreate, quên remove Cleanup trong onDestroy
Coroutine không cancel GlobalScope.launch giữ Activity ref Dùng viewModelScope/lifecycleScope
Singleton giữ Activity DI inject sai scope Inject Application thay vì Activity

Phát hiện: LeakCanary — tự dump heap khi nghi leak, hiển thị reference chain chi tiết.

Q21. Threading model — Looper / Handler / MessageQueue?

Mỗi thread có thể gắn 1 bộ Looper + MessageQueue + Handler:

  • MessageQueue: hàng đợi FIFO chứa các Message chờ xử lý.
  • Looper: vòng lặp vô hạn, liên tục lấy message từ queue ra xử lý. Mỗi lần lấy 1 message, xử lý xong mới lấy tiếp.
  • Handler: công cụ để post message vào queue (từ bất kỳ thread nào) + xử lý message khi đến lượt.

Main thread (UI thread) có Looper được khởi tạo sẵn trong ActivityThread.main(). Tất cả UI update, lifecycle callback, touch event đều chạy qua message queue này — tuần tự, không song song.

→ Block main thread = freeze UI. Mọi message phía sau đều phải đợi → nếu quá lâu → ANR.

Q22. ANR là gì? Ngưỡng bao nhiêu?

Application Not Responding — hệ thống phát hiện main thread không xử lý kịp sự kiện trong thời gian quy định:

  • Input event (touch, key): 5 giây
  • BroadcastReceiver onReceive(): 10 giây (foreground), 60 giây (background)
  • Service lifecycle: 20 giây (foreground), 200 giây (background)

Tránh ANR bằng cách đẩy việc nặng ra background:

  • I/O (network, file, DB) → Dispatchers.IO
  • CPU nặng (parse JSON lớn, image processing) → Dispatchers.Default
  • Tuyệt đối không Thread.sleep(), blocking .get(), hay synchronous network call trên main thread.

Debug: dùng StrictMode trong debug build để phát hiện disk/network operation trên main thread sớm.


4. Coroutines & Flow

Q23. Coroutine vs Thread?

Thread Coroutine
OS thread 1-1 N coroutines : M threads
Chi phí ~1MB stack ~vài KB
Chuyển đổi Kernel-level User-level (rẻ)
Tạm dừng Block thread Không block, giải phóng thread cho việc khác

suspend function compile thành state machine + Continuation (CPS — Continuation-Passing Style). Mỗi suspend point trở thành 1 state trong state machine. Khi suspend "tạm dừng", thread được giải phóng để chạy coroutine khác — không có "magic thread" hay green thread, chỉ là tái sử dụng thread pool thông minh.

Có thể chạy hàng ngàn coroutine trên vài thread mà không bị OOM — khác với thread, mỗi thread tốn ~1MB stack.

Q24. Dispatchers?

  • Main: UI thread. Chỉ dùng cho update view, observe Flow. Không bao giờ làm việc nặng ở đây.
  • IO: Pool cho I/O blocking (network, file, DB). Mặc định 64 thread, có thể mở rộng. Tối ưu cho blocking I/O (thread nằm chờ I/O response).
  • Default: Pool cho CPU-intensive (parse, sort, tính toán). Số thread = số CPU cores. Tối ưu cho compute (thread chạy liên tục, dùng hết CPU).
  • Unconfined: không confine vào thread cụ thể — chạy ở thread nào resume thì tiếp ở đó. Chủ yếu dùng cho testing hoặc case đặc biệt.

Q25. Structured Concurrency là gì?

Mọi coroutine phải chạy trong 1 CoroutineScope. Scope quản lý lifecycle: khi scope huỷ → tất cả coroutine con huỷ theo, tránh leak.

3 quy tắc cốt lõi:

  1. Parent đợi child: parent coroutine chỉ complete khi mọi child hoàn thành.
  2. Failure propagation: child fail → parent fail → cancel mọi sibling (trừ SupervisorJob).
  3. Cancellation propagation: cancel parent → cancel mọi child recursively.

Ví dụ: viewModelScope.launch { async { apiA() }; async { apiB() } } — nếu ViewModel cleared, cả 2 API call bị cancel. Không có coroutine "mồ côi" chạy ngầm tốn resource.

Q26. launch vs async vs withContext?

  • launch: "bắn-và-quên" — chạy coroutine nhưng không quan tâm kết quả. Trả về Job (có thể cancel/join). Dùng cho side effect (log, analytics, update UI).
  • async: chạy coroutine và trả về Deferred<T> — gọi .await() để lấy kết quả. Dùng khi cần chạy song song nhiều task rồi gộp kết quả.
  • withContext: switch dispatcher + trả kết quả, suspend đến khi xong. Dùng khi cần chạy tuần tự trên dispatcher khác.
// Tuần tự — tổng thời gian = a + b
val a = withContext(IO) { apiA() }
val b = withContext(IO) { apiB() }

// Song song — tổng thời gian = max(a, b)
coroutineScope {
    val a = async { apiA() }
    val b = async { apiB() }
    a.await() + b.await()
}

Exception handling khác nhau:

  • launch: throw ngay khi xảy ra → bắt bằng try-catch trong block, hoặc CoroutineExceptionHandler ở root.
  • async: exception được lưu trong Deferred → chỉ throw lúc gọi .await() → phải try-catch quanh .await(). CoroutineExceptionHandler KHÔNG bắt được exception từ async.

Q27. SupervisorJob vs Job?

  • Job (mặc định): child fail → cancel siblings + cancel parent → cả scope sập. Phù hợp khi các task phụ thuộc nhau — 1 cái fail thì cả nhóm vô nghĩa.
  • SupervisorJob: child fail không ảnh hưởng siblings. Mỗi child tự quản lý failure riêng. Phù hợp cho UI scope — 1 request fail không nên kill toàn bộ scope.

viewModelScopelifecycleScope đều dùng SupervisorJob internally → 1 coroutine crash không ảnh hưởng coroutine khác trong cùng scope.

Q28. Cancellation — cooperative là gì?

Coroutine không bị kill cưỡng bức — system chỉ set flag isActive = false. Coroutine phải tự kiểm tra flag này hoặc gọi suspend function (mọi standard suspend function đều tự check cancellation) để dừng.

// SAI — vòng lặp không check cancellation, chạy mãi
while (true) { heavyCompute() }

// ĐÚNG — check isActive mỗi vòng
while (isActive) { heavyCompute() }

// ĐÚNG — yield() là suspend point, check cancellation
while (true) { yield(); heavyCompute() }

Nếu code CPU-intensive không gọi suspend function nào, nó sẽ không bao giờ bị cancel dù scope đã huỷ.

Q29. Xử lý exception?

Hai cách bắt, tuỳ vị trí và nhu cầu:

  • try-catch: bắt exception trong block, xử lý local — biến lỗi thành UI state (Error), hiện snackbar, v.v.
  • CoroutineExceptionHandler: bắt uncaught exception ở root coroutine — dùng cho logging, crash reporting. Chỉ hoạt động với launch, không với async.

Quan trọng: Đừng bao giờ nuốt CancellationException. Đây là cơ chế structured concurrency dùng để huỷ coroutine. Nuốt nó = coroutine không biết đã bị cancel, tiếp tục chạy → phá vỡ lifecycle management. Nếu catch Exception chung, phải re-throw CancellationException.

Q30. Race condition — làm sao tránh?

Nhiều coroutine cùng đọc/ghi shared mutable state → kết quả không deterministic. Ví dụ: counter++ từ 1000 coroutine → kết quả < 1000 vì read-modify-write không atomic.

Cách Mô tả
Mutex Khoá đoạn critical: mutex.withLock { counter++ }
Atomic Cho primitive: AtomicInteger
Single-thread dispatcher Dispatchers.Default.limitedParallelism(1)

Đừng dùng synchronized trong suspend function — synchronized block thread (không phải suspend), giữ lock khi thread bị suspend → deadlock tiềm ẩn + phá structured concurrency.

Q31. StateFlow vs SharedFlow vs LiveData?

StateFlow SharedFlow LiveData
Giá trị khởi tạo Bắt buộc Không Không
Conflate Có (chỉ giữ latest) Tuỳ cấu hình
Lifecycle-aware Không (cần repeatOnLifecycle) Không
Use case UI state Sự kiện (one-shot) Legacy
  • StateFlow: luôn có giá trị hiện tại (.value), conflate tự động (emit giá trị trùng bị bỏ qua). Dùng cho UI state — Loading, Success, Error.
  • SharedFlow: configurable replay/buffer, không conflate mặc định, có thể emit cùng giá trị nhiều lần. Dùng cho events — snackbar, navigation, toast (sự kiện xảy ra 1 lần, không phải state).
  • LiveData: lifecycle-aware sẵn nhưng API hạn chế hơn Flow. Nên chuyển sang StateFlow cho dự án mới.

Q32. Cold vs Hot Flow?

  • Cold (flow { }, flowOf): không chạy cho đến khi có collector. Mỗi collector trigger 1 lần thực thi riêng — 2 collector = 2 lần chạy code bên trong flow { }.
  • Hot (StateFlow, SharedFlow): chạy độc lập với collector. Mọi collector chia sẻ cùng luồng emission. Không có collector vẫn có thể đang emit.

stateIn / shareIn chuyển cold → hot:

val users = repository.observeUsers()   // cold flow
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

WhileSubscribed(5000): tắt upstream sau 5 giây khi không có subscriber → tiết kiệm tài nguyên nhưng vẫn handle config change (Activity recreate trong <5 giây thì không restart).

Q33. Flow operators quan trọng?

Operator Công dụng
map, filter Transform/lọc giống collection
combine(a, b) Emit khi a HOẶC b thay đổi, cung cấp cả 2 latest
zip(a, b) Emit theo cặp — đợi cả hai có giá trị mới
flatMapLatest Huỷ flow trước, switch sang flow mới
debounce(300) Chờ 300ms im lặng mới emit — throttle input
distinctUntilChanged Bỏ emission trùng liên tiếp

Kết hợp kinh điển cho search box:

searchQuery
    .debounce(300)              // chờ user ngừng gõ
    .distinctUntilChanged()     // bỏ qua nếu query không đổi
    .flatMapLatest { query ->   // cancel search cũ, chạy search mới
        repo.search(query)
    }

Q34. Backpressure trong Flow?

Khi producer emit nhanh hơn consumer xử lý → buffer phình hoặc UI lag. Flow cung cấp 4 operator xử lý:

Operator Hành vi
buffer(n) Đệm n phần tử, suspend producer khi buffer đầy
conflate() Chỉ giữ value mới nhất, drop tất cả intermediate
collectLatest { } Huỷ block xử lý cũ khi có emission mới — chỉ xử lý cái cuối
sample(period) Lấy mẫu theo định kỳ, bỏ qua emission giữa các khoảng sampling

Ví dụ: sensor emit 60fps nhưng UI chỉ cần latest → sensorFlow.conflate().collect { render(it) }.

Q35. repeatOnLifecycle — tại sao cần?

Collect Flow trong UI phải dừng khi ở background (tránh tốn resource + crash khi update UI từ background) và restart khi foreground. repeatOnLifecycle tự động quản lý: start collect khi lifecycle đạt state chỉ định (STARTED), cancel khi xuống dưới.

Nếu collect bằng lifecycleScope.launch thuần → collector chạy mãi kể cả khi app background → waste.

Compose tương đương: collectAsStateWithLifecycle() — tự xử lý lifecycle binding.


5. Jetpack Compose

Q36. Compose vs XML?

XML + View Compose
Mô hình Imperative (ra lệnh từng bước) Declarative (mô tả trạng thái mong muốn)
State Cập nhật thủ công (setText()) Tự động recompose khi state đổi
Code XML layout + Kotlin logic riêng 100% Kotlin, UI và logic cùng chỗ

Compose loại bỏ View hierarchy truyền thống, thay bằng Composition tree. Không cần findViewById, ViewBinding, hay DataBinding. Preview bằng @Preview annotation ngay trong IDE.

Q37. Recomposition hoạt động thế nào? @Stable vs @Immutable?

Khi state được đọc bên trong composable thay đổi giá trị → Compose re-execute function đó (và children). Đây gọi là recomposition.

Skipping: Compose có thể bỏ qua recompose cho composable nếu 2 điều kiện thoả:

  1. Mọi param là stable type (primitive, String, function type, hoặc đánh dấu @Stable/@Immutable).
  2. Giá trị input không đổi (equals return true so với lần render trước).
  • @Immutable: cam kết mạnh — object không bao giờ đổi sau khi tạo. Mọi property đều val + immutable type. Compose tin tưởng hoàn toàn → skip nhanh nhất.
  • @Stable: cam kết yếu hơn — object có thể đổi, nhưng cam kết sẽ báo Compose biết khi đổi (thông qua MutableState bên trong). Compose vẫn skip được nếu equals trả true.

Bẫy: List, Mapunstable mặc định (vì là interface — impl có thể là MutableList). Compose không biết chắc chúng có thay đổi không → không skip. Fix: dùng ImmutableList (kotlinx.collections.immutable) hoặc wrap trong @Immutable data class.

Q38. remember vs rememberSaveable?

  • remember: lưu giá trị qua recomposition (composable bị gọi lại vẫn giữ value). Mất khi config change (rotate, locale change) vì Composition bị huỷ hoàn toàn.
  • rememberSaveable: lưu vào Bundlesống sót cả config change và process death. Chỉ dùng cho Parcelable, Serializable, primitive, hoặc khai báo custom Saver.

Quy tắc: UI state tạm (animation progress, dropdown expanded) → remember. UI state user quan tâm (text input, scroll position, selected tab) → rememberSaveable.

Q39. Side effects trong Compose?

Composable function phải idempotentkhông có side effect (đọc state, trả UI, không làm gì khác). Khi cần side effect, dùng các Effect API:

Effect Công dụng
LaunchedEffect(key) Chạy suspend khi vào composition. Chạy lại khi key đổi
DisposableEffect(key) Setup + cleanup (register/unregister listener)
SideEffect Chạy sau mỗi recomposition thành công
rememberCoroutineScope() Lấy scope để launch từ callback (onClick, onScroll)
  • LaunchedEffect: dùng khi cần gọi suspend function theo key (load data khi userId đổi, start animation). Cancel coroutine cũ khi key đổi hoặc composable rời composition.
  • DisposableEffect: dùng khi cần cleanup (unregister listener, release resource). Block onDispose được gọi khi key đổi hoặc composable rời composition.
  • SideEffect: không có coroutine, chạy synchronous sau mỗi successful recomposition. Dùng cho log analytics, sync state với non-Compose code.

Q40. Compose performance — best practices?

  • State hoisting: nâng state lên thấp nhất cần thiết (gần nơi đọc nhất), nhưng cao nhất 1 nơi dùng chung → giảm phạm vi recompose.
  • Stable types: dùng @Immutable cho data class, ImmutableList cho list → enable skipping.
  • Lambda stability: lambda capture biến không stable → mỗi recompose tạo lambda mới → child recompose theo. Fix bằng remember: val onClick = remember(id) { { vm.click(id) } }.
  • key trong LazyColumn: cung cấp unique key cho mỗi item → Compose track item đúng khi reorder/remove, tránh recompose toàn list.
  • derivedStateOf: cho computed state — chỉ trigger recompose khi kết quả tính toán thay đổi, không phải khi mỗi input thay đổi. Ví dụ: val showButton = derivedStateOf { scrollState.firstVisibleItemIndex > 0 } — chỉ recompose khi boolean đổi, không phải khi scroll mỗi pixel.
  • Defer reads: truyền () -> T (lambda) thay vì T trực tiếp → composable đọc giá trị ở phase cuối (draw), tránh recompose ở composition phase.

Q41. CompositionLocal — khi nào dùng?

Truyền data ngầm xuống deep tree mà không phải khai báo param qua từng cấp composable (tương tự React Context / Provider).

Built-in hay dùng: LocalContext (Android Context), LocalDensity (dp/px conversion), LocalLifecycleOwner (bind side effect vào lifecycle), LocalConfiguration (orientation, screenSize).

Tạo custom:

val LocalUser = compositionLocalOf<User?> { null }  // default value

CompositionLocalProvider(LocalUser provides currentUser) {
    // mọi composable bên trong đều đọc được LocalUser.current
}

Đúng: data toàn cục trong scope tree — theme, locale, current user, navigation controller. Sai: data 1 màn hình cụ thể — pass param explicit dễ trace flow data, dễ debug, dễ test hơn nhiều.

Trade-off: CompositionLocal tạo coupling ngầm — khó biết composable phụ thuộc gì khi nhìn vào signature. Đừng abuse.


6. Architecture (MVVM / MVI / Clean)

Q42. MVVM vs MVI?

MVVM MVI
State Nhiều StateFlow riêng lẻ 1 immutable UiState duy nhất
Sự kiện Gọi method trên ViewModel Gửi Intent/Action vào ViewModel
Luồng dữ liệu 2 chiều (binding) 1 chiều (unidirectional data flow)
Debug State thay đổi rời rạc Mọi thay đổi qua reducer → log dễ

MVVM: mỗi piece of state là 1 StateFlow/LiveData riêng. View gọi method trực tiếp trên ViewModel. Đơn giản, phù hợp form đơn giản, ít state.

MVI: toàn bộ UI state gói trong 1 immutable data class. View gửi Intent (sealed class mô tả hành động), ViewModel xử lý qua reducer (pure function: oldState + intent → newState). Mọi thay đổi state đi qua 1 chỗ → dễ debug, dễ log, dễ time-travel. Phù hợp UI phức tạp, nhiều state phụ thuộc nhau.

Q43. Clean Architecture — 3 layer + data flow?

┌─────────────────────────────────────┐
│ Presentation (UI + ViewModel)       │  ← Compose/Activity
├─────────────────────────────────────┤
│ Domain (UseCase + Entity)           │  ← Pure Kotlin, không Android
├─────────────────────────────────────┤
│ Data (Repository impl + DataSource) │  ← Room, Retrofit
└─────────────────────────────────────┘

Dependency Rule: chiều phụ thuộc hướng vào trong. Domain là lõi, không biết gì về Data hay Presentation. Data implement interface do Domain định nghĩa (Dependency Inversion). Presentation gọi UseCase của Domain.

Flow data thực tế (1 click → kết quả): User click "Refresh" → ViewModel gọi GetUsersUseCase() → UseCase gọi UserRepository.getUsers() (interface ở Domain) → UserRepositoryImpl (ở Data) gọi Retrofit API → JSON → map thành Entity → ViewModel cập nhật _uiState → Compose recompose → render danh sách.

Tại sao chia? Test Domain không cần Android framework. Đổi Room → SQLDelight, REST → GraphQL chỉ sửa Data layer, Domain và Presentation không động.

Q44. Repository pattern — vai trò?

Repository trừu tượng hoá data source — UseCase/ViewModel gọi repository qua interface, không biết và không quan tâm data đến từ network, local cache, hay in-memory.

Repository quyết định chiến lược data: check cache trước → nếu hết hạn gọi API → lưu DB → trả về. Mai mốt thêm logic sync offline, thêm CDN fallback → chỉ sửa Repository impl, không động logic nghiệp vụ.

Q45. UseCase — có cần không?

Pro: tái sử dụng business logic giữa nhiều ViewModel (ví dụ ValidateEmailUseCase dùng ở cả Register và Profile); tách Domain layer rõ ràng; dễ unit test (pure function).

Con: boilerplate nếu UseCase chỉ 1 dòng chuyển tiếp return repo.getUsers().

Quy tắc: dùng UseCase khi có logic thực sự (combine nhiều repo, validation, transformation, business rule). Đừng tạo UseCase chỉ để wrap 1 lời gọi repo — đó là over-engineering.

Q46. Hilt vs Koin?

Hilt Koin
Cơ chế Code-gen compile-time Runtime DSL
Khi sai cấu hình Compile error Runtime crash
Tích hợp Android Sâu (HiltViewModel, Worker) Cần plugin riêng
KMP Không

Hilt (Google): dùng annotation (@HiltAndroidApp, @Inject, @HiltViewModel) → KAPT/KSP sinh code lúc compile. Sai cấu hình → compile fail (phát hiện sớm). Build chậm hơn do code-gen nhưng runtime overhead gần như 0. Backed by Google → mainstream cho Android thuần.

Koin: pure Kotlin DSL (module { single { ... } }), không annotation processing. Build nhanh hơn, runtime có chút overhead (lookup map). Sai cấu hình → crash lúc chạy (phát hiện muộn). Hỗ trợ KMP — dùng chung module cho Android + iOS.

Chọn Hilt cho dự án lớn, team đông, ưu tiên type-safe. Chọn Koin cho dự án vừa/nhỏ, KMP, hoặc không muốn KAPT/KSP.


7. Modularization

Q47. Vì sao modular hoá?

  • Tốc độ build: Gradle incremental build chỉ rebuild module thay đổi → build nhanh hơn nhiều so với monolith.
  • Tái sử dụng: feature module có thể dùng lại ở app khác hoặc chia sẻ qua internal library.
  • Ranh giới rõ ràng: internal visibility modifier giới hạn trong module → module này không thấy implementation detail của module khác → giảm coupling.
  • Làm việc song song: team chia nhau theo module, ít conflict, review dễ.
  • Dynamic feature (Play Feature Delivery): tải module on-demand, giảm kích thước APK ban đầu.

Q48. Cách chia module phổ biến?

Chia theo layer + feature:

:app                — lắp ráp, DI, navigation graph
:core:ui            — design system, theme, common composable
:core:network       — Retrofit setup, interceptor, base response
:core:database      — Room setup, DAO base
:core:common        — utils, extensions, constants
:feature:home       — UI + ViewModel + UseCase riêng cho Home
:feature:profile    — UI + ViewModel + UseCase riêng cho Profile
:domain             — entity, repository interface (optional, tuỳ scale)

Quy tắc:

  • :feature không phụ thuộc :feature khác → giao tiếp qua :app hoặc navigation. Nếu 2 feature cần share logic → extract ra :core.
  • :core không phụ thuộc :feature → core là nền tảng, feature xây trên.
  • :app lắp ráp tất cả — DI graph, navigation, entry point.

Q49. api vs implementation trong Gradle?

  • implementation: dependency chỉ visible trong module hiện tại. Module khác phụ thuộc module này không thấy transitively. Build nhanh hơn vì Gradle biết phạm vi ảnh hưởng nhỏ.
  • api: dependency được expose ra ngoài → module phụ thuộc cũng thấy. Sửa dependency này → Gradle phải rebuild cả module phụ thuộc.

Best practice: dùng implementation mặc định. Chỉ dùng api khi type từ dependency xuất hiện trong public API của module (return type, parameter type, inheritance).


8. Design Patterns trong Android

Q50. Singleton — đúng cách trong Kotlin?

Kotlin cung cấp object declaration — thread-safe singleton được compiler đảm bảo. Tuy nhiên, dùng DI với @Singleton scope là preferred vì: test dễ thay thế (inject mock), lifecycle do DI container quản lý, tránh global mutable state khó debug.

Q51. Factory pattern — khi nào dùng?

Khi việc tạo object phức tạp (nhiều bước setup, cần logic chọn implementation) hoặc cần chọn impl runtime dựa trên input.

Trong Android: ViewModelProvider.Factory (tạo ViewModel với custom constructor), WorkerFactory (tạo Worker với custom dependency), Retrofit.Builder (kết hợp Factory + Builder).

Q52. Observer pattern — implement bằng Flow?

StateFlow/SharedFlow chính là Observer pattern hiện đại, tích hợp sẵn trong Kotlin:

  • Subject = Flow (nguồn phát)
  • Observer = collector (người nhận)
  • emit() = notifyObservers()

Lợi thế so với tự viết addListener/removeListener: Flow tự cleanup khi scope cancel → không lo quên unregister → không memory leak. Kết hợp với repeatOnLifecycle → lifecycle-aware.

Q53. Strategy pattern — ví dụ?

Đóng gói behavior vào object riêng, swap object để thay đổi behavior runtime mà không sửa class sử dụng.

Trong Android:

  • OkHttp Interceptor: mỗi interceptor là 1 strategy xử lý request (logging, auth, retry). Thêm/bỏ interceptor = thay đổi pipeline.
  • RecyclerView.ItemAnimator: swap animator khác nhau để đổi animation style.
  • DiffUtil.ItemCallback: strategy so sánh item cho ListAdapter.

Q54. Builder pattern — Kotlin có cần không?

Kotlin có named arguments + default parameters → thay thế Builder trong đa số trường hợp vì code gọn hơn nhiều, compiler kiểm tra type, không có vấn đề "quên gọi build()".

Builder vẫn dùng khi: Java interop (Java không có named argument), build phức tạp cần validation từng bước (ví dụ OkHttpClient.Builder() validate config trước khi tạo client), hoặc API cho library user (Builder pattern quen thuộc hơn với Java developer).


9. Điểm cộng: KMP, DSA cơ bản

Q55. Kotlin Multiplatform — share gì, không share gì?

Share (commonMain) — code chạy được trên mọi platform:

  • Domain models, Entity, UseCase, business logic, validation
  • Repository interface + implementation (nếu dùng KMP library)
  • Network: Ktor (thay Retrofit), Serialization: kotlinx.serialization
  • Database: SQLDelight (thay Room)
  • Utility, extension functions

Platform-specific (androidMain / iosMain) — code cần native API:

  • UI: Compose (Android) vs SwiftUI / Compose Multiplatform (iOS)
  • Platform API: notification, biometric, camera, file system
  • DI container setup (Koin module riêng cho mỗi platform)

Dùng expect/actual để khai báo API chung ở common, implement riêng ở mỗi platform.

Q56. Big-O — cheatsheet?

Big-O Tên Ví dụ
O(1) Hằng số HashMap get/put
O(log n) Logarit Binary search
O(n) Tuyến tính Duyệt array
O(n log n) Tuyến tính-log Merge sort
O(n^2) Bậc 2 Vòng lặp lồng nhau

Quy tắc phân tích: bỏ hằng số (2nn), giữ số hạng cao nhất (n^2 + nn^2), mặc định phân tích worst-case trừ khi đề bài nói rõ.

Q57. Pattern DSA hay gặp?

  • Two pointers: array đã sort, tìm cặp/triplet thoả điều kiện. Ví dụ: Two Sum II, 3Sum, Container With Most Water.
  • Sliding window: tìm substring/subarray thoả điều kiện với độ dài cố định hoặc biến đổi. Ví dụ: Longest Substring Without Repeating, Maximum Sum Subarray of Size K.
  • HashMap counting: đếm tần suất, kiểm tra anagram, Two Sum O(n). Dùng khi cần lookup nhanh.
  • BFS/DFS: duyệt tree, graph, matrix. BFS cho shortest path (unweighted), DFS cho explore toàn bộ.
  • Binary search: array sorted hoặc search-space monotonic. Biến thể: search insert position, find peak element.
  • Dynamic programming: khi bài toán có overlapping subproblemsoptimal substructure. Ví dụ: Climbing Stairs, Coin Change, Longest Common Subsequence.

Cheatsheet cuối — câu trả lời 1 dòng

Câu hỏi Trả lời
val vs var Tham chiếu bất biến vs khả biến
data class tự sinh gì? equals, hashCode, toString, copy, componentN (chỉ primary ctor)
Sealed dùng làm gì? Đóng kín hierarchy, when exhaustive, UI state
inline để làm gì? Bỏ lambda allocation + reified generics
SOLID — D nghĩa là? Phụ thuộc abstraction (interface), không phải impl
Activity lifecycle? onCreate→onStart→onResume→onPause→onStop→onDestroy
Fragment + View lifecycle? Khác nhau — observer phải dùng viewLifecycleOwner
ViewModel sống qua rotate nhờ? ViewModelStore qua NonConfigurationInstances
SavedStateHandle để làm gì? Sống sót process death (Bundle)
Memory leak phổ biến? Static context, inner class, listener, GlobalScope
Ngưỡng ANR? Input 5s, Receiver 10s/60s, Service 20s/200s
Coroutine vs Thread? N:M, rẻ, suspend không block
launch vs async? Bắn-và-quên vs Deferred (await)
Structured concurrency? Scope huỷ → con huỷ
StateFlow vs SharedFlow? State (1 latest) vs Events (replay tuỳ chỉnh)
Cold vs Hot Flow? Theo collector vs chia sẻ chung
@Stable vs @Immutable? Stable: báo Compose khi đổi. Immutable: không bao giờ đổi
MVI vs MVVM? 1 immutable state vs nhiều state
Clean Arch dependency rule? Phụ thuộc hướng vào trong (Domain trong cùng)
Hilt vs Koin? Hilt code-gen type-safe; Koin runtime DSL, hỗ trợ KMP
api vs implementation? Transitive vs không transitive