Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Kotlin Coroutine Mechanisms: Droidcon NYC 2024

mvndy_hd
September 20, 2024

Kotlin Coroutine Mechanisms: Droidcon NYC 2024

Sometimes you think you know coroutines and then after a while, you’re like “wait, do I really know coroutines?” . Inspired from "Programming Android with Kotlin: Achieving Structured Concurrency with Coroutines", this talks strengthens everyday coroutine understanding through playful explorations. We [the authors] always had sincere intentions with the book:

"While [coroutine] concepts are important if you want to master coroutines, you don’t have to understand everything right now to get started and be productive with coroutines." — Chapter 9: Coroutine concepts p. 127

You might be in beginning stages of learning Kotlin. Or maybe you’ve been using coroutines for a while and want to brush up, maybe you're a little burned from other talks: either way, you'll be looking at coroutines a little different by the end of this session!

mvndy_hd

September 20, 2024
Tweet

More Decks by mvndy_hd

Other Decks in Technology

Transcript

  1. Amanda Hinchman-Dominguez Android Developer, Kotlin GDE, Co-author of O'Reilly's "Programming

    Android with Kotlin: Achieving Structured Concurrency with Coroutines" Introduction to coroutine behavior through playful examples Droidcon NYC 2024 Kotlin Coroutine Mechanisms
  2. "While [coroutine] concepts are important [to master coroutines], you don’t

    have to understand everything right now to get started and be productive" - Chapter 9: Coroutine Concepts p. 127
  3. playground rules 1. We're in the recreation league: we probably

    will be doing things we're not supposed to do 2. Results of running coroutines will vary from platform and compiler
  4. playground setup 1. fun log(msg: String) = println("$msg | ${Thread.currentThread().name}")

    2. -Dkotlinx.coroutines.debug in VM options in "Edit Configurations"
  5. topics • coroutine behaviors a. runBlocking { ... } b.

    launch { ... } c. async {... } • swapping coroutine context a. CoroutineScope b. standard coroutine builders c. withContext
  6. runBlocking { .. } = T • runBlocking {...} starts

    a new coroutine that blocks and interrupts current thread until completion • Should not be used within a coroutine
  7. fun main() = runBlocking { log("main runBlocking") val task1 =

    runBlocking { log(" task1 runBlocking") // simulate a background task delay(1000) log(" task1 complete ") } val task2 = runBlocking { log(" task2 runBlocking") // simulate a background task delay(1000) log(" task2 complete") } log("Program ends") } #1 an impractical example runBlocking #dcnyc24 @mvndy_hd
  8. main runBlocking | main @coroutine#1 task1 runBlocking | main @coroutine#2

    task1 complete task2 runBlocking | main @coroutine#3 task2 complete Program ends | main @coroutine#1 fun main() = runBlocking { log("main runBlocking") val task1 = runBlocking { log(" task1 runBlocking") // simulate a background task delay(1000) log(" task1 complete ") } val task2 = runBlocking { log(" task2 runBlocking") // simulate a background task delay(1000) log(" task2 complete") } log("Program ends") } #1 an impractical example runBlocking #dcnyc24 @mvndy_hd
  9. • launch {...} starts a new coroutine and creates a

    Job instance. ◦ Does not return a result, just the reference to the background Job • A Job is able to: ◦ cancel() on its reference ◦ join() to force current thread to wait for completion of Job launch { .. } = Job
  10. fun main() = runBlocking { log("main runBlocking") val job =

    launch { log("job launched") val task1 = runBlocking { log(" task1") // simulate a background task delay(1000) log(" task1 complete ") } val task2 = runBlocking { log(" task2") // simulate a background task delay(1000) log(" task2 complete") } } log("Start job") log("Program ends") } main runBlocking | main @coroutine#1 task1 runBlocking | main @coroutine#2 task1 complete task2 runBlocking | main @coroutine#3 task2 complete Program ends | main @coroutine#1 launch #2 wrapping runBlocking in a launch #dcnyc24 @mvndy_hd
  11. main runBlocking | main @coroutine#1 Start job | main @coroutine#1

    Program ends | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task1 complete | main @coroutine#3 task2 | main @coroutine#4 task2 complete | main @coroutine#4 fun main() = runBlocking { log("main runBlocking") val job = launch { log("job launched") val task1 = runBlocking { log(" task1") // simulate a background task delay(1000) log(" task1 complete ") } val task2 = runBlocking { log(" task2") // simulate a background task delay(1000) log(" task2 complete") } } log("Start job") log("Program ends") } launch #2 wrapping runBlocking in a launch #dcnyc24 @mvndy_hd
  12. main runBlocking | main @coroutine#1 Start job | main @coroutine#1

    Program ends | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task1 complete | main @coroutine#3 task2 | main @coroutine#4 task2 complete | main @coroutine#4 fun main() = runBlocking { log("main runBlocking") val job = launch { log("job launched") val task1 = runBlocking { log(" task1") // simulate a background task delay(1000) log(" task1 complete ") } val task2 = runBlocking { log(" task2") // simulate a background task delay(1000) log(" task2 complete") } } log("Start job") log("Program ends") } #2 wrapping runBlocking in a launch launch #dcnyc24 @mvndy_hd
  13. fun main() = runBlocking { log("main runBlocking") val job =

    launch { log("job launched") val task1 = runBlocking { log(" task1") // simulate a background task delay(1000) log(" task1 complete ") } val task2 = runBlocking { log(" task2") // simulate a background task delay(1000) log(" task2 complete") } } log("Start job") job.join() log("Program ends") } main runBlocking | main @coroutine#1 Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task1 complete | main @coroutine#3 task2 | main @coroutine#4 task2 complete | main @coroutine#4 Program ends | main @coroutine#1 #3 job.join() to wait on completion launch Just as a reference to a job can be cancelled, we can also call join() to wait for job completion #dcnyc24 @mvndy_hd
  14. main runBlocking | main @coroutine#1 Start job | main @coroutine#1

    job launched | main @coroutine#2 task1 | main @coroutine#3 task1 complete | main @coroutine#3 task2 | main @coroutine#4 task2 complete | main @coroutine#4 Program ends | main @coroutine#1 launch fun main() = runBlocking { log("main runBlocking") val job = launch { val task1 = runBlocking { log(" task1") // simulate a background task delay(1000) log(" task1 complete ") } val task2 = runBlocking { log(" task2") // simulate a background task delay(1000) log(" task2 complete") } } log("Start job") job.join() log("Program ends") } #3 job.join() to wait on completion #dcnyc24 @mvndy_hd
  15. fun main() = runBlocking { log("main runBlocking") val job =

    launch { val task1 = runBlocking { log(" task1") // simulate a background task delay(1000) log(" task1 complete ") } val task2 = runBlocking { log(" task2") // simulate a background task delay(1000) log(" task2 complete") } } log("Start job") job.join() log("Program ends") } launch #4 runBlocking -> launch main runBlocking | main @coroutine#1 Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task1 complete | main @coroutine#3 task2 | main @coroutine#4 task2 complete | main @coroutine#4 Program ends | main @coroutine#1 #dcnyc24 @mvndy_hd
  16. fun main() = runBlocking { log("main runBlocking") val job =

    launch { val task1 = launch { log(" task1") // simulate a background task delay(1000) log(" task1 complete ") } val task2 = runBlocking { log(" task2") // simulate a background task delay(1000) log(" task2 complete") } } log("Start job") job.join() log("Program ends") } launch #4 runBlocking -> launch main runBlocking | main @coroutine#1 Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task1 complete | main @coroutine#3 task2 | main @coroutine#4 task2 complete | main @coroutine#4 Program ends | main @coroutine#1 #dcnyc24 @mvndy_hd
  17. fun main() = runBlocking { log("main runBlocking") val job =

    launch { val task1 = launch { log(" task1") // simulate a background task delay(1000) log(" task1 complete ") } val task2 = runBlocking { log(" task2") // simulate a background task delay(1000) log(" task2 complete") } } log("Start job") job.join() log("Program ends") } runBlocking main | main @coroutine#1 Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task2 | main @coroutine#4 task1 complete | main @coroutine#3 task2 complete | main @coroutine#4 Program ends | main @coroutine#1 #3 launch runBlocking -> launch #dcnyc24 @mvndy_hd
  18. fun main() = runBlocking { log("main runBlocking") val job =

    launch { val task1 = launch { log(" task1") // simulate a background task delay(1000) log(" task1 complete ") } val task2 = runBlocking { log(" task2") // simulate a background task delay(1000) log(" task2 complete") } } log("Start job") job.join() log("Program ends") } runBlocking main | main @coroutine#1 Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task2 | main @coroutine#4 task1 complete | main @coroutine#3 task2 complete | main @coroutine#4 Program ends | main @coroutine#1 #3 launch runBlocking -> launch #dcnyc24 @mvndy_hd
  19. #4 launch launch 3x fun main() = runBlocking { val

    job = launch { val task1 = launch { log(" task1") delay(1000) log(" task1 complete ") } val task2 = launch { log(" task2") delay(1000) log(" task2 complete") } val task3 = launch { log(" task3") delay(1000) log(" task3 complete") } } log("Start job") job.join() log("Program ends") } runBlocking main | main @coroutine#1 Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task2 | main @coroutine#4 task1 complete | main @coroutine#3 task2 complete | main @coroutine#4 Program ends | main @coroutine#1 #dcnyc24 @mvndy_hd
  20. fun main() = runBlocking { val job = launch {

    val task1 = launch { log(" task1") delay(1000) log(" task1 complete ") } val task2 = launch { log(" task2") delay(1000) log(" task2 complete") } val task3 = launch { log(" task3") delay(1000) log(" task3 complete") } } log("Start job") job.join() log("Program ends") } #4 launch launch 3x Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task2 | main @coroutine#4 task3 | main @coroutine#5 task1 complete | main @coroutine#3 task2 complete | main @coroutine#4 task3 complete | main @coroutine#5 Program ends | main @coroutine#1 #dcnyc24 @mvndy_hd
  21. #5 launch launch 3x and join fun main() = runBlocking

    { val job = launch { val task1 = launch { log(" task1") delay(1000) log(" task1 complete ") } val task2 = launch { log(" task2") delay(1000) log(" task2 complete") } task1.join() // <---- task2.join() // <---- val task3 = launch { log(" task3") delay(1000) log(" task3 complete") } } log("Start job") job.join() log("Program ends") } Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task2 | main @coroutine#4 task3 | main @coroutine#5 task1 complete | main @coroutine#3 task2 complete | main @coroutine#4 task3 complete | main @coroutine#5 Program ends | main @coroutine#1
  22. #5 launch launch 3x and join fun main() = runBlocking

    { val job = launch { log("Job launched") val task1 = launch { log(" task1") delay(1000) log(" task1 complete ") } val task2 = launch { log(" task2") delay(1000) log(" task2 complete") } task1.join() // <---- task2.join() // <---- val task3 = launch { log(" task3") delay(1000) log(" task3 complete") } } log("Start job") job.join() log("Program ends") } Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task2 | main @coroutine#4 task1 complete | main @coroutine#3 task2 complete | main @coroutine#4 task3 | main @coroutine#5 task3 complete | main @coroutine#5 Program ends | main @coroutine#1
  23. • Deferment is to put off an action/event for a

    later time; to postpone • async {...} starts a new coroutine and creates a Deferred instance ◦ return value T on completion ◦ Deferred<T> is also a Job type • Deferred::await( ) wait for completion of itself async { .. } = Deferred<T>
  24. #5 join() v. await() fun main() = runBlocking { val

    job = launch { val task1 = launch { log(" task1") delay(1000) log(" task1 complete ") } val task2 = launch { log(" task2") delay(1000) log(" task2 complete") } task1.join() task2.join() val task3 = launch { log(" task3") delay(1000) log(" task3 complete") } } log("Start job") job.join() log("Program ends") } Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task2 | main @coroutine#4 task1 complete | main @coroutine#3 task2 complete | main @coroutine#4 task3 | main @coroutine#5 task3 complete | main @coroutine#5 Program ends | main @coroutine#1 async
  25. #5 join() v. await() fun main() = runBlocking { val

    job = launch { val task1 = launch { log(" task1") delay(1000) log(" task1 complete ") } val task2: Deferred<String> = async { log(" task2") delay(1000) " task2 complete" } task1.join() val task3 = launch { log(" task3") delay(1000) log(" task3 complete") } log(" "task2 status: $task2") log(task2.await()) log(" "task2 status: $task2") } log("Start job") job.join() log("Program ends") Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task2 | main @coroutine#4 task1 complete | main @coroutine#3 task2 complete | main @coroutine#4 task3 | main @coroutine#5 task3 complete | main @coroutine#5 Program ends | main @coroutine#1 async
  26. #5 join() v. await() fun main() = runBlocking { val

    job = launch { val task1 = launch { log(" task1") delay(1000) log(" task1 complete ") } val task2: Deferred<String> = async { log(" task2") delay(1000) " task2 complete" } task1.join() val task3 = launch { log(" task3") delay(1000) log(" task3 complete") } log(" task2 status: $task2") log(task2.await()) log(" task2 status: $task2") } log("Start job") job.join() log("Program ends") async Start job | main @coroutine#1 job launched | main @coroutine#2 task1 | main @coroutine#3 task2 | main @coroutine#4 task1 complete | main @coroutine#3 task2 status | Deferred{Active}@58.. task3 | main @coroutine#5 task2 complete | main @coroutine#4 task2 status | Deferred{Completed}@58.. task3 complete | main @coroutine#5 Program ends | main @coroutine#1
  27. CoroutineContext • Designates what task executes on which thread /

    thread pool • Changing context alters behavior or concurrency • Three ways to use CoroutineContext: ◦ CoroutineScope(context) ◦ launch(context) ◦ withContext(context)
  28. class MainActivityViewModel( private val contextPool: CoroutineContextProvider = ..., ): ViewModel()

    { private val scope = CoroutineScope(contextPool.defaultDispatcher) private val _viewState = MutableLiveData<MainViewState>(MainViewState.Loading) val viewState: LiveData<MainViewState> = _viewState fun loadContent() { viewModelScope.launch { log("viewModel launched") scope.launch { log("scope launched") when(val result = doHeavyWork()) { is Result.Success -> {...} is Result.Failure -> {...} } } } } ... } CoroutineScope(context) #6 One ViewModel, Two Scopes
  29. class MainActivityViewModel( private val contextPool: CoroutineContextProvider = ..., ): ViewModel()

    { private val scope = CoroutineScope(contextPool.defaultDispatcher) ... fun loadContent() { viewModelScope.launch { log("viewModel launched") scope.launch { log("scope launched") when(val result = doHeavyWork()) { is Result.Success -> {...} is Result.Failure -> {...} } } } } ... } CoroutineScope(context) viewModelScope default set to Dispatchers.Main #6 One ViewModel, Two Scopes #dcnyc24 @mvndy_hd
  30. fun loadContent() { viewModelScope.launch { log("viewModel launched") scope.launch { log("scope

    launched") when(val result = doHeavyWork()) { /* update view state */ } } } } private suspend fun doHeavyWork(): Result<Int> { val count = AtomicInteger(0) (1..5).forEach { i -> val job = scope.launch { delay(1000) val curr = count.increment() log(" task$i | count: $curr") } job.join() } return Result.Success(count.get()) } #7 launch { ... } launch(context) #dcnyc24 @mvndy_hd
  31. fun loadContent() { viewModelScope.launch { log("viewModel launched") scope.launch { log("scope

    launched") when(val result = doHeavyWork()) { is Result.Success -> {...} is Result.Failure -> {...} } } } } private suspend fun doHeavyWork(): Result<Int> { // ^ operates within current context val count = AtomicInteger(0) (1..5).forEach { i -> val job = scope.launch { delay(1000) val curr = count.increment() log(" task$i | count: $curr") } job.join() } return Result.Success(count.get()) } viewModelScope launched | main scope launched | DefaultDispatcher-worker-2 suspend doHeavyWork() | DefaultDispatcher-worker-2 task1 | count: 1 | DefaultDispatcher-worker-1 task2 | count: 2 | DefaultDispatcher-worker-1 task3 | count: 3 | DefaultDispatcher-worker-1 task4 | count: 4 | DefaultDispatcher-worker-1 task5 | count: 5 | DefaultDispatcher-worker-1 loadContent: Success | main launch(context) #7 launch { ... } #dcnyc24 @mvndy_hd
  32. fun loadContent() { viewModelScope.launch { log("viewModel launched") scope.launch { log("scope

    launched") when(val result = doHeavyWork()) { is Result.Success -> {...} is Result.Failure -> {...} } } } } private suspend fun doHeavyWork(): Result<Int> { // ^ operates within current context val count = AtomicInteger(0) (1..5).forEach { i -> val job = scope.launch { // ^ launches new task on IO thread delay(1000) // simulate IO work val curr = count.increment() log(" task$i | count: $curr") } job.join() } return Result.Success(count.get()) } viewModelScope launched | main scope launched | DefaultDispatcher-worker-2 suspend doHeavyWork() | DefaultDispatcher-worker-2 task1 | count: 1 | DefaultDispatcher-worker-1 task2 | count: 2 | DefaultDispatcher-worker-1 task3 | count: 3 | DefaultDispatcher-worker-1 task4 | count: 4 | DefaultDispatcher-worker-1 task5 | count: 5 | DefaultDispatcher-worker-1 loadContent: Success | main launch(context) #6 launch { ... } #dcnyc24 @mvndy_hd
  33. fun loadContent() { viewModelScope.launch { log("viewModel launched") scope.launch { log("scope

    launched") when(val result = doHeavyWork()) { is Result.Success -> {...} is Result.Failure -> {...} } } } } private suspend fun doHeavyWork(): Result<Int> { // ^ operates within current context val count = AtomicInteger(0) (1..5).forEach { i -> val job = scope.launch(Dispatchers.Default) { // ^ launches new task on IO thread delay(1000) // simulate IO work val curr = count.increment() log(" task$i | count: $curr") } job.join() } return Result.Success(count.get()) } viewModelScope launched | main scope launched | DefaultDispatcher-worker-2 suspend doHeavyWork() | DefaultDispatcher-worker-2 task1 | count: 1 | DefaultDispatcher-worker-1 task2 | count: 2 | DefaultDispatcher-worker-1 task3 | count: 3 | DefaultDispatcher-worker-1 task4 | count: 4 | DefaultDispatcher-worker-1 task5 | count: 5 | DefaultDispatcher-worker-1 loadContent: Success | main launch(context) #6 Default for heavy CPU work #dcnyc24 @mvndy_hd
  34. fun loadContent() { viewModelScope.launch { log("viewModel launched") scope.launch { log("scope

    launched") when(val result = doHeavyWork()) { is Result.Success -> {...} is Result.Failure -> {...} } } } } private suspend fun doHeavyWork(): Result<Int> { // ^ operates within current context val count = AtomicInteger(0) (1..5).forEach { i -> val job = scope.launch(Dispatchers.Default) { // ^ launches new task on IO thread delay(1000) // simulate IO work val curr = count.increment() log(" task$i | count: $curr") } job.join() } return Result.Success(count.get()) } viewModelScope launched | main scope launched | DefaultDispatcher-worker-1 suspend doHeavyWork() | DefaultDispatcher-worker-1 task1 | count: 1 | DefaultDispatcher-worker-2 task2 | count: 2 | DefaultDispatcher-worker-2 task3 | count: 3 | DefaultDispatcher-worker-1 task4 | count: 4 | DefaultDispatcher-worker-4 task5 | count: 5 | DefaultDispatcher-worker-1 loadContent: Success | main launch(context) #6 Default for heavy CPU work #dcnyc24 @mvndy_hd
  35. viewModelScope launched | main scope launched | DefaultDispatcher-worker-1 suspend doHeavyWork()

    | DefaultDispatcher-worker-1 task1 | count: 1 | DefaultDispatcher-worker-2 task2 | count: 2 | DefaultDispatcher-worker-2 task3 | count: 3 | DefaultDispatcher-worker-1 task4 | count: 4 | DefaultDispatcher-worker-4 task5 | count: 5 | DefaultDispatcher-worker-1 loadContent: Success | main launch(context) private suspend fun doHeavyWork(): Result<Int> { val count = AtomicInteger(0) (1..5).forEach { i -> val job = scope.launch(Dispatchers.Default) { delay(1000) val curr = count.increment() log(" task$i | count: $curr") } job.join() } return Result.Success(count.get()) } #6 Default for heavy CPU work #dcnyc24 @mvndy_hd
  36. viewModelScope launched | main scope launched | DefaultDispatcher-worker-1 suspend doHeavyWork()

    | DefaultDispatcher-worker-1 task1 | count: 1 | DefaultDispatcher-worker-2 task2 | count: 2 | DefaultDispatcher-worker-2 task3 | count: 3 | DefaultDispatcher-worker-1 task4 | count: 4 | DefaultDispatcher-worker-4 task5 | count: 5 | DefaultDispatcher-worker-1 loadContent: Success | main launch(context) private suspend fun doHeavyWork(): Result<Int> { val count = AtomicInteger(0) (1..5).forEach { i -> val job = scope.launch(Dispatchers.IO) { delay(1000) val curr = count.increment() log(" task$i | count: $curr") } job.join() } return Result.Success(count.get()) } #6 IO for IO #dcnyc24 @mvndy_hd
  37. private suspend fun doHeavyWork(): Result<Int> { val count = AtomicInteger(0)

    (1..5).forEach { i -> val job = scope.launch(Dispatchers.IO) { delay(1000) val curr = count.increment() log(" task$i | count: $curr") } job.join() } return Result.Success(count.get()) } #6 IO for IO viewModelScope launched | main scope launched | DefaultDispatcher-worker-2 suspend doHeavyWork() | DefaultDispatcher-worker-2 task1 | count: 1 | DefaultDispatcher-worker-1 task2 | count: 2 | DefaultDispatcher-worker-1 task3 | count: 3 | DefaultDispatcher-worker-3 task4 | count: 4 | DefaultDispatcher-worker-3 task5 | count: 5 | DefaultDispatcher-worker-3 loadContent: Success | main launch(context) #dcnyc24 @mvndy_hd
  38. #6 Main for UI viewModelScope launched | main suspend doHeavyWork()

    | main task1 | count: 1 | main task2 | count: 2 | main task3 | count: 3 | main task4 | count: 4 | main task5 | count: 5 | main Success(data=5) | main private suspend fun doHeavyWork(): Result<Int> { val count = AtomicInteger(0) (1..5).forEach { i -> val job = scope.launch(Dispatchers.Main) { delay(1000) val curr = count.increment() log(" task$i | count: $curr") } job.join() } return Result.Success(count.get()) } #dcnyc24 @mvndy_hd
  39. fun loadContent() { viewModelScope.launch { scope.launch { when(val result =

    doHeavyWork()) { is Result.Success -> { _viewState.value = MainViewState.Content(result.data) } is Result.Failure -> { _viewState.value = MainViewState.Error } } } } } withContext(context) #7 Updating Android UI #dcnyc24 @mvndy_hd
  40. fun loadContent() { viewModelScope.launch { scope.launch { when(val result =

    doHeavyWork()) { is Result.Success -> { _viewState.value = MainViewState.Content(result.data) } is Result.Failure -> { _viewState.value = MainViewState.Error } } } } } withContext(context) #7 Updating Android UI FATAL EXCEPTION: DefaultDispatcher-worker-2 java.lang.IllegalStateException: Cannot invoke setValue on a background thread at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:502) at androidx.lifecycle.LiveData.setValue(LiveData.java:306) at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java #dcnyc24 @mvndy_hd
  41. fun loadContent() { viewModelScope.launch { scope.launch { when(val result =

    doHeavyWork()) { is Result.Success -> withContext(Dispatchers.Main) { _viewState.value = MainViewState.Content(result.data) } is Result.Failure -> withContext(Dispatchers.Main) { _viewState.value = MainViewState.Error } } } } } withContext(context) #7 Updating Android UI FATAL EXCEPTION: DefaultDispatcher-worker-2 java.lang.IllegalStateException: Cannot invoke setValue on a background thread at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:502) at androidx.lifecycle.LiveData.setValue(LiveData.java:306) at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java #dcnyc24 @mvndy_hd
  42. fun loadContent() { viewModelScope.launch { scope.launch { when(val result =

    doHeavyWork()) { is Result.Success -> withContext(Dispatchers.Main) { _viewState.value = MainViewState.Content(result.data) } is Result.Failure -> withContext(Dispatchers.Main) { _viewState.value = MainViewState.Error } } } } } withContext(context) #7 Updating Android UI FATAL EXCEPTION: DefaultDispatcher-worker-2 java.lang.IllegalStateException: Cannot invoke setValue on a background thread at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:502) at androidx.lifecycle.LiveData.setValue(LiveData.java:306) at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java #dcnyc24 @mvndy_hd
  43. Book giveaway at Droidcon NYC 24 @[email protected] @hinchman-amanda @mvndy_hd Come

    find me in the Golden Room tomorrow for book giveaway + signing - keep an eye on Droidcon socials for an announcement