Câu hỏi phỏng vấn Coroutines & Flow
Phần này tổng hợp các câu hỏi về Kotlin Coroutines và Flow - nền tảng async programming trong Android.
1. Coroutines Basics
Q: Coroutine là gì? Khác gì Thread?
Trả lời:
Coroutine là lightweight concurrent unit có thể suspend và resume mà không block thread.
| Aspect | Thread | Coroutine |
|---|---|---|
| Weight | Heavy (~1MB stack) | Lightweight (~few KB) |
| Context Switch | OS-level, expensive | User-level, cheap |
| Blocking | Blocks thread | Suspends, frees thread |
| Quantity | Limited (hundreds) | Many (millions) |
| Creation | Expensive | Cheap |
// Thread - blocks
Thread {
val result = networkCall() // Blocks this thread
runOnUiThread { updateUI(result) }
}.start()
// Coroutine - suspends
viewModelScope.launch {
val result = networkCall() // Suspends, doesn't block
updateUI(result) // Runs on Main after suspension
}
// Can launch millions of coroutines
repeat(100_000) {
launch {
delay(1000)
print(".")
}
}📚 Tìm hiểu thêm: Coroutines trong Android
Q: suspend function là gì?
Trả lời:
suspend function là function có thể pause và resume execution mà không block thread.
// suspend function can only be called from coroutine or another suspend function
suspend fun fetchUser(id: String): User {
delay(1000) // Built-in suspend function
return api.getUser(id) // Another suspend function
}
// ❌ Cannot call from regular function
fun regularFunction() {
fetchUser("123") // Compile error!
}
// ✅ Must call from coroutine
viewModelScope.launch {
val user = fetchUser("123") // OK
}
// ✅ Or from another suspend function
suspend fun loadData() {
val user = fetchUser("123") // OK
}Internal: Compiler transforms suspend functions to use Continuation (callbacks) internally.
Q: Dispatchers là gì? Có bao nhiêu loại?
Trả lời:
Dispatcher xác định thread nào coroutine chạy trên.
| Dispatcher | Thread Pool | Use Case |
|---|---|---|
Dispatchers.Main | Main/UI thread | UI updates |
Dispatchers.IO | Shared pool (64+ threads) | Network, disk I/O |
Dispatchers.Default | CPU cores | CPU-intensive work |
Dispatchers.Unconfined | Current thread | Testing, special cases |
viewModelScope.launch(Dispatchers.Main) {
showLoading()
val data = withContext(Dispatchers.IO) {
api.fetchData() // Runs on IO thread
}
val processed = withContext(Dispatchers.Default) {
heavyComputation(data) // Runs on Default
}
hideLoading()
updateUI(processed) // Runs on Main
}💡 Mẹo:
viewModelScope.launchmặc định dùngDispatchers.Main.immediate
2. Coroutine Scope
Q: Các loại CoroutineScope trong Android?
Trả lời:
| Scope | Lifecycle | Use Case |
|---|---|---|
viewModelScope | ViewModel | UI-related async work |
lifecycleScope | Activity/Fragment | Lifecycle-aware work |
GlobalScope | App process | ❌ Avoid! No lifecycle |
CoroutineScope() | Custom | Custom lifecycle management |
// ViewModel - auto-cancelled when ViewModel cleared
class UserViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
val data = repository.getData()
_state.value = data
}
}
}
// Activity/Fragment - auto-cancelled when lifecycle ends
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Cancelled when Activity destroyed
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { updateUI(it) }
}
}
}
}
// ❌ GlobalScope - never use!
GlobalScope.launch {
// Never cancelled, can cause memory leaks
}Q: Job và SupervisorJob khác nhau thế nào?
Trả lời:
| Type | Child Failure | Parent Failure |
|---|---|---|
| Job | Cancels parent + siblings | Cancels children |
| SupervisorJob | Doesn’t affect siblings | Cancels children |
// Job - one failure cancels all
val job = Job()
val scope = CoroutineScope(job + Dispatchers.Default)
scope.launch { task1() }
scope.launch { throw Exception() } // Cancels task1 too!
scope.launch { task3() } // Also cancelled
// SupervisorJob - failures isolated
val supervisorJob = SupervisorJob()
val scope = CoroutineScope(supervisorJob + Dispatchers.Default)
scope.launch { task1() } // Continues
scope.launch { throw Exception() } // Only this fails
scope.launch { task3() } // Continues
// supervisorScope block
viewModelScope.launch {
supervisorScope {
launch { task1() } // Independent
launch { throw Exception() } // Fails alone
launch { task3() } // Continues
}
}Use SupervisorJob when:
- Multiple independent tasks
- One failure shouldn’t affect others
- viewModelScope uses SupervisorJob by default
3. Structured Concurrency
Q: Structured Concurrency là gì?
Trả lời:
Structured Concurrency đảm bảo:
- Parent waits for all children to complete
- Cancellation propagates to children
- Errors propagate to parent
suspend fun loadDashboard(): Dashboard = coroutineScope {
// Both run concurrently
val user = async { api.getUser() }
val posts = async { api.getPosts() }
// Parent waits for both
Dashboard(user.await(), posts.await())
} // This line only reached when both complete
// Cancellation propagates
val job = viewModelScope.launch {
coroutineScope {
launch { longTask1() } // Cancelled
launch { longTask2() } // Cancelled
}
}
job.cancel() // Cancels all childrenQ: launch vs async?
Trả lời:
| Builder | Return | Use Case |
|---|---|---|
launch | Job | Fire-and-forget |
async | Deferred<T> | Need result |
viewModelScope.launch {
// launch - fire and forget
launch { analytics.trackEvent() }
// async - concurrent with result
val user = async { api.getUser() }
val posts = async { api.getPosts() }
// Await results
val data = CombinedData(user.await(), posts.await())
updateUI(data)
}
// Sequential vs Concurrent
suspend fun sequential() {
val a = api.callA() // Wait
val b = api.callB() // Then this
// Total: timeA + timeB
}
suspend fun concurrent() = coroutineScope {
val a = async { api.callA() } // Start
val b = async { api.callB() } // Start immediately
a.await() to b.await()
// Total: max(timeA, timeB)
}4. Error Handling
Q: Xử lý exceptions trong Coroutines?
Trả lời:
1. try-catch in suspend function:
viewModelScope.launch {
try {
val data = api.fetchData()
_state.value = UiState.Success(data)
} catch (e: Exception) {
_state.value = UiState.Error(e.message)
}
}2. CoroutineExceptionHandler:
val handler = CoroutineExceptionHandler { _, exception ->
Log.e("Error", "Caught: ${exception.message}")
_state.value = UiState.Error(exception.message)
}
viewModelScope.launch(handler) {
api.fetchData() // Exception handled by handler
}3. supervisorScope for independent tasks:
supervisorScope {
launch(handler) { riskyTask1() } // Fails independently
launch(handler) { riskyTask2() } // Continues
}4. runCatching for Result:
suspend fun loadUser(id: String): Result<User> = runCatching {
api.getUser(id)
}
// Usage
viewModelScope.launch {
loadUser("123")
.onSuccess { _user.value = it }
.onFailure { _error.value = it.message }
}Q: CancellationException là gì?
Trả lời:
CancellationException là special exception cho coroutine cancellation - không phải lỗi.
viewModelScope.launch {
try {
longRunningTask()
} catch (e: CancellationException) {
// ❌ Don't catch! Let it propagate
throw e
} catch (e: Exception) {
// Handle other errors
}
}
// ✅ Better pattern
viewModelScope.launch {
try {
longRunningTask()
} catch (e: Exception) {
if (e is CancellationException) throw e
handleError(e)
}
}
// Or use runCatching carefully
runCatching { suspendFunction() }
.onSuccess { }
.onFailure { if (it is CancellationException) throw it }⚠️ Quan trọng: Luôn re-throw
CancellationExceptionđể không chặn cancellation propagation!
5. Flow
Q: Flow là gì? Khác gì LiveData?
Trả lời:
Flow là cold asynchronous stream cho Kotlin, phù hợp cho reactive programming.
| Aspect | LiveData | Flow |
|---|---|---|
| Origins | Android | Kotlin (multiplatform) |
| Lifecycle | Built-in | Manual with operators |
| Operators | Limited | Rich (map, filter, combine) |
| Cold/Hot | Hot | Cold by default |
| Testing | Harder | Easier |
// LiveData
class ViewModel : ViewModel() {
private val _data = MutableLiveData<List<Item>>()
val data: LiveData<List<Item>> = _data
}
// Flow (Recommended)
class ViewModel : ViewModel() {
val data: StateFlow<List<Item>> = repository.getItems()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
// Collect in Compose
@Composable
fun ItemList(viewModel: ViewModel) {
val items by viewModel.data.collectAsStateWithLifecycle()
LazyColumn {
items(items) { ItemRow(it) }
}
}📚 Tìm hiểu thêm: Flow trong Android
Q: StateFlow vs SharedFlow?
Trả lời:
| Aspect | StateFlow | SharedFlow |
|---|---|---|
| Initial Value | Required | Optional |
| Replays | Latest value (1) | Configurable |
| Equality | Skips duplicate | Doesn’t skip |
| Use Case | UI state | Events |
class ViewModel : ViewModel() {
// StateFlow - for UI state
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// SharedFlow - for one-time events
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
fun showMessage(text: String) {
viewModelScope.launch {
_events.emit(Event.ShowSnackbar(text))
}
}
}
// Collect events
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is Event.ShowSnackbar -> snackbarHostState.showSnackbar(event.text)
is Event.Navigate -> navController.navigate(event.route)
}
}
}Q: Flow operators thường dùng?
Trả lời:
// Transform
repository.getUsers()
.map { users -> users.filter { it.isActive } }
.filter { it.isNotEmpty() }
// Combine multiple flows
combine(userFlow, settingsFlow) { user, settings ->
ProfileState(user, settings)
}
// FlatMap
searchQuery
.debounce(300) // Wait 300ms after last change
.distinctUntilChanged() // Skip if same
.flatMapLatest { query -> // Cancel previous search
repository.search(query)
}
// Error handling
flow
.catch { e -> emit(emptyList()) } // Handle upstream errors
.onEach { result -> /* process */ }
.launchIn(viewModelScope)
// Retry
repository.fetchData()
.retry(3) { e -> e is IOException }
.catch { emit(cachedData) }6. Best Practices
Q: collectAsStateWithLifecycle vs collectAsState?
Trả lời:
| Function | Lifecycle-aware | Use Case |
|---|---|---|
collectAsState | ❌ | Quick testing |
collectAsStateWithLifecycle | ✅ | Production |
// ❌ Continues collecting in background
@Composable
fun BadScreen(viewModel: ViewModel) {
val state by viewModel.flow.collectAsState()
}
// ✅ Stops when lifecycle is less than STARTED
@Composable
fun GoodScreen(viewModel: ViewModel) {
val state by viewModel.flow.collectAsStateWithLifecycle()
}Q: Testing Coroutines?
Trả lời:
@OptIn(ExperimentalCoroutinesApi::class)
class ViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun `load data success`() = runTest {
// Given
val repository = FakeRepository()
val viewModel = UserViewModel(repository)
// When
viewModel.loadData()
// Then
assertEquals(UiState.Success(data), viewModel.state.value)
}
}
// Main dispatcher rule
class MainDispatcherRule(
private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}📝 Quick Reference
// Scope
viewModelScope.launch { } // ViewModel
lifecycleScope.launch { } // Activity/Fragment
// Dispatchers
withContext(Dispatchers.IO) { } // I/O
withContext(Dispatchers.Default) { } // CPU
// Builders
launch { } // Fire-and-forget
async { }.await() // Get result
// Flow
flow.collect { } // Terminal operator
flow.collectAsStateWithLifecycle() // In Compose
flow.stateIn(scope, started, initial) // Convert to StateFlow
// Error handling
try { } catch (e: Exception) { if (e is CancellationException) throw e }
runCatching { }.onSuccess { }.onFailure { }📚 Tìm hiểu thêm: Coroutines, Flow