https://kotlinlang.org logo
#coroutines
Title
# coroutines
h

Hamba

11/23/2023, 1:08 PM
I have a class that needs to do some work during its construction that may take a while. How do i use coroutines to set the class properties to null initially and allow the instance to be used immediately, and also in the constructor trigger an async method to set the property afterward? something like this
Copy code
class myClass() {
    var property: Any? = null

    init {
        // how to run this function asyncly without blocking?
        property = funThatTakesAWhile()
    }

    private fun funThatTakesAWhile() {
        // working...
        return value
    }
}
c

CLOVIS

11/23/2023, 1:15 PM
Copy code
class MyClass(
    scope: CoroutineScope,
) {
    var property: Any? = null
        private set

    init {
        scope.launch {
            property = …
        }
    }
}
If you want to avoid the
nullable
value, and you want the user to be able to know when the value finishes:
Copy code
class MyClass(
    scope: CoroutineScope,
) {
    lateinit var val property: Deferred<Any>
        private set 

    init {
        property = scope.async {
            …
        }
    }
}
h

Hamba

11/23/2023, 1:23 PM
do i have to pass a scope in? can i just create one internally?
p

Patrick Steiger

11/23/2023, 1:24 PM
The problem with just creating a scope out of thin air is two fold: 1. Who’s the parent job? 2. What’s the event that will trigger scope cancellation?
c

CLOVIS

11/23/2023, 1:24 PM
You should always let the caller control the execution context, to avoid memory leaks / performance issues
1
h

Hamba

11/23/2023, 1:26 PM
cant it create it and then close it itself? it only needs to be used once at construction and never again
c

CLOVIS

11/23/2023, 1:26 PM
In which thread pool will it run? This should be controlled by the caller
h

Hamba

11/23/2023, 1:30 PM
i dont know what that means but is it something i need to care about? like is it technically impossible? because otherwise i need to go modify everywhere ive initialised this class
c

CLOVIS

11/23/2023, 1:31 PM
It's not technically impossible, but it's a bad idea because it creates "work leaks": you lose the control of what is executing and where, which is a waste of your resources
If you have any questions, do ask them, but currently you are trying to fight against the model coroutines bring, and you won't get most benefits from coroutines if you continue on this path
h

Hamba

11/23/2023, 1:34 PM
ok, so i pass in scope, does that mean i have to create a scope at every location i want to instantiate one of these classes?
or create a scope to store in viewmodel (im making a compose app) and reuse that everywhere?
c

CLOVIS

11/23/2023, 1:36 PM
That depends what you want and what the class represents. Ask yourself the following questions: • what is the lifetime of
MyClass
? When does it stop being useful? • can multiple instances be created at the same time?
h

Hamba

11/23/2023, 1:38 PM
theres a bunch of instances in a list, and they can be deleted by user actions
and they arent being created rapidly, at most 2 would be created at the same time based on a user action
c

CLOVIS

11/23/2023, 1:41 PM
This way, if a user removes an element while it hasn't finished loading, the work will automatically be cancelled, which frees the resources
h

Hamba

11/23/2023, 1:59 PM
ok im still struggling a bit. im experimenting with just using globalscope for now to even see if i can get this working, and then will use a proper scope after. So the list of instances is held in the viewmodel, and get created by user action, say the onclick of a button will run a method in the viewmodel that adds a new instance to the list. and i also want to display this asyncly calculated value in the ui. so usually for some property held in the viewmodel, i would set it with
by mutablestateof()
so when it gets updated the composable recomposes and displays the new value. but trying to set the mutable property in a coroutine causes exception
Exception in thread "DefaultDispatcher-worker-4" java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
if i dont use mutablestateof, and just set the property null initially and then set it in the coroutine, it works fine but doesnt trigger a recomposition. when i trigger a recomposition some other way, then it displays correctly.
c

CLOVIS

11/23/2023, 2:07 PM
You would have to show more of your code to understand exactly what is happening. This should get you started:
Copy code
class MyClass(
    foo: Int,
    scope: CoroutineScope,
) {
    var value by mutableStateOf<String?>(null)

    init {
        scope.launch {
            delay(1000)
            value = foo.toString()
        }
    }
}

@Composable
fun Display(value: Int) {
    val scope = rememberCoroutineScope()
    val myClass = remember(value) { MyClass(value, scope) }

    Text(myClass.value ?: "Not loaded yet")
}
h

Hamba

11/23/2023, 2:12 PM
hm strange, my code seems to be structured similarly. so im making a video editor kinda app, and the instances being created represent a segment of a video, basically a filepath, start and end times. and they are stored in a list representing the trimmed/stitched video. but when i create the segment, i want to fetch a frame from the video as a thumbnail (which takes some time) to display in the video timeline.
and i have a composable which takes an ImageBitmap? as an argument and displays it if its not null.
Copy code
@Composable
private fun TimelineSegment(image: ImageBitmap?) {
    image?.let {
        Image(
            bitmap = it,
            contentDescription = null
        )
    }
}
which at its core is this
c

CLOVIS

11/23/2023, 2:14 PM
So far, I see nothing strange
(well, except the GlobalScope usage, but we mentioned it already, and it'll create performance issues but shouldn't change the behavior)
h

Hamba

11/23/2023, 2:14 PM
so it only throws those errors on the initial creation
but creating segments after works ok without errors
to clarify, the way segments are created at the moment is by splitting a segment in 2 parts.
right now in the constructor of the viewmodel, i am creating some segments (for testing so i dont have to manually add them). maybe its some weird interaction of coroutines and viewmodel thats causing it
c

CLOVIS

11/23/2023, 2:17 PM
What's the problem you are seeing exactly?
h

Hamba

11/23/2023, 2:18 PM
this exception for the initial creation of a segment in the viewmodel constructor
and the image doesnt get displayed
c

CLOVIS

11/23/2023, 2:19 PM
can you show me the code where it happens? I don't think any of the code you showed so far is the culprit
h

Hamba

11/23/2023, 2:19 PM
but if i split it, which deletes the old segment and creates 2 new segments, it works fine
image.png
c

CLOVIS

11/23/2023, 2:20 PM
I'm not an Android dev, but are you allowed to use state inside the view model's constructor?
The error message seems to point to something like that
h

Hamba

11/23/2023, 2:21 PM
im doing this on compose multiplatform by the way, using precompose library which recreates the functionality of viewmodel from android
erm im not sure, ill search about that
c

CLOVIS

11/23/2023, 2:25 PM
I think you would be better off exposing a
load
function and let compose do the work
Copy code
class MyClass(…) {
    var thumbnail by mutableStateOf<…?>(null)

    suspend fun load() {
        …
        thumbnail = …
    }
}

@Composable
fun Foo(…) {
    val myClass = remember { MyClass(…) }

    LaunchedEffect(Unit) { myClass.load() }

    Text(myClass.thumbnail ?: "Not loaded yet")
}
This way, you don't have to inject the scope at all, and you don't have anything in the constructors, it's just
LaunchedEffect
that does all the work
h

Hamba

11/23/2023, 2:27 PM
the segments arent tied to the ui though. for example, if the user clicks a button and it creates a new segment, i want to update my model (i.e. create a new segment instance and add it to the timeline list) and then have the ui read that list and display itself accordingly. so i cant really put it inside a launchedeffect
c

CLOVIS

11/23/2023, 2:28 PM
You can put the
LaunchedEffect
where you display the thumbnail
h

Hamba

11/23/2023, 2:29 PM
oh as in, the first time the segment image is attempted to be displayed, it runs the method that generates the thumbnail
hm thats a good idea
let me try
great, that works
thanks a lot for your help
f

Francesc

11/23/2023, 4:49 PM
you presumably want to restart this if the image changes, so you should use the image as the key,
Copy code
LaunchedEffect(key1 = image) {
    if (image != null) { /* load */ }
}
1
d

Daniel Pitts

11/23/2023, 5:17 PM
I haven't read through this thread, so if this was mention already, apologies. It's not a good idea to block in constructors. It would be better to have a factory method that is suspend instead.
Copy code
class MyClass(private val value: Value) {}

suspend fun MyClass(valueSource: Any) =
    MyClass(expensiveSuspendFunction(valueSource))
a

andriyo

11/23/2023, 6:14 PM
I didn't read the whole thing but entire premise doesn't sit right with me. Constructor should build a valid object. The instance should not be accessible until it's ready. Maybe lazy delegate is more appropriate here or having two objects instead of one (shallow and deep)
c

CLOVIS

11/23/2023, 10:21 PM
@Daniel Pitts all good, no one in this thread mentioned blocking in constructors. @andriyo the object built here is valid. One of its fields requires more work to initialize, and finishes asynchronously, but I think that's fine.
d

Daniel Pitts

11/23/2023, 10:52 PM
I see now. I was confused since this channel is coroutines, but it doesn’t seem like this is a problem that needs to be solved via coroutines.
h

Hamba

11/24/2023, 2:13 AM
@Daniel Pitts can you explain why you think that? im new to kotlin so my thought process was "something is taking ages and blocking the main thread -> i should do it async -> kotlin uses coroutines for async stuff"
or have i completely misunderstood the point of coroutines lol
d

Daniel Pitts

11/24/2023, 2:37 AM
Coroutines are for structured concurrency. If this is on JVM, you probably just want a regular thread. If this isn’t on the JVM, then I’m not sure, but it might be some other mechanism to offload the expensive work. If that “expensive” work is any kind of blocking task (a tight for loop or blocking io) then coroutines aren’t likely to help the way you want.
c

CLOVIS

11/24/2023, 9:12 AM
@Hamba don't worry about @Daniel Pitts's first comment. If you look at his example, and your final version (with the
load
function), you'll see it's essentially the same, so you're good on that front. The important question there is whether
loadThumbnail
is blocking or not. But since it's
suspend
, I'm going to assume it comes from a well-known library and they did it correctly. If you ever get blocking warnings from that function, do ask another question about it.
If this is on JVM, you probably just want a regular thread.
I completely disagree. This is UI-triggered work, where you can't know if the user will navigate away / do something else that makes the result irrelevant. This is what structured concurrency is for.
If this isn’t on the JVM, then I’m not sure, but it might be some other mechanism to offload the expensive work.
It's always threads, when they exist. On JS, there are no threads, so everything runs concurrently in the main thread.
If that “expensive” work is any kind of blocking task (a tight for loop or blocking io) then coroutines aren’t likely to help the way you want.
Coroutines are just syntax sugar over thread pools, that allow cancellation and nice usage. They solve this problem exactly as well as thread pools do, it's the same underlying mechanism.
h

Hamba

11/24/2023, 9:35 AM
loadThumbnail isnt a suspend function, its just a regular function, and if i add suspend the editor just tells me its redundant. the function inside is just a regular function that i put together that fetches a frame from the video using javacv library which uses ffmpeg under the hood. when you say blocking, do you mean multiple threads running the same function simultaneously would block each other?
this is what it uses
c

CLOVIS

11/24/2023, 10:06 AM
when you say blocking, do you mean multiple threads running the same function simultaneously would block each other?
No, blocking means that the function stops the thread it is running on from doing anything else while it is running. Blocking may or may not be a problem depending on the thread that's blocked. For most threads, it's not an issue, but for the main thread it's a big nono: while it's blocked, it can't process user events, so your entire screen is frozen (no animations, no click events…). Coroutines makes this very easy, though: you can control in which threads operations execute. Here, you only have to replace your
load
function:
Copy code
suspend fun load() {
    withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
        thumbail = loadThumbnail()
    }
}
<http://Dispatchers.IO|Dispatchers.IO>
is a thread pool that's made to be blocked by IO operations (reading files, network requests…). With this, you're guaranteed that the operation runs in that thread pool and not in the main thread. There is also
Dispatchers.Default
for CPU operations (recursive functions, expensive math…), and a few others for other goals.
h

Hamba

11/24/2023, 10:14 AM
ok right, the regular meaning of blocking. i asked that because im confused why that matters? isnt the whole point of coroutine/launchedeffect to run a function off the main thread? or are you saying it may randomly run something in launchedeffect on the main thread by chance?
i updated it but even previously never had any warnings or experienced hitching in my app when it got called
c

CLOVIS

11/24/2023, 10:16 AM
The
LaunchedEffect
documentation doesn't say in which dispatcher the coroutines are started… You can log
currentCoroutineContext()
inside your loading function to know what it uses
h

Hamba

11/24/2023, 10:16 AM
so adding the Dispatchers.IO ensures its run off the main thread, whereas otherwise it probably would but isnt guaranteed?
c

CLOVIS

11/24/2023, 10:17 AM
so adding the Dispatchers.IO ensures its run off the main thread
yes
otherwise it probably would but isnt guaranteed
I don't know. It depends on how
LaunchedEffect
is implemented, but I don't see it written in the documentation. Maybe LaunchedEffect already adds
<http://Dispatchers.IO|Dispatchers.IO>
or
Dispatchers.Default
by itself? I don't know
h

Hamba

11/24/2023, 10:18 AM
right makes sense, thanks