1. Pengenalan Kotlin Coroutines
Kotlin Coroutines adalah solusi Kotlin untuk asynchronous programming. Coroutines memungkinkan Anda menulis kode asynchronous yang terlihat seperti kode synchronous biasa — tanpa callback nesting yang rumit (callback hell) dan tanpa kompleksitas reactive programming yang berlebihan.
Coroutines bukan sekadar "thread yang lebih ringan". Coroutines adalah komputasi yang bisa di-suspend (ditangguhkan) tanpa memblokir thread. Ketika coroutine menunggu hasil operasi (misalnya network request), thread yang menjalankannya bisa digunakan oleh coroutine lain — ini membuat resource utilization sangat efisien.
Mengapa Coroutines?
| Pendekatan | Kelebihan | Kekurangan |
|---|---|---|
| Callback | Simple untuk kasus sederhana | Callback hell, sulit dibaca, sulit di-debug |
| RxJava/Reactive | Operator kaya, backpressure | Kurva belajar curam, boilerplate, overhead |
| AsyncTask (Android) | Built-in di Android | Deprecated, memory leak, sulit compose |
| Coroutines | Sintaks bersih, ringan, structured concurrency | Perlu pemahaman konsep baru |
TANPA COROUTINES (Blocking): ┌──────────────────────────────────────┐ │ Thread 1: ████████████████████████████│ ← Tunggu network (blokir thread) │ Thread 2: ████████████████████████████│ ← Tunggu database (blokir thread) │ Thread 3: ░░░░░░░░░░░░░░░░░░░░░░░░░░│ ← Idle, tidak digunakan └──────────────────────────────────────┘ Setiap operasi async = 1 thread terblokir DENGAN COROUTINES (Non-blocking): ┌──────────────────────────────────────┐ │ Thread 1: ██░░██░░██░░██░░██░░██░░██│ ← Banyak coroutine share 1 thread │ A B A C B A D C A │ Coroutine di-suspend saat tunggu └──────────────────────────────────────┘ Thread tidak pernah terblokir — efisien!
Setup Gradle
// build.gradle.kts (Module: app)
dependencies {
// Coroutines Core
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
// Coroutines Android (untuk Dispatchers.Main)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
// Coroutines Test
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
// Lifecycle-aware coroutines (Android)
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
}
2. Suspend Functions
Suspend function adalah fungsi yang bisa menangguhkan eksekusinya tanpa memblokir thread, lalu melanjutkan dari titik terakhir saat hasil sudah tersedia. Suspend function hanya bisa dipanggil dari dalam coroutine atau dari suspend function lainnya.
import kotlinx.coroutines.*
// Suspend function sederhana
suspend fun fetchUserFromNetwork(userId: String): String {
delay(1000) // Simulasi network delay (tidak memblokir thread!)
return "User_$userId"
}
// Suspend function dengan beberapa langkah
suspend fun loadUserProfile(userId: String): Map<String, String> {
val name = fetchUserFromNetwork(userId) // Tunggu 1 detik
val email = fetchUserEmail(userId) // Tunggu 1 detik
val avatar = fetchUserAvatar(userId) // Tunggu 1 detik
return mapOf(
"name" to name,
"email" to email,
"avatar" to avatar
)
}
suspend fun fetchUserEmail(userId: String): String {
delay(1000)
return "$userId@example.com"
}
suspend fun fetchUserAvatar(userId: String): String {
delay(1000)
return "https://api.example.com/avatars/$userId.png"
}
// Harus dipanggil dari dalam coroutine:
fun main() = runBlocking {
val profile = loadUserProfile("budi123")
println(profile)
// Output: {name=User_budi123, email=budi123@example.com, avatar=https://...}
}
Suspend vs Regular Function
| Aspek | Regular Function | Suspend Function |
|---|---|---|
| Sintaks | fun nama() | suspend fun nama() |
| Blokir Thread? | Ya, jika ada operasi lambat | Tidak — di-suspend tanpa blokir |
| Bisa delay? | Tidak (kecuali Thread.sleep) | Ya (delay()) |
| Bisa panggil suspend lain? | Tidak | Ya |
| Pemanggilan | Dari mana saja | Hanya dari coroutine/suspend |
delay(1000) menangguhkan coroutine selama 1 detik tanpa memblokir thread — thread bisa menjalankan coroutine lain. Thread.sleep(1000) memblokir thread selama 1 detik. Selalu gunakan delay() di dalam coroutines.
3. Coroutine Scope & Builders
Coroutine Scope menentukan lifecycle dari coroutines yang dibuat di dalamnya. Setiap coroutine harus berjalan dalam scope tertentu. Scope memastikan structured concurrency — semua coroutine child akan dibatalkan ketika parent scope dibatalkan.
Coroutine Builders
| Builder | Mengembalikan | Penggunaan |
|---|---|---|
| launch | Job | Fire-and-forget, tidak perlu hasil |
| async | Deferred<T> | Perlu hasil return value |
| runBlocking | T | Blokir thread sampai selesai (testing/main) |
| withContext | T | Pindah dispatcher, tidak bikin coroutine baru |
| coroutineScope | T | Bikin child scope baru |
| supervisorScope | T | Seperti coroutineScope tapi failure tidak propagate |
import kotlinx.coroutines.*
fun main() = runBlocking {
// ===== LAUNCH =====
val job: Job = launch {
delay(1000)
println("Task selesai dari launch!")
}
println("Menunggu launch...")
job.join()
// ===== ASYNC =====
val deferred: Deferred<Int> = async {
delay(1000)
42 // Return value
}
val result = deferred.await()
println("Hasil async: $result")
// ===== CONCURRENT ASYNC =====
val startTime = System.currentTimeMillis()
val userDeferred = async { fetchUser() }
val postsDeferred = async { fetchPosts() }
val commentsDeferred = async { fetchComments() }
val user = userDeferred.await()
val posts = postsDeferred.await()
val comments = commentsDeferred.await()
val elapsed = System.currentTimeMillis() - startTime
println("$user | $posts | $comments")
println("Waktu: ${elapsed}ms") // ~1 detik, bukan 3 detik!
}
suspend fun fetchUser(): String { delay(1000); return "Budi Santoso" }
suspend fun fetchPosts(): String { delay(1000); return "15 postingan" }
suspend fun fetchComments(): String { delay(1000); return "128 komentar" }
Android Coroutine Scopes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.*
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_uiState.value = UserUiState.Loading
try {
val user = userRepository.getUser(userId)
_uiState.value = UserUiState.Success(user)
} catch (e: Exception) {
_uiState.value = UserUiState.Error(e.message ?: "Unknown error")
}
}
}
fun loadDashboard() {
viewModelScope.launch {
try {
val profile = async { api.getProfile() }
val orders = async { api.getOrders() }
val notifications = async { api.getNotifications() }
val dashboardData = DashboardData(
profile = profile.await(),
orders = orders.await(),
notifications = notifications.await()
)
_uiState.value = UserUiState.Dashboard(dashboardData)
} catch (e: Exception) {
_uiState.value = UserUiState.Error(e.message ?: "Gagal memuat")
}
}
}
}
class UserActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
}
sealed class UserUiState {
object Loading : UserUiState()
data class Success(val user: User) : UserUiState()
data class Error(val message: String) : UserUiState()
data class Dashboard(val data: DashboardData) : UserUiState()
}
4. launch & Job
launch adalah coroutine builder yang memulai coroutine baru tanpa mengembalikan hasil ke pemanggilnya (fire-and-forget). launch mengembalikan objek Job yang bisa digunakan untuk mengontrol lifecycle coroutine.
import kotlinx.coroutines.*
fun main() = runBlocking {
// LAUNCH DASAR
val job = launch {
repeat(5) { i ->
println("Coroutine: iterasi $i")
delay(500)
}
}
println("Job aktif: ${job.isActive}")
job.join()
println("Job selesai: ${job.isCompleted}")
// CANCEL JOB
val longJob = launch {
repeat(100) { i ->
println("Long task: $i")
delay(300)
}
}
delay(1000)
longJob.cancel()
longJob.join()
println("Long job dibatalkan: ${longJob.isCancelled}")
// JOB HIERARCHY
val parentJob = launch {
val child1 = launch {
repeat(3) { println("Child 1: $it"); delay(300) }
}
val child2 = launch {
repeat(3) { println("Child 2: $it"); delay(400) }
}
}
parentJob.join()
println("Semua child selesai!")
// CANCEL DENGAN TIMEOUT
try {
withTimeout(2000) {
repeat(100) { i ->
println("Timeout task: $i")
delay(300)
}
}
} catch (e: TimeoutCancellationException) {
println("Task dibatalkan karena timeout!")
}
// CHECK isActive
val cancellableJob = launch {
repeat(100) { i ->
if (!isActive) return@launch
println("Processing: $i")
delay(200)
}
}
delay(800)
cancellableJob.cancelAndJoin()
}
Pembatalan coroutine bersifat cooperative — coroutine harus secara aktif memeriksa pembatalan. Fungsi suspend bawaan seperti delay(), yield(), withContext() sudah mendukung pembatalan otomatis. Namun, jika coroutine menjalankan operasi blocking berat, Anda harus memeriksa isActive atau menggunakan ensureActive().
5. async & Deferred
async mirip dengan launch, tetapi mengembalikan hasil berupa Deferred<T> — sebuah "janji" yang bisa di-await() untuk mendapatkan nilai. Gunakan async saat Anda perlu mendapatkan hasil dari coroutine.
import kotlinx.coroutines.*
object ApiService {
suspend fun getUserProfile(userId: String): UserProfile {
delay(1000)
return UserProfile(userId, "Budi Santoso", "budi@example.com")
}
suspend fun getUserOrders(userId: String): List<Order> {
delay(1500)
return listOf(
Order("ORD-001", "iPhone 16", 18999000),
Order("ORD-002", "AirPods Pro", 3999000)
)
}
suspend fun getUserWishlist(userId: String): List<Product> {
delay(800)
return listOf(
Product("MacBook Air M3", 16999000),
Product("iPad Pro M4", 14999000)
)
}
}
data class UserProfile(val id: String, val name: String, val email: String)
data class Order(val id: String, val product: String, val price: Int)
data class Product(val name: String, val price: Int)
data class UserDashboard(
val profile: UserProfile,
val orders: List<Order>,
val wishlist: List<Product>
)
suspend fun loadDashboard(userId: String): UserDashboard = coroutineScope {
val profileDeferred = async { ApiService.getUserProfile(userId) }
val ordersDeferred = async { ApiService.getUserOrders(userId) }
val wishlistDeferred = async { ApiService.getUserWishlist(userId) }
UserDashboard(
profile = profileDeferred.await(),
orders = ordersDeferred.await(),
wishlist = wishlistDeferred.await()
)
}
fun main() = runBlocking {
val start = System.currentTimeMillis()
val dashboard = loadDashboard("budi123")
val elapsed = System.currentTimeMillis() - start
println("Profil: ${dashboard.profile.name}")
println("Pesanan: ${dashboard.orders.size}")
println("Waktu: ${elapsed}ms") // ~1500ms, bukan ~3300ms!
}
Kapan Pakai launch vs async?
| Situasi | Builder | Alasan |
|---|---|---|
| Update UI / logging | launch | Tidak perlu return value |
| Save to database | launch | Fire-and-forget |
| Fetch API & pakai hasilnya | async | Perlu return value |
| Parallel requests | async | Bisa di-await bersamaan |
| Background task dengan timeout | launch + withTimeout | Kontrol lifecycle |
6. withContext & Dispatchers
Dispatchers menentukan thread mana yang akan menjalankan coroutine. withContext memungkinkan Anda berpindah dispatcher di tengah eksekusi coroutine tanpa membuat coroutine baru — sangat penting untuk memastikan kode berjalan di thread yang tepat.
Dispatchers di Kotlin Coroutines
| Dispatcher | Thread | Penggunaan |
|---|---|---|
| Dispatchers.Main | Main/UI thread | Update UI, user interaction |
| Dispatchers.IO | Shared pool (64+ thread) | Network, file I/O, database |
| Dispatchers.Default | CPU-limited pool (= core count) | Kalkulasi berat, sorting, parsing |
| Dispatchers.Unconfined | Thread caller (lalu apa saja) | Testing, kasus khusus |
import kotlinx.coroutines.*
suspend fun fetchAndProcessData(): String {
// 1. Fetch data di IO thread
val rawData = withContext(Dispatchers.IO) {
println("Fetching on: ${Thread.currentThread().name}")
// Simulasi network call
delay(1000)
"""{"name": "Budi", "score": 95}"""
}
// 2. Parse data di Default thread (CPU-intensive)
val parsed = withContext(Dispatchers.Default) {
println("Parsing on: ${Thread.currentThread().name}")
// Simulasi parsing berat
rawData.uppercase()
}
// 3. Update UI di Main thread (Android)
// withContext(Dispatchers.Main) {
// textView.text = parsed
// }
return parsed
}
// Repository pattern dengan withContext
class UserRepository(
private val api: ApiService,
private val dao: UserDao
) {
suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
try {
// Coba fetch dari network
val users = api.fetchUsers()
// Simpan ke local database
withContext(Dispatchers.IO) {
dao.insertAll(users)
}
users
} catch (e: Exception) {
// Fallback ke local database
dao.getAllUsers()
}
}
suspend fun searchUsers(query: String): List<User> = withContext(Dispatchers.IO) {
val allUsers = dao.getAllUsers()
// Filtering di Default (CPU work)
withContext(Dispatchers.Default) {
allUsers.filter { user ->
user.name.contains(query, ignoreCase = true) ||
user.email.contains(query, ignoreCase = true)
}.sortedBy { it.name }
}
}
}
// Custom Dispatcher
val singleThreadDispatcher = newSingleThreadContext("MyThread")
val fixedThreadPool = newFixedThreadPoolContext(4, "MyPool")
fun main() = runBlocking {
launch(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name}")
}
launch(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name}")
}
launch(singleThreadDispatcher) {
println("Custom: ${Thread.currentThread().name}")
}
delay(1000)
singleThreadDispatcher.close()
}
Di Android, pola yang benar adalah: ViewModel menggunakan viewModelScope (Dispatchers.Main), Repository melakukan operasi IO dengan withContext(Dispatchers.IO). Jangan hardcode dispatcher di ViewModel — biarkan Repository yang menentukan dispatcher yang tepat. Ini juga memudahkan testing karena bisa di-inject test dispatcher.
7. Flow & Reactive Streams
Flow adalah solusi Kotlin untuk reactive programming — sebuah cold stream yang mengeluarkan multiple values secara sekuensial. Flow sangat cocok untuk mengamati perubahan data dari database, WebSocket, atau operasi berulang lainnya. Flow terintegrasi sempurna dengan coroutines.
Cold Stream vs Hot Stream
| Tipe | Karakteristik | Contoh |
|---|---|---|
| Cold (Flow) | Dimulai saat di-collect, setiap collector dapat stream sendiri | Database query, API call |
| Hot (SharedFlow/StateFlow) | Berjalan tanpa collector, semua collector share stream | UI events, user input, WebSocket |
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// ===== MEMBUAT FLOW =====
// Flow builder
fun simpleFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(300)
emit(i) // Mengeluarkan nilai
println("Emitted: $i")
}
}
// Flow dari collection
fun namesFlow(): Flow<String> = listOf("Budi", "Ani", "Citra", "Dodi").asFlow()
// FlowOf
fun numberFlow(): Flow<Int> = flowOf(1, 2, 3, 4, 5)
// ===== OPERATOR =====
fun main() = runBlocking {
// COLLECT — Mengambil semua nilai dari Flow
simpleFlow().collect { value ->
println("Received: $value")
}
// MAP — Transformasi nilai
namesFlow()
.map { it.uppercase() }
.collect { println(it) }
// FILTER — Filter nilai
simpleFlow()
.filter { it % 2 == 0 }
.collect { println("Even: $it") }
// REDUCE / FOLD — Aggregasi
val sum = simpleFlow().reduce { acc, value -> acc + value }
println("Sum: $sum") // 15
// COMBINE — Gabungkan beberapa Flow
val flow1 = flowOf(1, 2, 3)
val flow2 = flowOf("A", "B", "C")
flow1.combine(flow2) { num, letter -> "$num$letter" }
.collect { println(it) }
// FLATMAP — Nested flow
namesFlow()
.flatMapConcat { name ->
flow {
emit("$name - Loading...")
delay(500)
emit("$name - Loaded!")
}
}
.collect { println(it) }
// TRANSFORM — Operator fleksibel
simpleFlow()
.transform { value ->
emit("Processing $value")
emit("Done $value")
}
.collect { println(it) }
// BUFFER — Non-blocking buffer
simpleFlow()
.buffer() // Producer dan consumer berjalan concurrent
.collect {
delay(500) // Consumer lambat
println("Slow collect: $it")
}
// CONFLATE — Skip intermediate values
simpleFlow()
.conflate() // Skip nilai yang tertinggal
.collect {
delay(500)
println("Conflated: $it")
}
// CATCH — Error handling
flow {
emit(1)
emit(2)
throw RuntimeException("Error!")
emit(3) // Tidak tercapai
}
.catch { e -> println("Caught: ${e.message}") }
.collect { println(it) }
// ON_COMPLETION — Cleanup
simpleFlow()
.onCompletion { cause ->
if (cause == null) println("Flow selesai normal")
else println("Flow error: ${cause.message}")
}
.collect { println(it) }
// TERMINAL OPERATORS
val first = simpleFlow().first() // Ambil nilai pertama
val list = simpleFlow().toList() // Kumpulkan semua ke List
val count = simpleFlow().count() // Hitung jumlah
println("First: $first, Count: $count")
// TAKE — Batasi jumlah nilai
simpleFlow()
.take(3) // Ambil 3 nilai pertama saja
.collect { println("Take: $it") }
}
StateFlow & SharedFlow
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// ===== STATEFLOW =====
// Selalu memiliki nilai, replays nilai terbaru ke collector baru
class CounterViewModel {
private val _count = MutableStateFlow(0) // Private mutable
val count: StateFlow<Int> = _count.asStateFlow() // Public read-only
fun increment() { _count.value++ }
fun decrement() { _count.value-- }
fun reset() { _count.value = 0 }
}
// ===== SHAREDFLOW =====
// Tidak memiliki nilai awal, cocok untuk one-shot events
class EventManager {
private val _events = MutableSharedFlow<AppEvent>()
val events: SharedFlow<AppEvent> = _events.asSharedFlow()
suspend fun emitEvent(event: AppEvent) {
_events.emit(event)
}
}
sealed class AppEvent {
data class ShowToast(val message: String) : AppEvent()
data class Navigate(val route: String) : AppEvent()
object Logout : AppEvent()
}
// ===== CONTOH PENGGUNAAN =====
fun main() = runBlocking {
// StateFlow example
val counter = CounterViewModel()
// Collector 1
launch {
counter.count.collect { value ->
println("Collector 1: $value")
}
}
// Collector 2 (akan langsung mendapat nilai terbaru)
launch {
counter.count.collect { value ->
println("Collector 2: $value")
}
}
delay(100)
counter.increment() // Kedua collector menerima update
delay(100)
counter.increment()
delay(100)
counter.reset()
// SharedFlow example
val eventManager = EventManager()
launch {
eventManager.events.collect { event ->
when (event) {
is AppEvent.ShowToast -> println("Toast: ${event.message}")
is AppEvent.Navigate -> println("Navigate to: ${event.route}")
is AppEvent.Logout -> println("Logging out...")
}
}
}
delay(100)
eventManager.emitEvent(AppEvent.ShowToast("Selamat datang!"))
eventManager.emitEvent(AppEvent.Navigate("profile"))
eventManager.emitEvent(AppEvent.Logout)
delay(500)
cancelChildren()
}
// Flow di Repository pattern
class ProductRepository(
private val api: ProductApi,
private val dao: ProductDao
) {
// Observasi perubahan data dari database
fun observeProducts(): Flow<List<Product>> {
return dao.getAllProductsFlow()
.distinctUntilChanged() // Hanya emit jika berubah
}
// Refresh data dari network, simpan ke database
suspend fun refreshProducts() {
withContext(Dispatchers.IO) {
val products = api.fetchProducts()
dao.insertAll(products)
}
}
// Search dengan debounce
fun searchProducts(query: String): Flow<List<Product>> {
return flow {
val results = withContext(Dispatchers.IO) {
api.searchProducts(query)
}
emit(results)
}
}
}
8. Exception Handling
Exception handling di coroutines berbeda dari kode biasa karena sifatnya yang asynchronous dan structured. Coroutine yang gagal akan membatalkan parent scope-nya (structured concurrency), kecuali menggunakan supervisorScope.
import kotlinx.coroutines.*
fun main() = runBlocking {
// ===== try-catch biasa =====
val job = launch {
try {
val result = riskyOperation()
println("Result: $result")
} catch (e: Exception) {
println("Caught: ${e.message}")
}
}
job.join()
// ===== CoroutineExceptionHandler =====
val handler = CoroutineExceptionHandler { _, exception ->
println("Handler caught: ${exception.message}")
}
val safeJob = GlobalScope.launch(handler) {
throw RuntimeException("Something went wrong!")
}
safeJob.join()
// ===== async exception handling =====
val deferred = async {
throw RuntimeException("Async error!")
}
try {
deferred.await()
} catch (e: Exception) {
println("Async caught: ${e.message}")
}
// ===== supervisorScope =====
supervisorScope {
val child1 = launch {
delay(100)
throw RuntimeException("Child 1 gagal!")
}
val child2 = launch {
delay(300)
println("Child 2 berhasil!") // Tetap jalan meski child1 gagal
}
}
// ===== SupervisorJob =====
val supervisor = SupervisorJob()
val scope = CoroutineScope(supervisor + Dispatchers.Default)
scope.launch {
throw RuntimeException("Child A gagal!")
}
scope.launch {
delay(100)
println("Child B berhasil! (tidak terpengaruh)")
}
delay(500)
supervisor.cancel()
println("Selesai")
}
suspend fun riskyOperation(): String {
delay(100)
if (Math.random() < 0.5) throw RuntimeException("Random failure!")
return "Success!"
}
9. Best Practices & Pola Umum
| Best Practice | Penjelasan |
|---|---|
| Structured Concurrency | Selalu gunakan scope yang tepat (viewModelScope, lifecycleScope), hindari GlobalScope |
| Injection Dispatcher | Inject dispatcher ke Repository untuk kemudahan testing |
| Error Handling | Selalu tangkap exception — coroutine yang unhandled bisa crash app |
| withContext | Gunakan untuk switch dispatcher, bukan launch baru |
| Flow vs Callback | Gunakan Flow untuk data yang berubah-ubah, suspend untuk one-shot |
| StateFlow vs LiveData | StateFlow lebih powerful dan multiplatform, lebih disarankan untuk proyek baru |
| Testing | Gunakan runTest dan TestDispatcher untuk unit testing coroutines |
| Avoid blocking | Hindari Thread.sleep(), runBlocking di Android code (kecuali main) |
import kotlinx.coroutines.test.*
import org.junit.Test
class UserViewModelTest {
@Test
fun `loadUser should update state to Success`() = runTest {
// Arrange
val fakeApi = FakeApiService()
val viewModel = UserViewModel(fakeApi)
// Act
viewModel.loadUser("test123")
// Assert
val state = viewModel.uiState.value
assert(state is UserUiState.Success)
assert((state as UserUiState.Success).user.name == "Test User")
}
@Test
fun `loadUser should update state to Error on failure`() = runTest {
val fakeApi = FakeApiService(shouldFail = true)
val viewModel = UserViewModel(fakeApi)
viewModel.loadUser("test123")
val state = viewModel.uiState.value
assert(state is UserUiState.Error)
}
}
Arsitektur MVVM + Coroutines yang disarankan: ViewModel menggunakan viewModelScope.launch, Repository menggunakan withContext(Dispatchers.IO) untuk operasi network/database, UseCase untuk business logic, dan StateFlow untuk expose UI state. Gunakan sealed class untuk UI state representation.
10. Quiz Pemahaman
1. Apa yang terjadi saat coroutine di-suspend?
2. Kapan menggunakan async dibanding launch?
3. Apa perbedaan StateFlow dan SharedFlow?
4. Apa yang dilakukan withContext(Dispatchers.IO)?
5. Mengapa harus menghindari GlobalScope di Android?
Anda telah mempelajari Kotlin Coroutines secara mendalam — dari suspend functions, coroutine builders, dispatchers, Flow, hingga exception handling dan best practices. Coroutines adalah fondasi untuk async programming modern di Android dan Kotlin backend.