I have a resource from the web that can be cached ...
# coroutines
m
I have a resource from the web that can be cached locally, but that sometimes needs to be refreshed. I couldn't find anything in the docs about this, so I wrote a little class for myself, but I feel there should be a library solution. Is there? Okay well scratch the code below, I did not properly realise what it was doing. I'm now deadlocking upon calling the get function. Does joining a blocked job never end? Or is there another mistake I'm making.
Copy code
class RefreshableObject<T : Any>(
    private val scope: CoroutineScope,
    private val getter: suspend () -> T,
    private val getOnInit: Boolean = true,
    private var inner: T? = null,
    private var getInnerJob: Job? = null,
) {
    init {
        if (getOnInit) needsToBeRefreshed()
    }

    fun get(): T {
        if (inner != null) return inner!!

        if (getInnerJob == null) needsToBeRefreshed()

        runBlocking {
            getInnerJob!!.join()
        }
        return inner!!
    }

    // Ensures that the object is eventually refreshed, calls to get after this may still receive the old object.
    fun needsToBeRefreshed() {
        getInnerJob = scope.launch {
            inner = getter()
        }
    }
}
a
Does joining a blocked job never end?
Yes,
join
only returns when the job completes. You didn't ask for this, but here are some ways to avoid
null
checks:
Copy code
class RefreshableObject<T : Any>(
    private val scope: CoroutineScope,
    private val getter: suspend () -> T,
    private val getOnInit: Boolean = true,
    private var inner: T? = null,
    private var getInnerDeferred: Deferred<T>? = null,
) {
    init {
        if (getOnInit) needsToBeRefreshed()
    }

    fun get(): T {
        inner?.let { return it }

        if (getInnerDeferred == null) needsToBeRefreshed()

        return runBlocking {
            getInnerDeferred!!.await()
        }
    }

    // Ensures that the object is eventually refreshed, calls to get after this may still receive the old object.
    fun needsToBeRefreshed() {
        getInnerDeferred = scope.async {
            getter().also {
                inner = it
            }
        }
    }
}
If you change
Job
to a
Deferred
like that, you can avoid storing
inner
at all, as
Deferred
will deal with that anyway:
Copy code
class RefreshableObject<T : Any>(
    private val scope: CoroutineScope,
    private val getter: suspend () -> T,
    getOnInit: Boolean = true, // doesn't need to be a `val`
    private var getInnerDeferred: Deferred<T>? = null,
) {
    init {
        if (getOnInit) needsToBeRefreshed()
    }

    fun get(): T {
        if (getInnerDeferred == null) needsToBeRefreshed()
        return runBlocking {
            getInnerDeferred!!.await()
        }
    }

    // Ensures that the object is eventually refreshed, calls to get after this may still receive the old object.
    fun needsToBeRefreshed() {
        getInnerDeferred = scope.async {
            getter()
        }
    }
}
The last
!!
can be removed like this:
Copy code
class RefreshableObject<T : Any>(
    private val scope: CoroutineScope,
    private val getter: suspend () -> T,
    getOnInit: Boolean = true,
    private var getInnerDeferred: Deferred<T>? = null,
) {
    init {
        if (getOnInit) needsToBeRefreshed()
    }

    fun get(): T {
        val deferred = getInnerDeferred ?: refresh()
        return runBlocking {
            deferred.await()
        }
    }
    
    private fun refresh(): Deferred<T> = scope.async {
        getter()
    }

    // Ensures that the object is eventually refreshed, calls to get after this may still receive the old object.
    fun needsToBeRefreshed() {
        getInnerDeferred = refresh()
    }
}
Now, why is
get
not a
suspend
function?
m
well the idea is that all the suspend stuff is background
the blocking in get is just an impl detail I suppose
Well I think I get my underlying problem
I'm blocking the UI thread waiting for this object to arrive, and compose desktop hasn't started pumping the event loop for the scope yet
This isn't great
I'll need to do create some placeholder composable and then populate from a lambda. Still, that is kinda ugly. I wonder if I can have compose wait on the Deferred and just do all that natively. Surely basically every compose program needs to do this in a billion places
d
I think your "runBlocking" approach isn't going to work for what you want to do. It doesn't really matter how you structure it if your
get
method isn't suspend, it will have block until something is ready.