Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

"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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

playground setup 1. fun log(msg: String) = println("$msg | ${Thread.currentThread().name}") 2. -Dkotlinx.coroutines.debug in VM options in "Edit Configurations"

Slide 5

Slide 5 text

topics ● coroutine behaviors a. runBlocking { ... } b. launch { ... } c. async {... } ● swapping coroutine context a. CoroutineScope b. standard coroutine builders c. withContext

Slide 6

Slide 6 text

runBlocking { .. } = T ● runBlocking {...} starts a new coroutine that blocks and interrupts current thread until completion ● Should not be used within a coroutine

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

● 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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

#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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

#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

Slide 22

Slide 22 text

#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

Slide 23

Slide 23 text

● 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 is also a Job type ● Deferred::await( ) wait for completion of itself async { .. } = Deferred

Slide 24

Slide 24 text

#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

Slide 25

Slide 25 text

#5 join() v. await() fun main() = runBlocking { val job = launch { val task1 = launch { log(" task1") delay(1000) log(" task1 complete ") } val task2: Deferred = 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

Slide 26

Slide 26 text

#5 join() v. await() fun main() = runBlocking { val job = launch { val task1 = launch { log(" task1") delay(1000) log(" task1 complete ") } val task2: Deferred = 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

Slide 27

Slide 27 text

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)

Slide 28

Slide 28 text

class MainActivityViewModel( private val contextPool: CoroutineContextProvider = ..., ): ViewModel() { private val scope = CoroutineScope(contextPool.defaultDispatcher) private val _viewState = MutableLiveData(MainViewState.Loading) val viewState: LiveData = _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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

fun loadContent() { viewModelScope.launch { log("viewModel launched") scope.launch { log("scope launched") when(val result = doHeavyWork()) { /* update view state */ } } } } private suspend fun doHeavyWork(): Result { 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

Slide 31

Slide 31 text

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 { // ^ 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

Slide 32

Slide 32 text

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 { // ^ 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

Slide 33

Slide 33 text

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 { // ^ 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

Slide 34

Slide 34 text

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 { // ^ 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

Slide 35

Slide 35 text

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 { 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

Slide 36

Slide 36 text

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 { 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

Slide 37

Slide 37 text

private suspend fun doHeavyWork(): Result { 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

Slide 38

Slide 38 text

#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 { 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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

https://medium.com/@hinchman-amanda Find all 3 parts of Kotlin Coroutine mechanisms on Medium

Slide 44

Slide 44 text

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