Bảng Điều Khiển Ôn Tập Android

Kotlin • MVVM • Flow • REST API • Compose • Testing

Về Lộ trình kiến thức
P0 - Bắt buộc vững

Kotlin core

Kotlin là ngôn ngữ chính của job này. Mục tiêu là viết code sạch, ít bug và model UI state gọn gàng bằng type system.

1Vì sao phần này quan trọng

Job này dùng Kotlin làm ngôn ngữ chính, nên người phỏng vấn sẽ nhìn cách bạn viết code hằng ngày: có hay bị crash vặt không, code có gọn không, có dễ đọc lại sau này không. Bốn thứ dưới đây là những thứ bạn dùng MỖI NGÀY khi code Android, không phải lý thuyết suông — cứ đọc từng cái, tự hỏi "mình đang làm theo cách này hay cách kia" là sẽ thấy quen ngay.

2Null: khi một biến "có thể không có gì cả"

Crash phổ biến nhất khi mới học Android là: gọi một hàm trên một biến, mà biến đó lại đang "trống" (null) — ứng dụng lập tức sập. Kotlin xử lý việc này rất khéo: khi bạn khai báo một biến, bạn phải nói rõ nó CÓ THỂ trống hay LUÔN LUÔN có giá trị. Nếu khai báo là "luôn có giá trị" mà lại để nó trống thì Kotlin không cho biên dịch — lỗi bị bắt ngay khi viết code, chứ không phải đợi tới lúc user đang dùng app mới crash.

Cách viết: thêm dấu ? sau kiểu dữ liệu nghĩa là "biến này có thể trống". Ví dụ String là bắt buộc phải có chữ, còn String? là có thể có chữ hoặc có thể trống (null).

Vài cách xử lý một biến có thể trống

val name: String? = user.name   // name có thể trống

// Cách 1 — hỏi trước khi dùng: "nếu có thì lấy độ dài, không có thì thôi"
val length: Int? = name?.length

// Cách 2 — cho một giá trị dự phòng khi trống
val safeLength: Int = name?.length ?: 0

// Cách 3 — hỏi liên tiếp qua nhiều cấp
val city = user.address?.city ?: "Không rõ"

// Cách 4 — ép "chắc chắn có", nếu đoán sai app SẼ CRASH
val forced: Int = name!!.length

Lưu ý

Dấu !! giống như nói "tôi cam đoan cái này không trống, khỏi kiểm tra" — nếu đoán sai, app crash ngay tại chỗ. Đây là lý do !! bị coi là dấu hiệu code chưa cẩn thận. Cứ ưu tiên ?. và ?: trước, chỉ dùng !! khi thật sự chắc chắn 100%.

3val và var: biến nào được đổi, biến nào không

Nghĩ đơn giản: val giống như viết bằng bút mực — viết một lần, không sửa lại được. var giống bút chì — viết rồi vẫn xoá đi viết lại được. Trong code, val là gán giá trị một lần rồi thôi, var là biến có thể đổi giá trị nhiều lần.

Vì sao nên ưu tiên val: khi một biến không thể đổi, bạn đọc code ở bất kỳ đâu cũng chắc chắn giá trị của nó luôn như lúc gán, không cần lo có đoạn code khác lỡ tay đổi nó mất. Điều này giúp tránh rất nhiều bug "tự nhiên dữ liệu sai" mà không biết ai đã đổi lúc nào.

  • Mặc định hãy dùng val. Chỉ đổi sang var khi bạn thực sự cần cập nhật lại giá trị đó.
  • Một lưu ý nhỏ: val chỉ khoá việc gán lại biến, còn nếu bên trong là một danh sách có thể sửa (MutableList) thì nội dung danh sách đó vẫn thêm/xoá được. Muốn thật sự "đóng băng" cả nội dung thì dùng List thường (không phải MutableList).

4data class: dùng cho mọi thứ chỉ để "chứa dữ liệu"

Rất nhiều thứ trong app chỉ đơn giản là một "gói dữ liệu": một sản phẩm, một người dùng, một trạng thái màn hình. Với những gói dữ liệu như vậy, bạn thường cần vài việc: so sánh hai gói có giống nhau không, in ra xem bên trong có gì (để debug), và tạo một bản gần giống nhưng sửa một chút. Nếu tự viết class bình thường, bạn phải tự code tay cả ba việc đó — dễ quên, dễ sai. data class giúp bạn có sẵn cả ba, chỉ cần thêm một chữ "data" trước "class".

So sánh: class bình thường (phải viết tay) và data class (có sẵn)

// Class bình thường — muốn so sánh, in ra, hay tạo bản sửa đều phải viết tay
class Product(val id: String, val name: String) {
    override fun equals(other: Any?): Boolean { /* tự so sánh từng phần */ }
    override fun toString() = "Product(id=$id, name=$name)"
    fun copy(id: String = this.id, name: String = this.name) = Product(id, name)
}

// data class — chỉ một dòng, có sẵn tất cả
data class Product(val id: String, val name: String)

Ghi nhớ

Điểm hay nhất cần nhớ — cách hai object được coi là "giống nhau": • Với class thường: hai đối tượng dù trùng dữ liệu bên trong vẫn bị coi là khác nhau, vì Kotlin so sánh theo địa chỉ ô nhớ (chúng có phải đúng một object vật lý hay không), không nhìn vào nội dung. • Với data class: hai đối tượng có nội dung dữ liệu giống nhau thì được coi là bằng nhau, dù là hai object khác nhau trong bộ nhớ.

Điều này cực kỳ quan trọng trong Android: mỗi khi có UI state mới, hệ thống phải so sánh state mới với state cũ để quyết định có cần vẽ lại màn hình hay không. Nếu UI state là data class, phép so sánh đó nhìn vào nội dung — state thật sự không đổi thì không vẽ lại (đỡ lãng phí), state đổi thật thì mới vẽ lại. Nếu UI state là class thường, phép so sánh chỉ nhìn địa chỉ ô nhớ — mà mỗi lần bạn tạo state mới thì nó luôn ở một địa chỉ khác, nên hệ thống lúc nào cũng nghĩ là "đã đổi" và vẽ lại dù nội dung có khi giống y hệt.

Còn hàm copy() dùng để tạo một bản mới, giữ nguyên mọi thứ trừ vài phần bạn muốn đổi — tiện khi bạn muốn cập nhật một phần nhỏ của trạng thái mà không phải viết lại từ đầu.

Dùng copy() để cập nhật một phần trạng thái

data class UiState(
    val isLoading: Boolean = false,
    val items: List<Product> = emptyList(),
    val error: String? = null
)

// giữ nguyên items, chỉ đổi isLoading và error
_state.value = _state.value.copy(isLoading = true, error = null)

5sealed class: khi một màn hình chỉ ở đúng MỘT trạng thái tại một thời điểm

Một màn hình danh sách thường chỉ ở một trong vài trạng thái: đang tải, đã có dữ liệu, danh sách rỗng, hoặc bị lỗi. Chú ý là CHỈ MỘT trong số đó tại một thời điểm — không thể vừa "đang tải" vừa "bị lỗi" cùng lúc. Cách hay bị làm sai là gom hết vào một gói dữ liệu với nhiều cờ true/false riêng lẻ:

Cách dễ gây rối: nhiều cờ rời nhau

data class UiState(
    val isLoading: Boolean = false,
    val items: List<Product> = emptyList(),
    val error: String? = null
)

// Vấn đề: Kotlin không cấm bạn tạo ra những trạng thái vô lý, ví dụ:
// isLoading = true VÀ error khác null cùng lúc — vậy đang tải hay đang lỗi?
// Và không có gì nhắc bạn phải xử lý đủ hết các trường hợp có thể xảy ra.

sealed class (hoặc sealed interface) giải quyết đúng vấn đề này: bạn định nghĩa hẳn ra một DANH SÁCH CỐ ĐỊNH các trạng thái có thể xảy ra, và Kotlin biết chính xác danh sách đó gồm những gì. Mỗi trạng thái là một kiểu riêng biệt, chỉ mang đúng thông tin nó cần — trạng thái Success mang theo list, trạng thái Error mang theo dòng lỗi, còn Loading/Empty thì không cần mang gì cả. Nhờ vậy không còn chuyện tạo ra một trạng thái vô lý nữa.

Lợi ích thứ hai còn hay hơn: khi bạn viết when để xử lý từng trạng thái, Kotlin sẽ BẮT bạn xử lý đủ hết mọi trạng thái trong danh sách — nếu bạn bỏ quên một cái, code báo lỗi ngay khi viết, không đợi tới lúc chạy mới lộ ra màn hình trắng. Sau này nếu thêm một trạng thái mới, Kotlin lại nhắc bạn đi sửa hết những chỗ còn thiếu.

sealed interface: mỗi trạng thái một kiểu riêng, xử lý đủ hết không sót

sealed interface UiState {
    data object Loading : UiState
    data class Success(val items: List<Product>) : UiState
    data object Empty : UiState
    data class Error(val message: String) : UiState
}

when (state) {
    UiState.Loading -> showLoading()
    is UiState.Success -> showList(state.items)   // chắc chắn có items ở đây
    UiState.Empty -> showEmpty()
    is UiState.Error -> showError(state.message)  // chắc chắn có message ở đây
}

Mẹo

Nhớ đơn giản thế này: data class dùng cho "một gói dữ liệu" (so sánh, sao chép cho tiện). sealed class dùng cho "một trong nhiều trạng thái có thể xảy ra" (không tạo được trạng thái vô lý, không bỏ quên trường hợp nào). Hai cái này hay đi cùng nhau: một sealed class mà mỗi trạng thái bên trong lại là một data class.

6let, apply, also, run: những hàm tiện dùng cho gọn code

Đây là những hàm nhỏ giúp code ngắn lại, không có gì phức tạp — chỉ cần nhớ MỖI HÀM DÙNG KHI NÀO, không cần nhớ máy móc lý thuyết bên trong.

  • let — dùng khi muốn "chỉ chạy đoạn này nếu giá trị không trống". Ví dụ: user.email?.let { gửi email }.
  • apply — dùng khi muốn "tạo một object rồi chỉnh vài thuộc tính của nó luôn", cuối cùng trả lại chính object đó.
  • also — dùng khi muốn "làm thêm một việc phụ (như ghi log) mà không ảnh hưởng tới giá trị đang có".
  • run — dùng khi muốn "nhóm vài dòng code lại rồi lấy kết quả cuối cùng".

Ví dụ thường gặp

// let: chỉ gửi welcome email nếu có email
user.email?.let { email -> sendWelcome(email) }

// apply: tạo Intent rồi gắn thêm dữ liệu vào, cuối cùng có lại chính Intent đó
val intent = Intent(context, DetailActivity::class.java).apply {
    putExtra("id", productId)
}

Mẹo

Không nhớ hết cũng không sao. Khi đọc code người khác thấy let/apply/also/run, chỉ cần hiểu chúng là cách viết gọn hơn cho những việc rất quen: kiểm tra null, cấu hình object, làm việc phụ, hay gom nhóm code.

7Xử lý danh sách (list) mà không cần viết vòng lặp

Khi làm app, bạn thường xuyên phải "biến đổi" một danh sách: lọc ra vài phần tử, đổi mỗi phần tử thành một dạng khác, tìm một phần tử cụ thể... Cách cũ là viết vòng lặp for, nhưng Kotlin có sẵn các hàm ngắn hơn, đọc vào là hiểu ngay đang làm gì.

Vài hàm hay dùng nhất với danh sách

val products: List<Product> = ...

val names = products.map { it.name }                 // lấy ra tên của từng sản phẩm
val inStock = products.filter { it.stock > 0 }        // chỉ giữ sản phẩm còn hàng
val totalStock = products.sumOf { it.stock }          // cộng tổng số lượng còn trong kho
val first = products.firstOrNull { it.imageUrl != null } // tìm sản phẩm đầu tiên có ảnh
val grouped = products.groupBy { it.stock > 0 }       // chia thành nhóm còn hàng / hết hàng

Ghi nhớ

Tên gọi cần nhớ: map, filter, sumOf, firstOrNull, groupBy... đều được gọi là higher-order function — tức là những hàm NHẬN VÀO một hàm khác làm tham số (chính là đoạn { ... } bạn viết bên trong dấu ngoặc nhọn). Ví dụ products.filter { it.stock > 0 }: filter là hàm có sẵn của Kotlin, còn { it.stock > 0 } là một hàm nhỏ do bạn viết, được truyền vào để filter biết "giữ lại phần tử nào". Bạn không cần nhớ định nghĩa hàn lâm, chỉ cần nhận ra: khi thấy một hàm có cặp {} theo sau, đó là higher-order function — hàm đó đang được bạn "dạy" cách xử lý qua đoạn code trong {}.

Mẹo

Ưu tiên các hàm có chữ OrNull (như firstOrNull) khi bạn không chắc danh sách có phần tử phù hợp — chúng trả về null thay vì làm app crash khi không tìm thấy gì.

Checklist

Tick những gì bạn đã nắm chắc

0/6

Lỗi thường gặp

  • Dùng `!!` bừa bãi khiến app crash NullPointerException.
  • Nhầm `let`/`apply`: `apply` trả về receiver, `let` trả về kết quả lambda.
  • Dùng `var` và mutable state ở nơi lẽ ra nên immutable.

Câu trả lời mẫu

Kotlin null-safety bắt em xử lý null ngay ở compile time, nên giảm crash so với Java. Em model UI state bằng sealed interface để compiler ép xử lý đủ Loading/Success/Error, và ưu tiên `val` cùng immutable data class cho state.

Câu hỏi liên quan

Tự luyện trước khi trả lời