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
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 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 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()
}
}
| Enum | Sealed Class | |
|---|---|---|
| State | Fixed set, no data per instance | Fixed set, EACH can hold different data |
| Instances | Singleton per value | Multiple instances per subclass |
| Use case | Simple constants (Status, Color) | Result types, UI states, Navigation events |
| Function | Object ref | Return | Use case |
|---|---|---|---|
let | it | Lambda result | Null check + transform |
run | this | Lambda result | Object config + compute result |
apply | this | Object itself | Object initialization (builder pattern) |
also | it | Object itself | Side effects (logging, validation) |
with | this | Lambda result | Grouping 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
}
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(", ")
| Component | Vai trò | Ví dụ |
|---|---|---|
| Activity | 1 screen/UI entry point | MainActivity, LoginActivity |
| Service | Background work (no UI) | Music player, sync service |
| BroadcastReceiver | Respond to system events | Battery low, network change |
| ContentProvider | Share data giữa apps | Contacts, MediaStore |
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).
| Layout | Mô tả | Khi nào |
|---|---|---|
ConstraintLayout | Flat hierarchy, constraint-based | Complex layouts (recommended) |
LinearLayout | Horizontal/Vertical stack | Simple lists, rows |
FrameLayout | Stack children on top | Overlays, 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() }
}
}
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.
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.
Kotlin coroutines = async/await của Kotlin. Lightweight threads cho concurrent operations.
launch | async | |
|---|---|---|
| Return | Job (fire and forget) | Deferred<T> (có result) |
| Use case | Side effects (update UI, save DB) | Parallel computation (cần return value) |
| Get result | Khô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()
}
| Dispatcher | Thread | Use case |
|---|---|---|
Main | UI thread | Update UI, LiveData |
IO | Background (shared pool) | Network, DB, file I/O |
Default | CPU-intensive (shared pool) | Sorting, parsing, heavy computation |
| StateFlow | SharedFlow | LiveData | |
|---|---|---|---|
| Initial value | Required | Optional | Optional |
| Replay | Always 1 (latest) | Configurable | Always 1 |
| Lifecycle-aware | No (need collectAsStateWithLifecycle) | No | Yes |
| Use case | UI state (Replace LiveData) | Events (snackbar, navigation) | Simple UI state (legacy) |
@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()
| File | Dùng khi | Lưu ý |
|---|---|---|
android/app/build.gradle | Dependency, signing, buildTypes, versionCode | Đổi native dependency thường cần clean/rebuild |
AndroidManifest.xml | Permissions, activity, deep link intent-filter | Sai intent-filter làm deep link không vào app |
MainActivity.kt | Activity entry point, splash, new architecture config | Tránh làm heavy work trong onCreate |
MainApplication.kt | Package/native module registration | Kiể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>
?. (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 nulllet: execute block chỉ khi non-null, biến scope function itequals()/hashCode() (compare by properties), toString() (readable output), copy() (shallow copy với optional property override), componentN() (destructuring). Chỉ dùng properties trong primary constructor.
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.
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.
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.
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.
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).
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).
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.
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.
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.
@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).
myapp://product/123, mở bằng adb, xác nhận React Navigation nhận đúng URL.
!! trong native module rồi crash khi value null từ JS truyền sang.android:exported khi target SDK mới, build/release fail.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.
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.
launch khác async thế nào?