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. Thread-safe nghĩa là gì? synchronized hoạt động thế nào?

Thread-safe nghĩa là code/class hoạt động đúng đắn khi nhiều thread truy cập đồng thời, không cần caller phải tự xử lý đồng bộ bên ngoài. Ngược lại, code không thread-safe có thể gây race condition (kết quả phụ thuộc thứ tự chạy), data corruption, hoặc crash khi dùng từ nhiều thread.

Ví dụ không thread-safe: 2 thread cùng counter++ → cả hai đọc counter = 0, cả hai ghi counter = 1, mất 1 lần tăng. Đây là race condition kinh điển vì counter++ không phải atomic — nó gồm 3 bước: đọc → tăng → ghi.

synchronized là cơ chế khoá (lock/monitor) của JVM: chỉ 1 thread được vào block synchronized tại 1 thời điểm, các thread khác phải chờ (block) cho đến khi thread kia ra khỏi block.

// synchronized block — khoá trên object cụ thể
val lock = Any()
fun increment() {
    synchronized(lock) {
        counter++  // chỉ 1 thread chạy đoạn này tại 1 thời điểm
    }
}

// @Synchronized annotation — khoá trên this (tương đương synchronized(this))
class Counter {
    private var count = 0

    @Synchronized
    fun increment() { count++ }

    @Synchronized
    fun get(): Int = count
}

@Volatile: đảm bảo mọi thread luôn đọc giá trị mới nhất từ main memory, không đọc bản cache cũ trong CPU register/cache line. Tuy nhiên @Volatile chỉ đủ cho đọc/ghi đơn lẻ (set flag, publish reference), KHÔNG đủ cho compound operation (read-modify-write như counter++).

@Volatile
var stopped = false  // thread A ghi true → thread B thấy ngay, không đọc bản cũ

Khi nào dùng gì?

Tình huống Dùng
Cờ on/off, publish reference giữa các thread @Volatile
Compound operation (counter++, check-then-act) synchronized hoặc Atomic
Primitive counter cần atomic AtomicInteger, AtomicLong
Trong coroutine / suspend function Mutex (KHÔNG dùng synchronized)
Toàn bộ collection cần thread-safe ConcurrentHashMap, CopyOnWriteArrayList

Tại sao KHÔNG dùng synchronized trong coroutine?

synchronized block thread — thread bị giữ cứng, không thể giải phóng cho coroutine khác. Tệ hơn, coroutine có thể suspend giữa chừng rồi resume trên thread khác → vi phạm quy tắc "cùng thread phải giữ và nhả lock". Kết quả: deadlock hoặc mutual exclusion bị phá vỡ.

→ Trong coroutine, dùng Mutex — suspend (giải phóng thread) thay vì block (giữ thread), tương thích hoàn toàn với structured concurrency.

Q23. 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

Q24. 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.

Q25. 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.

Q26. 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.

Q27. 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.

Q28. 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.

Q29. 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ỷ.

Q30. 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.

Q31. 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.

Q32. 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.

Q33. 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).

Q34. 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)
    }

Q35. 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) }.

Q36. 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

Q37. 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.

Q38. 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.

Q39. 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.

Q40. 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.

Q41. 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.

Q42. 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)

Q43. 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.

Q44. 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.

Q45. 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ụ.

Q46. 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.

Q47. Hilt vs Koin — cơ chế, ưu nhược, khi nào chọn cái nào?

Dependency Injection (DI) là gì? Thay vì class tự tạo dependency bên trong (val repo = UserRepositoryImpl()), dependency được truyền từ bên ngoài vào (qua constructor, setter, hoặc DI framework). Lợi ích: loose coupling, dễ test (inject mock), dễ thay đổi implementation. DI framework tự động hoá quá trình này — developer khai báo "cần gì" và "cung cấp gì", framework lo việc kết nối.

Bản chất khác nhau hoàn toàn:

  • Hilt: dựa trên Dagger (Google), dùng annotation processing → KAPT/KSP đọc annotation lúc compile, sinh Java/Kotlin code thật sự chứa logic inject. Runtime chỉ gọi code đã sinh sẵn → overhead gần như 0.
  • Koin: pure Kotlin DSL, không annotation processing, không code-gen. Khai báo dependency bằng lambda trong module { } block. Runtime dùng HashMap lookup để resolve dependency khi được yêu cầu → có chút overhead.
Hilt Koin
Cơ chế Annotation → code-gen compile-time Kotlin DSL → runtime resolution
Nền tảng Dagger 2 (Google) Pure Kotlin (Insert-Koin)
Khi sai cấu hình Compile error (phát hiện sớm) Runtime crash (phát hiện muộn)
Build time Chậm hơn (KAPT/KSP code-gen) Nhanh hơn (không code-gen)
Runtime performance Gần 0 overhead (code sinh sẵn) Nhỏ (HashMap lookup mỗi lần inject)
Tích hợp Android Sâu (@HiltViewModel, @AndroidEntryPoint, WorkManager) Cần thêm koin-android, koin-compose
Scoping @Singleton, @ViewModelScoped, @ActivityScoped single { }, viewModel { }, scope { }
KMP Không — share DI module cross-platform
Learning curve Cao (annotation, component, module, scope) Thấp (Kotlin DSL quen thuộc)
Testing @HiltAndroidTest, @UninstallModules checkModules(), swap trực tiếp
Google recommend? — official Jetpack Không — third-party

Hilt — setup + code ví dụ:

// 1. Application — khởi tạo Hilt
@HiltAndroidApp
class MyApp : Application()

// 2. Module — khai báo "cung cấp gì"
@Module
@InstallIn(SingletonComponent::class)  // scope: toàn app
object NetworkModule {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit =
        Retrofit.Builder()
            .baseUrl("https://api.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    @Provides
    @Singleton
    fun provideUserApi(retrofit: Retrofit): UserApi =
        retrofit.create(UserApi::class.java)
}

// 3. Repository — inject qua constructor
@Singleton
class UserRepositoryImpl @Inject constructor(
    private val api: UserApi,
    private val db: UserDao
) : UserRepository {
    override suspend fun getUsers() = api.getUsers().map { it.toEntity() }
}

// 4. Bind interface → implementation
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    abstract fun bindUserRepo(impl: UserRepositoryImpl): UserRepository
}

// 5. ViewModel — Hilt tự inject
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repo: UserRepository
) : ViewModel() {
    val users = repo.observeUsers().stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
}

// 6. Activity/Fragment — nhận inject
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()  // Hilt tự tạo + inject
}

Hilt — ưu điểm chi tiết:

  • Compile-time safety: quên provide dependency → build fail ngay, không đợi đến runtime mới biết. Với dự án lớn (50+ module, 200+ dependency), đây là lợi thế cực lớn.
  • Scoping rõ ràng: @Singleton (app lifetime), @ActivityScoped, @ViewModelScoped, @FragmentScoped — Hilt tự quản lý lifecycle, tự cleanup khi scope kết thúc.
  • @HiltViewModel: tích hợp seamless với by viewModels() — không cần custom ViewModelProvider.Factory.
  • @AndroidEntryPoint: inject vào Activity, Fragment, Service, BroadcastReceiver, View — cover hết Android component.
  • WorkManager integration: @HiltWorker — inject dependency vào Worker dễ dàng.
  • Testing: @HiltAndroidTest + @UninstallModules → thay module production bằng module test trong integration test.

Hilt — nhược điểm:

  • Build time: KAPT chậm (deprecated dần), KSP nhanh hơn nhưng vẫn tốn thời gian code-gen. Project nhỏ cảm nhận rõ.
  • Learning curve cao: phải hiểu Dagger concepts (Component, Module, Scope, Qualifier, Binds vs Provides). Error message từ Dagger đôi khi khó hiểu.
  • Boilerplate: mỗi dependency cần annotation, module class, install-in annotation. Simple project cảm thấy nặng nề.
  • Không hỗ trợ KMP — chỉ Android/JVM.

Koin — setup + code ví dụ:

// 1. Khai báo module — Kotlin DSL thuần
val networkModule = module {
    single {
        Retrofit.Builder()
            .baseUrl("https://api.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    single { get<Retrofit>().create(UserApi::class.java) }
}

val repositoryModule = module {
    single<UserRepository> { UserRepositoryImpl(api = get(), db = get()) }
}

val viewModelModule = module {
    viewModel { UserViewModel(repo = get()) }
}

// 2. Khởi tạo trong Application
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MyApp)
            modules(networkModule, repositoryModule, viewModelModule)
        }
    }
}

// 3. Repository — constructor bình thường, không cần annotation
class UserRepositoryImpl(
    private val api: UserApi,
    private val db: UserDao
) : UserRepository {
    override suspend fun getUsers() = api.getUsers().map { it.toEntity() }
}

// 4. ViewModel — constructor bình thường
class UserViewModel(
    private val repo: UserRepository
) : ViewModel() {
    val users = repo.observeUsers().stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
}

// 5. Activity/Fragment — inject bằng delegate
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModel()  // Koin delegate
}

// 6. Compose — inject trực tiếp
@Composable
fun UserScreen(viewModel: UserViewModel = koinViewModel()) {
    val users by viewModel.users.collectAsStateWithLifecycle()
}

Koin — ưu điểm chi tiết:

  • Setup nhanh: không KAPT/KSP, không annotation, không code-gen → build nhanh hơn, đặc biệt với project vừa/nhỏ.
  • Kotlin DSL tự nhiên: single { }, factory { }, viewModel { } — đọc hiểu ngay, không cần học Dagger concepts.
  • KMP native: cùng module khai báo dùng chung cho Android + iOS + Desktop. Đây là lý do chính chọn Koin khi làm KMP.
  • Flexible: không bị ràng buộc bởi annotation → dễ tạo dynamic module, conditional provide, module override trong test.
  • checkModules(): verify DI graph trong unit test — phần nào bù đắp việc thiếu compile-time check.
  • Compose integration: koinViewModel() seamless trong Composable function.

Koin — nhược điểm:

  • Runtime crash: quên khai báo dependency → crash khi inject lần đầu (thường là khi user mở màn hình), không phải lúc build. Với dự án lớn, rủi ro miss dependency rất cao.
  • Runtime overhead: mỗi lần inject = HashMap lookup + lambda invocation. Với factory { } (tạo mới mỗi lần), overhead tích luỹ. Thực tế: overhead rất nhỏ, hiếm khi là bottleneck.
  • Không có scope lifecycle tự động như Hilt: phải tự quản lý scope creation/destruction bằng createScope()/closeScope(). Dễ quên cleanup → leak.
  • Google không recommend — documentation/tutorial chính thống đều dùng Hilt.

So sánh code cùng 1 use case:

// === HILT ===
// Cần: @Module, @InstallIn, @Provides/@Binds, @Inject, @HiltViewModel, @AndroidEntryPoint
// 6 annotation + module class → compile-time safe

// === KOIN ===
// Cần: module { }, single/factory/viewModel { }, startKoin { }, by viewModel()
// 0 annotation, pure Kotlin DSL → runtime resolution

Khi nào chọn cái nào?

Tình huống Chọn
Dự án lớn, team đông, nhiều module Hilt (compile-time safety cực quan trọng)
Dự án nhỏ/vừa, prototype nhanh Koin (setup nhanh, ít boilerplate)
KMP — share code Android + iOS Koin (Hilt không hỗ trợ KMP)
Team mới với DI, chưa biết Dagger Koin (learning curve thấp hơn nhiều)
Cần tích hợp sâu Jetpack (WorkManager, Nav) Hilt (first-class support từ Google)
Ưu tiên build time, CI/CD nhanh Koin (không code-gen)
Cần đảm bảo không crash runtime do DI Hilt (compile error > runtime crash)
Dự án đang dùng Dagger 2, muốn migrate Hilt (Hilt = Dagger + Android simplification)

Tóm gọn cho phỏng vấn: Hilt = Dagger-based, compile-time code-gen, type-safe, Google official, chỉ Android. Koin = pure Kotlin DSL, runtime resolution, nhanh setup, hỗ trợ KMP. Chọn Hilt khi dự án lớn cần compile-time safety + Jetpack ecosystem. Chọn Koin khi cần KMP hoặc dự án nhỏ muốn setup nhanh. Cả hai đều giải quyết cùng bài toán DI — chỉ khác approach.


7. Modularization

Q48. 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.

Q49. 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.

Q50. 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

Nền tảng: Design pattern là giải pháp đã được chứng minh cho các bài toán thiết kế lặp đi lặp lại. GoF (Gang of Four) chia thành 3 nhóm: Creational (tạo object), Structural (tổ chức class/object), Behavioral (giao tiếp giữa object). Trong phỏng vấn Android, không cần nhớ hết 23 pattern — chỉ cần nắm vững những pattern thường gặp trong codebase Android thực tế.


Creational Patterns — Tạo object

Q51. Singleton — đảm bảo chỉ 1 instance duy nhất

Ý tưởng: Đảm bảo 1 class chỉ có đúng 1 instance trong toàn bộ ứng dụng, và cung cấp điểm truy cập toàn cục đến instance đó.

Khi nào dùng: Quản lý tài nguyên dùng chung (database connection, network client, cache, logging), đảm bảo consistency (config, preference manager).

Cấu trúc:

  • Constructor phải private → bên ngoài không new được.
  • Class tự quản lý instance duy nhất.
  • Cung cấp static method/property để lấy instance.

Trong Kotlin: object declaration — compiler đảm bảo thread-safe, lazy-init (init lần đầu access), chỉ 1 instance:

// Kotlin object = thread-safe singleton, compiler sinh code đảm bảo
object Analytics {
    fun track(event: String) { /* ... */ }
}
Analytics.track("click_buy")  // truy cập trực tiếp, không cần getInstance()

Trong Android thực tế: dùng DI với @Singleton scope là preferred hơn object vì:

  • Testable: inject mock/fake dễ dàng, object thì không thay thế được.
  • Lifecycle rõ ràng: DI container quản lý lifecycle, object sống mãi đến khi process chết.
  • Tránh global mutable state: object dễ bị truy cập từ mọi nơi, khó trace data flow.
// Preferred: DI quản lý singleton
@Singleton
class AnalyticsTracker @Inject constructor(
    private val api: AnalyticsApi
) {
    fun track(event: String) { /* ... */ }
}

Bẫy: Singleton giữ reference đến Activity/Context → memory leak. Luôn dùng applicationContext nếu Singleton cần Context.

Q52. Factory Method — uỷ quyền việc tạo object cho subclass

Ý tưởng: Định nghĩa interface cho việc tạo object, nhưng để subclass quyết định tạo class cụ thể nào. Client code gọi factory method mà không biết class thật sự được tạo — chỉ biết interface/abstract type.

Khi nào dùng: Khi class không biết trước chính xác object nào sẽ được tạo; khi muốn subclass mở rộng bộ sản phẩm mà không sửa code cũ.

Cấu trúc:

  • Creator (abstract class/interface): khai báo factory method trả về Product.
  • ConcreteCreator: override factory method, trả về ConcreteProduct cụ thể.
  • Product (interface): type chung mà client code sử dụng.
  • ConcreteProduct: implementation cụ thể.
// Product interface
interface Notification {
    fun send(message: String)
}

// Concrete products
class PushNotification : Notification {
    override fun send(message: String) { /* FCM push */ }
}
class EmailNotification : Notification {
    override fun send(message: String) { /* SMTP email */ }
}

// Factory method — client không cần biết class cụ thể
fun createNotification(type: String): Notification = when (type) {
    "push" -> PushNotification()
    "email" -> EmailNotification()
    else -> throw IllegalArgumentException("Unknown type: $type")
}

Trong Android:

  • ViewModelProvider.Factory — tạo ViewModel với custom constructor. ViewModelProvider gọi create() mà không biết ViewModel cụ thể nào.
  • WorkerFactory — tạo ListenableWorker với custom dependency injection.
  • Fragment.instantiate() / FragmentFactory — tạo Fragment với custom constructor.

Biến thể — Abstract Factory: factory tạo ra họ object liên quan (ví dụ: LightThemeFactory tạo LightButton + LightCard, DarkThemeFactory tạo DarkButton + DarkCard). Ít gặp trực tiếp trong Android vì Compose theme system xử lý việc này.

Q53. Builder — xây dựng object phức tạp từng bước

Ý tưởng: Tách quá trình xây dựng object phức tạp ra khỏi biểu diễn của nó. Cho phép cùng 1 quy trình xây dựng tạo ra các biểu diễn khác nhau. Client config từng bước, gọi build() khi xong.

Khi nào dùng: Object có nhiều parameter (đặc biệt nhiều optional), cần validation trước khi tạo, hoặc quá trình tạo gồm nhiều bước.

Cấu trúc:

  • Builder: class chứa các setter/method config, mỗi method return this (fluent API).
  • Product: object phức tạp cần tạo.
  • Director (optional): định nghĩa thứ tự gọi builder methods.
// Builder trong Android — OkHttpClient
val client = OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS)
    .addInterceptor(loggingInterceptor)     // bước 1
    .addInterceptor(authInterceptor)        // bước 2
    .cache(Cache(cacheDir, 10 * 1024 * 1024)) // bước 3
    .build()                                  // validate + tạo object

Trong Android: Notification.Builder, AlertDialog.Builder, OkHttpClient.Builder, Retrofit.Builder, Room.databaseBuilder().

Kotlin có cần Builder không? Kotlin có named arguments + default parameters → thay thế Builder cho đa số case đơn giản:

// Không cần Builder — named args đủ rõ ràng
data class ServerConfig(
    val host: String,
    val port: Int = 8080,
    val ssl: Boolean = true,
    val timeout: Long = 30_000
)
val config = ServerConfig(host = "api.com", ssl = false)

Builder vẫn cần khi: Java interop (Java không có named argument), validation từng bước phức tạp, hoặc API cho library user.


Structural Patterns — Tổ chức class/object

Q54. Adapter — chuyển đổi interface không tương thích

Ý tưởng: Chuyển đổi interface của 1 class thành interface khác mà client mong đợi. Adapter cho phép 2 class không tương thích interface làm việc cùng nhau — đóng vai trò "phiên dịch" giữa 2 bên.

Khi nào dùng: Muốn dùng class có sẵn nhưng interface không khớp với code hiện tại; tích hợp library bên thứ 3; wrap legacy code.

Cấu trúc:

  • Target: interface mà client mong đợi.
  • Adaptee: class có sẵn với interface không tương thích.
  • Adapter: implement Target, bên trong giữ reference đến Adaptee, chuyển đổi lời gọi.
// Adaptee — API trả về DTO format
data class UserDto(val user_name: String, val user_email: String)

// Target — UI cần domain model
data class User(val name: String, val email: String)

// Adapter — chuyển đổi DTO → Domain
fun UserDto.toUser() = User(name = user_name, email = user_email)

Trong Android:

  • RecyclerView.Adapter: đây chính là Adapter pattern kinh điển nhất trong Android. Data (List) không tương thích trực tiếp với RecyclerView → Adapter chuyển đổi data thành ViewHolder mà RecyclerView hiểu.
  • ListAdapter / PagingDataAdapter: biến thể của Adapter kết hợp DiffUtil.
  • DTO → Domain mapping trong Data layer: UserResponse.toEntity(), ApiError.toDomainError().
// RecyclerView.Adapter — chuyển List<User> thành các ViewHolder
class UserAdapter : ListAdapter<User, UserViewHolder>(UserDiffCallback()) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        // tạo ViewHolder — "khung" hiển thị
    }
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bind(getItem(position))  // "dịch" User thành UI
    }
}

Q55. Decorator — thêm behavior mà không sửa class gốc

Ý tưởng: Gắn thêm trách nhiệm mới vào object động (runtime), bằng cách wrap object gốc trong object decorator. Decorator implement cùng interface với object gốc → client không phân biệt được.

Khác Inheritance ở chỗ: Inheritance thêm behavior tĩnh (compile-time), cho tất cả instance. Decorator thêm behavior động (runtime), cho từng instance riêng, có thể xếp chồng nhiều decorator.

Khi nào dùng: Khi cần thêm behavior mà không muốn tạo subclass; khi muốn tổ hợp nhiều behavior linh hoạt; khi class gốc không cho phép kế thừa (final).

Cấu trúc:

  • Component (interface): contract chung.
  • ConcreteComponent: object gốc.
  • Decorator (abstract): implement Component, giữ reference đến Component bên trong.
  • ConcreteDecorator: thêm behavior trước/sau khi delegate cho Component bên trong.
// Component interface
interface DataSource {
    fun readData(): String
}

// ConcreteComponent
class FileDataSource(private val filename: String) : DataSource {
    override fun readData(): String = File(filename).readText()
}

// Decorator — thêm logging mà không sửa FileDataSource
class LoggingDataSource(private val wrapped: DataSource) : DataSource {
    override fun readData(): String {
        Log.d("DataSource", "Reading data...")
        val data = wrapped.readData()  // delegate cho object gốc
        Log.d("DataSource", "Read ${data.length} chars")
        return data
    }
}

// Xếp chồng decorator
val source = LoggingDataSource(EncryptionDataSource(FileDataSource("data.txt")))

Trong Android:

  • OkHttp Interceptor Chain: mỗi interceptor wrap request/response, thêm behavior (logging, auth header, retry, caching) rồi delegate cho interceptor tiếp theo. Đây là Decorator + Chain of Responsibility.
  • InputStream wrapping: BufferedInputStream(FileInputStream(file)) — thêm buffering mà không sửa FileInputStream.
  • Context wrapper: ContextThemeWrapper wrap Context gốc, override theme.

Q56. Facade — giao diện đơn giản cho hệ thống phức tạp

Ý tưởng: Cung cấp 1 interface đơn giản che giấu sự phức tạp của hệ thống con bên dưới. Client chỉ giao tiếp với Facade thay vì phải biết và phối hợp nhiều class phức tạp.

Khi nào dùng: Khi hệ thống có nhiều class phụ thuộc lẫn nhau, client chỉ cần subset tính năng; khi muốn giảm coupling giữa client và hệ thống con.

Cấu trúc:

  • Facade: class đơn giản, expose các method high-level.
  • Subsystem classes: các class phức tạp bên trong, Facade phối hợp chúng.
// Subsystem classes — phức tạp, nhiều bước
class RetrofitClient { fun fetchUser(id: Int): UserDto = /* ... */ }
class RoomDatabase { fun cacheUser(user: UserEntity) { /* ... */ } }
class DtoMapper { fun toEntity(dto: UserDto): UserEntity = /* ... */ }

// Facade — client chỉ cần gọi 1 method
class UserRepository(
    private val api: RetrofitClient,
    private val db: RoomDatabase,
    private val mapper: DtoMapper
) {
    suspend fun getUser(id: Int): UserEntity {
        val cached = db.getUser(id)
        if (cached != null) return cached
        val dto = api.fetchUser(id)
        val entity = mapper.toEntity(dto)
        db.cacheUser(entity)
        return entity
    }
}

Trong Android:

  • Repository pattern chính là Facade — giấu chi tiết data source (API, DB, cache) sau 1 interface đơn giản.
  • Retrofit: Retrofit.create(ApiService::class.java) giấu toàn bộ HTTP plumbing (OkHttp, converter, call adapter).
  • MediaPlayer: Facade cho audio decoder, output device, buffer management.
  • Glide/Coil: Glide.with(context).load(url).into(imageView) — giấu networking, caching, decoding, transformation.

Q57. Proxy — kiểm soát truy cập đến object khác

Ý tưởng: Cung cấp object đại diện (proxy) cho object khác để kiểm soát truy cập: lazy init, cache, access control, logging, remote call. Proxy implement cùng interface với object thật → client không phân biệt.

Khi nào dùng: Object tốn tài nguyên để tạo (lazy loading), cần kiểm soát quyền truy cập, cần cache kết quả, hoặc object ở remote.

Các loại:

  • Virtual Proxy: lazy init — chỉ tạo object thật khi cần dùng lần đầu.
  • Protection Proxy: kiểm tra quyền trước khi delegate.
  • Caching Proxy: cache kết quả, trả cache nếu còn hạn.
interface ImageLoader {
    fun load(url: String): Bitmap
}

// Real object — tốn tài nguyên
class NetworkImageLoader : ImageLoader {
    override fun load(url: String): Bitmap = /* download từ network */
}

// Caching Proxy — cache kết quả, tránh download lại
class CachedImageLoader(private val real: NetworkImageLoader) : ImageLoader {
    private val cache = LruCache<String, Bitmap>(50)
    override fun load(url: String): Bitmap {
        return cache.get(url) ?: real.load(url).also { cache.put(url, it) }
    }
}

Trong Android:

  • by lazy: virtual proxy — Kotlin delegate tạo object lần đầu access.
  • Retrofit: interface method → proxy object tạo bởi Proxy.newProxyInstance() → mỗi lời gọi method được chuyển thành HTTP request.
  • Glide cache: memory cache → disk cache → network (chuỗi proxy).

Behavioral Patterns — Giao tiếp giữa object

Q58. Observer — thông báo khi state thay đổi

Ý tưởng: Định nghĩa quan hệ 1-nhiều giữa các object: khi 1 object (Subject) thay đổi state, tất cả object phụ thuộc (Observer) được tự động thông báo và cập nhật.

Khi nào dùng: Khi thay đổi ở 1 object cần phản ánh ở nhiều object khác; khi không biết trước có bao nhiêu observer; khi muốn loose coupling giữa publisher và subscriber.

Cấu trúc:

  • Subject (Observable): giữ danh sách observer, notify khi state đổi.
  • Observer: interface nhận thông báo, cập nhật theo state mới.
// Tự implement Observer (để hiểu cơ chế)
interface Observer<T> {
    fun onChanged(value: T)
}

class Observable<T> {
    private val observers = mutableListOf<Observer<T>>()
    private var value: T? = null

    fun observe(observer: Observer<T>) { observers.add(observer) }
    fun removeObserver(observer: Observer<T>) { observers.remove(observer) }

    fun setValue(newValue: T) {
        value = newValue
        observers.forEach { it.onChanged(newValue) }  // notify tất cả
    }
}

Trong Android — không cần tự implement, framework cung cấp sẵn:

Implementation Subject Observer Lifecycle-aware
LiveData MutableLiveData Observer { }
StateFlow / SharedFlow MutableStateFlow collect { } Cần repeatOnLifecycle
RxJava Observable Observable subscribe { } Không (dùng CompositeDisposable)
BroadcastReceiver System/App onReceive() Theo register/unregister

Flow là Observer hiện đại:

  • emit() = notifyObservers()
  • collect { } = onChanged()
  • Scope cancel → tự unregister → không memory leak.
  • Kết hợp operators (map, filter, combine) → reactive programming.

Q59. Strategy — đổi thuật toán runtime

Ý tưởng: Định nghĩa họ thuật toán, đóng gói mỗi thuật toán thành class riêng, và cho phép thay đổi thuật toán runtime mà không sửa class sử dụng. Client giữ reference đến interface Strategy, swap implementation tuỳ ý.

Khi nào dùng: Khi có nhiều cách thực hiện cùng 1 việc và muốn chọn cách nào tuỳ theo điều kiện runtime; khi muốn loại bỏ chuỗi if-else/when dài chọn behavior.

Cấu trúc:

  • Strategy (interface): khai báo method chung cho thuật toán.
  • ConcreteStrategy: implement thuật toán cụ thể.
  • Context: giữ reference đến Strategy, delegate việc thực thi.
// Strategy interface
interface SortStrategy {
    fun <T : Comparable<T>> sort(list: MutableList<T>)
}

// Concrete strategies
class QuickSort : SortStrategy {
    override fun <T : Comparable<T>> sort(list: MutableList<T>) { /* quick sort */ }
}
class MergeSort : SortStrategy {
    override fun <T : Comparable<T>> sort(list: MutableList<T>) { /* merge sort */ }
}

// Context — swap strategy runtime
class Sorter(private var strategy: SortStrategy) {
    fun setStrategy(newStrategy: SortStrategy) { strategy = newStrategy }
    fun <T : Comparable<T>> sort(list: MutableList<T>) = strategy.sort(list)
}

// Dùng
val sorter = Sorter(QuickSort())
sorter.sort(data)             // dùng QuickSort
sorter.setStrategy(MergeSort())
sorter.sort(data)             // đổi sang MergeSort, không sửa Sorter

Trong Android:

  • OkHttp Interceptor: mỗi interceptor là 1 strategy xử lý request/response (logging, auth, retry, caching). Thêm/bỏ interceptor = thay đổi pipeline xử lý.
  • DiffUtil.ItemCallback: strategy so sánh item cho ListAdapter — mỗi adapter có callback riêng tuỳ loại item.
  • RecyclerView.LayoutManager: LinearLayoutManager vs GridLayoutManager vs StaggeredGridLayoutManager — swap layout strategy mà không sửa RecyclerView.
  • RecyclerView.ItemAnimator: swap animator để đổi animation style.
  • Comparator: list.sortedWith(compareBy { it.name }) — strategy so sánh.

Kotlin idiom: thay vì tạo interface + class, dùng function type (lambda) — gọn hơn nhiều:

class Sorter(private var strategy: (MutableList<Int>) -> Unit) {
    fun sort(list: MutableList<Int>) = strategy(list)
}
val sorter = Sorter { list -> list.sort() }  // lambda thay thế class

Q60. Template Method — định nghĩa khung, subclass điền chi tiết

Ý tưởng: Định nghĩa bộ khung (skeleton) của thuật toán trong method cha, để subclass override các bước cụ thể mà không thay đổi cấu trúc tổng thể. Cha quyết định "thứ tự làm gì", con quyết định "làm thế nào".

Khi nào dùng: Khi nhiều class có thuật toán giống nhau về cấu trúc, chỉ khác ở vài bước; khi muốn kiểm soát thứ tự thực thi từ class cha.

Cấu trúc:

  • AbstractClass: chứa template method (final — không cho override) gọi các step method.
  • ConcreteClass: override các step method cụ thể.
abstract class DataParser {
    // Template method — khung cố định, subclass không override được
    fun parse(source: String): List<Item> {
        val raw = readData(source)      // bước 1: đọc
        val parsed = parseData(raw)     // bước 2: parse
        val validated = validate(parsed) // bước 3: validate
        return validated
    }

    abstract fun readData(source: String): String  // subclass implement
    abstract fun parseData(raw: String): List<Item> // subclass implement
    open fun validate(items: List<Item>): List<Item> = items // hook — có default
}

class JsonParser : DataParser() {
    override fun readData(source: String) = File(source).readText()
    override fun parseData(raw: String) = Gson().fromJson(raw, /* ... */)
}
class CsvParser : DataParser() {
    override fun readData(source: String) = File(source).readText()
    override fun parseData(raw: String) = raw.lines().map { /* parse CSV */ }
}

Trong Android — đây là pattern phổ biến nhất trong Android framework:

  • Activity lifecycle: onCreate()onStart()onResume() — framework quyết định thứ tự gọi, developer override để điền logic cụ thể. Activity là abstract class, lifecycle là template method.
  • Fragment lifecycle: tương tự — onCreateView(), onViewCreated() là các "step" mà developer override.
  • RecyclerView.Adapter: onCreateViewHolder()onBindViewHolder() — framework gọi theo thứ tự, developer implement từng bước.
  • AsyncTask (deprecated): doInBackground()onPostExecute().
  • View.draw(): framework gọi onDraw() — developer override để vẽ custom view.

Q61. Chain of Responsibility — truyền request qua chuỗi xử lý

Ý tưởng: Cho phép nhiều object có cơ hội xử lý request bằng cách truyền request qua chuỗi handler. Mỗi handler quyết định: xử lý request hoặc chuyển tiếp cho handler tiếp theo.

Khi nào dùng: Khi có nhiều object có khả năng xử lý request và muốn quyết định handler runtime; khi muốn decouple sender và receiver.

Cấu trúc:

  • Handler (interface): khai báo method xử lý + reference đến handler tiếp theo.
  • ConcreteHandler: xử lý nếu có thể, nếu không → chuyển tiếp cho next.
abstract class ApprovalHandler(private val next: ApprovalHandler? = null) {
    fun handle(amount: Int): String {
        return if (canHandle(amount)) approve(amount)
        else next?.handle(amount) ?: "Rejected — no handler"
    }
    abstract fun canHandle(amount: Int): Boolean
    abstract fun approve(amount: Int): String
}

class Manager : ApprovalHandler(Director()) {
    override fun canHandle(amount: Int) = amount <= 1000
    override fun approve(amount: Int) = "Manager approved $$amount"
}
class Director : ApprovalHandler(VP()) {
    override fun canHandle(amount: Int) = amount <= 5000
    override fun approve(amount: Int) = "Director approved $$amount"
}
class VP : ApprovalHandler() {
    override fun canHandle(amount: Int) = amount <= 20000
    override fun approve(amount: Int) = "VP approved $$amount"
}

Manager().handle(3000)  // "Director approved $3000"

Trong Android:

  • OkHttp Interceptor Chain: request đi qua chuỗi interceptor (Application → Network), mỗi interceptor có thể modify request, short-circuit, hoặc chuyển tiếp. Đây là CoR + Decorator kết hợp.
  • Android touch event dispatch: Activity.dispatchTouchEvent()ViewGroup.onInterceptTouchEvent()View.onTouchEvent() — event đi qua chuỗi View hierarchy, mỗi level quyết định xử lý hoặc chuyển tiếp.
  • Exception handling: try-catch nested — exception đi qua các catch block từ trong ra ngoài.

Q62. Command — đóng gói request thành object

Ý tưởng: Đóng gói request (hành động + tham số) thành 1 object riêng biệt. Cho phép: queue request, log request, undo/redo, schedule thực thi sau.

Khi nào dùng: Khi cần undo/redo; khi cần queue hoặc schedule thao tác; khi muốn decouple "ai ra lệnh" và "ai thực thi".

Cấu trúc:

  • Command (interface): khai báo execute() (và optional undo()).
  • ConcreteCommand: giữ receiver + params, implement execute().
  • Invoker: giữ command, gọi execute() khi cần.
  • Receiver: object thực sự thực hiện công việc.
// Command interface
interface Command {
    fun execute()
    fun undo()
}

// ConcreteCommand
class AddTextCommand(
    private val editor: TextEditor,
    private val text: String
) : Command {
    override fun execute() { editor.insert(text) }
    override fun undo() { editor.delete(text.length) }
}

// Invoker — quản lý history cho undo
class CommandManager {
    private val history = ArrayDeque<Command>()
    fun execute(command: Command) {
        command.execute()
        history.addLast(command)
    }
    fun undo() { history.removeLastOrNull()?.undo() }
}

Trong Android:

  • MVI Intent/Action: mỗi Intent (sealed class) là 1 Command — đóng gói user action thành object, ViewModel là Invoker xử lý.
  • WorkManager: OneTimeWorkRequest / PeriodicWorkRequest — đóng gói task thành object, queue thực thi, retry, chain.
  • Navigation action: NavDirections — đóng gói navigation destination + args thành object.
  • Undo/Redo trong text editor, drawing app.

Tổng hợp — Pattern nào gặp ở đâu trong Android?

Pattern Nhóm Ví dụ Android thực tế
Singleton Creational object, Hilt @Singleton, Application class
Factory Method Creational ViewModelProvider.Factory, WorkerFactory, FragmentFactory
Builder Creational Notification.Builder, OkHttpClient.Builder, Room.databaseBuilder
Adapter Structural RecyclerView.Adapter, DTO → Domain mapping
Decorator Structural OkHttp Interceptor, InputStream wrapping, ContextWrapper
Facade Structural Repository pattern, Retrofit, Glide/Coil
Proxy Structural by lazy, Retrofit dynamic proxy, image cache
Observer Behavioral Flow/LiveData/RxJava, BroadcastReceiver, lifecycle observer
Strategy Behavioral LayoutManager, Interceptor, DiffUtil.ItemCallback, Comparator
Template Method Behavioral Activity/Fragment lifecycle, RecyclerView.Adapter, custom View
Chain of Resp. Behavioral OkHttp chain, touch event dispatch
Command Behavioral MVI Intent, WorkManager, Navigation action

Mẹo phỏng vấn: Đừng nhớ pattern theo tên — nhớ theo bài toán nó giải quyết. Khi được hỏi "pattern nào dùng trong project?", liên hệ ngay với code thật: "Repository của em là Facade pattern, giấu API + DB sau 1 interface. Touch event dispatch là Chain of Responsibility. RecyclerView.Adapter chính là Adapter pattern + Template Method."


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

Q63. 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.

Q64. Room vs SQLDelight — khác nhau thế nào, khi nào dùng cái nào?

Bản chất khác nhau hoàn toàn:

  • Room: abstraction layer trên SQLite, viết query bằng annotation trong Kotlin/Java → compile-time verify → sinh code tương tác SQLite. Tư duy object-first — định nghĩa Entity (data class) trước, Room sinh table.
  • SQLDelight: viết SQL thuần trong file .sq → compile-time verify → sinh Kotlin data class + type-safe API. Tư duy SQL-first — định nghĩa schema/query bằng SQL trước, SQLDelight sinh code Kotlin.
Room SQLDelight
Ngôn ngữ query Annotation + SQL trong @Query File .sq chứa SQL thuần
Sinh code Entity → Table (object-first) SQL → Kotlin class (SQL-first)
Multiplatform (KMP) Không — chỉ Android — Android, iOS, Desktop, JS
SQLite engine Android SQLite (hoặc bundled) Tuỳ platform: SQLite, native driver
Migration Auto migration hoặc manual SQL Versioned .sqm migration files
Reactive (Flow/observe) Flow<List<T>> return type trong DAO .asFlow().mapToList() extension
Compile-time verify Có (KAPT/KSP verify SQL syntax) Có (plugin verify SQL syntax)
IDE support Android Studio tích hợp sâu Plugin riêng, ít mature hơn
Backing database Chỉ SQLite SQLite + có thể dùng driver khác
Google maintain? — Jetpack official Không — CashApp/Square maintain

Room — ưu điểm và hạn chế:

// Room: object-first — định nghĩa Entity, Room sinh table
@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "user_name") val name: String,
    val email: String
)

@Dao
interface UserDao {
    @Query("SELECT * FROM users WHERE id = :id")
    fun getUser(id: Int): Flow<UserEntity?>   // reactive tự nhiên

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(user: UserEntity)

    @Query("SELECT * FROM users ORDER BY user_name ASC")
    fun getAllSorted(): Flow<List<UserEntity>>
}

@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Ưu điểm:

  • Ecosystem Jetpack: tích hợp sâu với Hilt (@HiltViewModel), Paging 3 (PagingSource), WorkManager. Google maintain → documentation tốt, community lớn.
  • Auto migration (Room 2.4+): @AutoMigration(from = 1, to = 2) — Room tự phát hiện schema change đơn giản (thêm column, thêm table) và sinh migration code.
  • IDE support: Android Studio hiểu Room annotation → autocomplete SQL, highlight lỗi, navigate Entity↔DAO.
  • Type converter đơn giản: @TypeConverter cho custom type (Date, enum, JSON string).
  • Quen thuộc với Android developer — hầu hết tutorial/course đều dạy Room.

Hạn chế:

  • Chỉ Android — không dùng được cho KMP.
  • Object-first → với query phức tạp (JOIN nhiều bảng, subquery, window function), phải tạo thêm data class riêng cho kết quả hoặc dùng @RawQuery — mất type-safety.
  • SQL viết trong annotation string → khó format, khó đọc khi query dài.

SQLDelight — ưu điểm và hạn chế:

-- src/commonMain/sqldelight/com/example/User.sq

-- SQLDelight: SQL-first — viết SQL, tool sinh Kotlin class
CREATE TABLE user (
    id INTEGER NOT NULL PRIMARY KEY,
    user_name TEXT NOT NULL,
    email TEXT NOT NULL
);

getUser:
SELECT * FROM user WHERE id = ?;

getAllSorted:
SELECT * FROM user ORDER BY user_name ASC;

insert:
INSERT OR REPLACE INTO user (id, user_name, email) VALUES (?, ?, ?);
// Code Kotlin được sinh tự động — type-safe, không viết tay
val user: User? = database.userQueries.getUser(id = 1).executeAsOneOrNull()

// Reactive với Flow
database.userQueries.getAllSorted()
    .asFlow()
    .mapToList(Dispatchers.IO)
    .collect { users -> /* update UI */ }

Ưu điểm:

  • KMP native — cùng schema + query dùng cho Android, iOS, Desktop, JS. Database layer share 100% cross-platform.
  • SQL-first → tận dụng full power SQL: complex JOIN, subquery, window function, CTE — viết SQL thuần, không bị giới hạn bởi ORM abstraction.
  • Query phức tạp tự nhiên hơn Room vì viết SQL trực tiếp trong file .sq, không nhồi vào annotation string.
  • Compile-time verify mạnh: verify cả SQL syntax lẫn type mapping.
  • Migration rõ ràng: file .sqm versioned, dễ review trong PR.

Hạn chế:

  • Phải biết SQL tốt — không có abstraction layer giúp như Room @Insert, @Update, @Delete. Phải viết INSERT/UPDATE/DELETE thủ công.
  • IDE plugin kém mature hơn Room — ít autocomplete, ít navigation support.
  • Community nhỏ hơn Room nhiều → ít tutorial, ít StackOverflow answer.
  • Tích hợp với Jetpack (Paging, Hilt) phải tự setup, không có sẵn như Room.
  • Learning curve cao hơn nếu team quen Room.

Khi nào chọn cái nào?

Tình huống Chọn
Android thuần, team quen Jetpack Room
KMP — share database layer cho iOS SQLDelight
Query đơn giản (CRUD cơ bản) Room (ít boilerplate hơn)
Query phức tạp (JOIN, subquery, analytics) SQLDelight (SQL thuần mạnh hơn)
Cần tích hợp Paging 3, Hilt sâu Room (first-class support)
Team giỏi SQL, muốn kiểm soát hoàn toàn SQLDelight
Dự án mới, có kế hoạch KMP tương lai SQLDelight (tránh migrate sau)
Dự án đang dùng Room, không cần KMP Giữ Room (không đáng migrate)

Tóm gọn cho phỏng vấn: Room = object-first, Jetpack ecosystem, chỉ Android. SQLDelight = SQL-first, KMP cross-platform, full SQL power. Chọn Room cho Android thuần + team quen Jetpack. Chọn SQLDelight khi cần KMP hoặc query phức tạp. Không nên migrate Room → SQLDelight nếu không có lý do KMP rõ ràng.

Q66. 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õ.

Q67. 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 compile-time code-gen, type-safe. Koin runtime DSL, KMP
api vs implementation? Transitive vs không transitive
3 nhóm Design Pattern? Creational (tạo), Structural (tổ chức), Behavioral (giao tiếp)
Singleton trong Kotlin? object declaration, nhưng prefer DI @Singleton cho testable
Factory Method dùng khi? Uỷ quyền tạo object cho subclass, chọn impl runtime
Adapter pattern trong Android? RecyclerView.Adapter, DTO→Domain mapping
Decorator vs Inheritance? Decorator: thêm behavior động, xếp chồng. Inheritance: tĩnh
Observer trong Android? Flow/LiveData = Observer hiện đại, tự cleanup khi scope cancel
Template Method ở đâu? Activity/Fragment lifecycle, RecyclerView.Adapter callbacks
Chain of Responsibility? OkHttp interceptor chain, touch event dispatch
Room vs SQLDelight? Room: object-first, Android only. SQLDelight: SQL-first, KMP