Mobile

Kotlin Coroutines: Async Programming

TOKEN

Panduan lengkap async programming dengan Kotlin Coroutines — dari launch, async, withContext, Flow, hingga structured concurrency dan best practices

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?

PendekatanKelebihanKekurangan
CallbackSimple untuk kasus sederhanaCallback hell, sulit dibaca, sulit di-debug
RxJava/ReactiveOperator kaya, backpressureKurva belajar curam, boilerplate, overhead
AsyncTask (Android)Built-in di AndroidDeprecated, memory leak, sulit compose
CoroutinesSintaks bersih, ringan, structured concurrencyPerlu pemahaman konsep baru
Diagram: Coroutines vs Thread
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

Kotlin — build.gradle.kts
// 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.

Kotlin — Suspend Functions
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

AspekRegular FunctionSuspend Function
Sintaksfun nama()suspend fun nama()
Blokir Thread?Ya, jika ada operasi lambatTidak — di-suspend tanpa blokir
Bisa delay?Tidak (kecuali Thread.sleep)Ya (delay())
Bisa panggil suspend lain?TidakYa
PemanggilanDari mana sajaHanya dari coroutine/suspend
💡 delay() vs Thread.sleep()

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

BuilderMengembalikanPenggunaan
launchJobFire-and-forget, tidak perlu hasil
asyncDeferred<T>Perlu hasil return value
runBlockingTBlokir thread sampai selesai (testing/main)
withContextTPindah dispatcher, tidak bikin coroutine baru
coroutineScopeTBikin child scope baru
supervisorScopeTSeperti coroutineScope tapi failure tidak propagate
Kotlin — Coroutine Builders
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

Kotlin — Android 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.

Kotlin — launch & Job
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()
}
⚠️ Cancellation Cooperative

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.

Kotlin — async & Deferred
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?

SituasiBuilderAlasan
Update UI / logginglaunchTidak perlu return value
Save to databaselaunchFire-and-forget
Fetch API & pakai hasilnyaasyncPerlu return value
Parallel requestsasyncBisa di-await bersamaan
Background task dengan timeoutlaunch + withTimeoutKontrol 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

DispatcherThreadPenggunaan
Dispatchers.MainMain/UI threadUpdate UI, user interaction
Dispatchers.IOShared pool (64+ thread)Network, file I/O, database
Dispatchers.DefaultCPU-limited pool (= core count)Kalkulasi berat, sorting, parsing
Dispatchers.UnconfinedThread caller (lalu apa saja)Testing, kasus khusus
Kotlin — withContext & Dispatchers
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()
}
💡 Pola Repository yang Benar

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

TipeKarakteristikContoh
Cold (Flow)Dimulai saat di-collect, setiap collector dapat stream sendiriDatabase query, API call
Hot (SharedFlow/StateFlow)Berjalan tanpa collector, semua collector share streamUI events, user input, WebSocket
Kotlin — Flow Basics
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

Kotlin — 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.

Kotlin — Exception Handling
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 PracticePenjelasan
Structured ConcurrencySelalu gunakan scope yang tepat (viewModelScope, lifecycleScope), hindari GlobalScope
Injection DispatcherInject dispatcher ke Repository untuk kemudahan testing
Error HandlingSelalu tangkap exception — coroutine yang unhandled bisa crash app
withContextGunakan untuk switch dispatcher, bukan launch baru
Flow vs CallbackGunakan Flow untuk data yang berubah-ubah, suspend untuk one-shot
StateFlow vs LiveDataStateFlow lebih powerful dan multiplatform, lebih disarankan untuk proyek baru
TestingGunakan runTest dan TestDispatcher untuk unit testing coroutines
Avoid blockingHindari Thread.sleep(), runBlocking di Android code (kecuali main)
Kotlin — Testing Coroutines
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)
    }
}
🎉 Pola Arsitektur yang Disarankan

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?

🎉 Selamat!

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.

🔍 Zoom
100%
🎨 Tema