Module 14 — Kotlin & Android Basics

Nên có ⏱ 10-12 giờ 📋 Prerequisites: Module 01

🎯 Mục tiêu học tập

📖 Hướng dẫn học

1 Kotlin Basicskotlinlang.org/docs
Focus: Basic syntax, Null safety, Classes, Collections, Scope functions. Thời gian: ~3h
2 Android Fundamentalsdeveloper.android.com/courses
Focus: Activity lifecycle, Intents, XML Layouts, RecyclerView. Thời gian: ~4h
3 Build mini Todo app trong Android Studio
RecyclerView + ViewModel + Room hoặc in-memory. Thời gian: ~3h
4 Debug Android side của React Native
Mở Android Studio, đọc Logcat, kiểm tra Gradle files, Manifest, permissions, build variants. Thời gian: ~2h

⚡ Ôn nhanh cho React Native dev

🧭 Vòng lặp học module này

📚 Lý thuyết chi tiết

1. Kotlin Fundamentals

val / var / const

val name = "Linh"       // immutable (như JS const cho primitives)
var age = 25            // mutable (như JS let)
const val API_URL = "https://api.example.com"  // compile-time constant

Null Safety

Kotlin's killer feature — NullPointerException prevention tại compile time.

var name: String = "Linh"    // non-null — KHÔNG thể assign null
var email: String? = null    // nullable — CÓ thể null

// Safe call operator
val length = email?.length        // null nếu email null

// Elvis operator
val len = email?.length ?: 0      // default value nếu null

// Non-null assertion (TRÁNH dùng)
val len2 = email!!.length         // crash nếu null

// Smart cast
if (email != null) {
  println(email.length)           // auto smart-cast thành non-null
}

// let scope function
email?.let { validEmail ->
  sendEmail(validEmail)           // chỉ chạy nếu non-null
}

Data Class

data class User(
  val id: String,
  val name: String,
  val email: String,
  val age: Int = 0
)

val user = User("1", "Linh", "linh@test.com")
val copy = user.copy(age = 25)
val (id, name) = user  // destructuring

Auto-generates: equals(), hashCode(), toString(), copy(), componentN() functions.

Sealed Class vs Enum

sealed class Result<out T> {
  data class Success<T>(val data: T) : Result<T>()
  data class Error(val message: String) : Result<Nothing>()
  data object Loading : Result<Nothing>()
}

fun handleResult(result: Result<User>) {
  when (result) {
    is Result.Success -> showUser(result.data)
    is Result.Error -> showError(result.message)
    is Result.Loading -> showLoading()
  }
}
EnumSealed Class
StateFixed set, no data per instanceFixed set, EACH can hold different data
InstancesSingleton per valueMultiple instances per subclass
Use caseSimple constants (Status, Color)Result types, UI states, Navigation events

Scope Functions

FunctionObject refReturnUse case
letitLambda resultNull check + transform
runthisLambda resultObject config + compute result
applythisObject itselfObject initialization (builder pattern)
alsoitObject itselfSide effects (logging, validation)
withthisLambda resultGrouping calls on object
val user = User("1", "Linh", "linh@test.com").apply {
  // this = user, return user
}

user.also {
  Log.d("TAG", "Created user: $it")  // side effect
}

val displayName = user.let {
  "${it.name} (${it.email})"  // transform, return result
}

Extension Functions & Collections

fun String.isValidEmail(): Boolean =
  this.matches(Regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"))

val emails = listOf("a@b.com", "invalid", "c@d.org")
val validEmails = emails.filter { it.isValidEmail() }

val users = listOf(user1, user2, user3)
val namesByAge = users
  .filter { it.age > 18 }
  .sortedBy { it.age }
  .map { it.name }
  .joinToString(", ")

2. Android Components

ComponentVai tròVí dụ
Activity1 screen/UI entry pointMainActivity, LoginActivity
ServiceBackground work (no UI)Music player, sync service
BroadcastReceiverRespond to system eventsBattery low, network change
ContentProviderShare data giữa appsContacts, MediaStore

3. Activity Lifecycle

onCreate()     → Activity created (init UI, bind data)
  ↓
onStart()      → Visible (nhưng chưa interactive)
  ↓
onResume()     → Foreground + interactive (user can interact)
  ↓
onPause()      → Partially visible (dialog, multi-window)
  ↓
onStop()       → Not visible (user navigated away)
  ↓
onDestroy()    → Activity destroyed (cleanup)

Configuration Change (rotate screen):
onPause() → onStop() → onDestroy() → onCreate() → onStart() → onResume()

Configuration change problem: Khi xoay màn hình → Activity bị destroy + recreate → mất state. Giải pháp: ViewModel (survive config changes), savedInstanceState (survive process death).

4. XML Layouts

LayoutMô tảKhi nào
ConstraintLayoutFlat hierarchy, constraint-basedComplex layouts (recommended)
LinearLayoutHorizontal/Vertical stackSimple lists, rows
FrameLayoutStack children on topOverlays, fragments container

View Binding — type-safe access to views (replace findViewById):

class MainActivity : AppCompatActivity() {
  private lateinit var binding: ActivityMainBinding

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    binding.btnSubmit.setOnClickListener { submit() }
  }
}

5. RecyclerView

class UserAdapter(private val users: List<User>) :
  RecyclerView.Adapter<UserAdapter.ViewHolder>() {

  class ViewHolder(val binding: ItemUserBinding) :
    RecyclerView.ViewHolder(binding.root)

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val binding = ItemUserBinding.inflate(
      LayoutInflater.from(parent.context), parent, false
    )
    return ViewHolder(binding)
  }

  override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val user = users[position]
    holder.binding.tvName.text = user.name
    holder.binding.tvEmail.text = user.email
  }

  override fun getItemCount() = users.size
}

DiffUtil: Efficient list updates — calculate diff, animate changes. Tương tự React reconciliation.

6. ViewModel + StateFlow

class UserViewModel : ViewModel() {
  private val _uiState = MutableStateFlow<Result<List<User>>>(Result.Loading)
  val uiState: StateFlow<Result<List<User>>> = _uiState.asStateFlow()

  fun loadUsers() {
    viewModelScope.launch {
      _uiState.value = Result.Loading
      try {
        val users = repository.getUsers()
        _uiState.value = Result.Success(users)
      } catch (e: Exception) {
        _uiState.value = Result.Error(e.message ?: "Unknown error")
      }
    }
  }
}

Tại sao ViewModel: Survive configuration changes (rotate screen). Activity destroy + recreate nhưng ViewModel vẫn sống. Tương tự concept store trong Zustand/Redux.

7. Coroutines

Kotlin coroutines = async/await của Kotlin. Lightweight threads cho concurrent operations.

launchasync
ReturnJob (fire and forget)Deferred<T> (có result)
Use caseSide effects (update UI, save DB)Parallel computation (cần return value)
Get resultKhông.await()
viewModelScope.launch {
  val user = withContext(Dispatchers.IO) {
    api.getUser(userId)
  }
  _uiState.value = Result.Success(user)
}

// Parallel
viewModelScope.launch {
  val userDeferred = async(Dispatchers.IO) { api.getUser(id) }
  val postsDeferred = async(Dispatchers.IO) { api.getPosts(id) }
  val user = userDeferred.await()
  val posts = postsDeferred.await()
}

Dispatchers

DispatcherThreadUse case
MainUI threadUpdate UI, LiveData
IOBackground (shared pool)Network, DB, file I/O
DefaultCPU-intensive (shared pool)Sorting, parsing, heavy computation

8. Flow

StateFlowSharedFlowLiveData
Initial valueRequiredOptionalOptional
ReplayAlways 1 (latest)ConfigurableAlways 1
Lifecycle-awareNo (need collectAsStateWithLifecycle)NoYes
Use caseUI state (Replace LiveData)Events (snackbar, navigation)Simple UI state (legacy)

9. Hilt DI (Basics)

@HiltAndroidApp
class MyApp : Application()

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
  @Provides
  @Singleton
  fun provideApiService(): ApiService =
    Retrofit.Builder()
      .baseUrl(BASE_URL)
      .build()
      .create(ApiService::class.java)
}

@HiltViewModel
class UserViewModel @Inject constructor(
  private val repository: UserRepository
) : ViewModel()

10. Android files hay đụng trong React Native

FileDùng khiLưu ý
android/app/build.gradleDependency, signing, buildTypes, versionCodeĐổi native dependency thường cần clean/rebuild
AndroidManifest.xmlPermissions, activity, deep link intent-filterSai intent-filter làm deep link không vào app
MainActivity.ktActivity entry point, splash, new architecture configTránh làm heavy work trong onCreate
MainApplication.ktPackage/native module registrationKiểm tra khi autolinking không hoạt động
<uses-permission android:name="android.permission.CAMERA" />

<activity
  android:name=".MainActivity"
  android:exported="true"
  android:windowSoftInputMode="adjustResize">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="myapp" android:host="product" />
  </intent-filter>
</activity>

❓ Câu hỏi phỏng vấn + Đáp án

Q1: Kotlin null safety — ?. !! ?: let khác nhau thế nào?
  • ?. (safe call): return null nếu receiver null, không crash
  • !! (non-null assertion): crash NullPointerException nếu null — TRÁNH dùng
  • ?: (Elvis): provide default value khi null
  • let: execute block chỉ khi non-null, biến scope function it
Q2: data class tự generate gì?

equals()/hashCode() (compare by properties), toString() (readable output), copy() (shallow copy với optional property override), componentN() (destructuring). Chỉ dùng properties trong primary constructor.

Q3: sealed class vs enum — khi nào dùng cái nào?

Enum: fixed set of constants, mỗi value là singleton, không hold different data. Ví dụ: Status(ACTIVE, INACTIVE).

Sealed class: restricted hierarchy, mỗi subclass CÓ THỂ hold different data, multiple instances. Ví dụ: Result(Success(data), Error(message), Loading). Dùng sealed khi cần khác nhau data per variant.

Q4: launch vs async trong coroutines?

launch: fire-and-forget, return Job, dùng cho side effects (update UI, save DB). async: return Deferred<T>, dùng khi cần result — gọi .await() để lấy. Parallel execution: dùng multiple async + await.

Q5: ViewModel tại sao survive configuration change?

ViewModel stored trong ViewModelStore — owned bởi ViewModelStoreOwner (Activity/Fragment). Khi config change: Activity destroy + recreate nhưng ViewModelStore giữ lại. Chỉ clear khi Activity truly finished (finish() hoặc user back). Internal mechanism: retained fragment hoặc NonConfigurationInstances.

Q6: StateFlow vs LiveData — nên dùng cái nào?

StateFlow: Kotlin-native, testable, combine/transform operators, works everywhere. LiveData: lifecycle-aware tự động, no leak. Trend: StateFlow + collectAsStateWithLifecycle() thay thế LiveData. Dùng StateFlow cho new code.

Q7: Scope functions (let/run/apply/also/with) — chọn cái nào?

Dùng let: null check + transform. apply: object init/config (return object). also: side effects/logging (return object). run: compute result từ object. with: grouping calls (non-extension).

Q8: Activity lifecycle khi xoay màn hình?

onPause → onStop → onDestroy → onCreate → onStart → onResume. Activity hoàn toàn destroy rồi recreate. State mất nếu không save. Solutions: ViewModel (survive config change), savedInstanceState (survive process death), android:configChanges (prevent recreate — không recommended).

Q9: RecyclerView DiffUtil là gì?

DiffUtil tính diff giữa old list và new list → chỉ update items changed (insert, remove, move). Tương tự React reconciliation algorithm. Dùng AsyncListDiffer/ListAdapter cho async diff computation — không block UI thread.

Q10: Dispatchers.IO vs Dispatchers.Default?

IO: optimized cho I/O-bound work (network, DB, file) — thread pool size = max(64, number of cores). Default: optimized cho CPU-bound work (sort, parse) — thread pool = number of cores. Dùng sai dispatcher → block threads → app lag.

Q11: Coroutine error handling — structured concurrency?

Structured concurrency: child coroutine failure → cancel parent + siblings. Dùng supervisorScope để isolate failures. Error handling: try/catch trong coroutine, hoặc CoroutineExceptionHandler cho uncaught exceptions. viewModelScope auto-cancel khi ViewModel cleared.

Q12: Hilt @Inject vs @Provides — khác nhau?

@Inject: constructor injection — Hilt tự tạo instance (bạn own class). @Provides: factory method trong @Module — bạn tự tạo instance (third-party classes: Retrofit, Room, OkHttp mà bạn không own constructor).

🧩 Mini exercise thực tế React Native Android

Exercise 1: Debug permission camera
Thêm permission vào Manifest, request permission từ JS, kiểm tra Logcat khi permission bị deny.
Exercise 2: Deep link Android
Thêm intent-filter cho myapp://product/123, mở bằng adb, xác nhận React Navigation nhận đúng URL.
Exercise 3: Build variant
Tạo debug/staging/release base URL khác nhau bằng Gradle config hoặc env file, verify app đọc đúng variant.

⚠️ Lỗi thường gặp

🏢 Case đi làm thật

Case: Deep link chạy trên iOS nhưng Android không mở app. JS navigation không nhận được URL vì intent-filter thiếu BROWSABLE hoặc host/scheme không khớp.

Cách xử lý: Kiểm tra Manifest, chạy adb shell am start -W -a android.intent.action.VIEW -d "myapp://product/123", đọc Logcat và verify Linking listener ở JS.

🧠 Ghi nhớ nhanh

RN dev không cần thành Android engineer ngay, nhưng phải đọc được Gradle, Manifest, Logcat và Kotlin cơ bản để không bị kẹt ở native boundary.

❔ Câu hỏi tự kiểm tra

1. Khi app crash native trên Android, bạn mở log ở đâu?
2. Permission cần khai báo ở những lớp nào?
3. launch khác async thế nào?
4. Intent-filter deep link cần những category nào?

✅ Checklist tự đánh giá

📎 Tài nguyên

📘 Kotlin Official Docs
📘 Android Developer Courses (Google)
📘 ViewModel Guide
📘 Kotlin Coroutines Guide
📺 Philipp Lackner — YouTube, best Android/Kotlin tutorials
📺 Coding With Mitch — YouTube, Android architecture