if I run GlobeScope.launch from android view model...
# coroutines
u
if I run GlobeScope.launch from android view model, I leak it, right?
s
It depends, but yes, you run that risk.
u
but how can I just detach the reference, so it doesnt leak, yet complete the upstream suspend functions?
if I cancel the job it cancels everything
t
What is your use case, so that you need the launched job to outlive your ViewModel ?
u
data sync, which does some api calls and then writes to database
s
Yup, you want the request to continue even if the user cancels the screen, so that you could cache it. Use your view-model scope for your requests from your UI/ViewModel. Then in your service/datasource implementation use your own 'global' scope (
CoroutineScope(...)
) and switch to it for running and caching your request.
Maybe something like this?
Copy code
class MyViewModel : ViewModel() {
    ...
    viewModelScope.launch {
        ...
        val result = service.getDataFromNetwork(input) 
        liveData.value = result.toUiResult()
    }
    ...
}

class ServiceImpl : Service {
    private val scope = CoroutineScope(SupervisorJob() + <http://Dispatchers.IO|Dispatchers.IO>)

    override suspend fun getDataFromNetwork(input: String): NetworkResult = coroutineScope {
        scope.async { 
            val result = ... get result from network...
            addResultToCache(result)
            result
        }.await()
    }
}
☝️ 1
u
interesting, ill try that out
s
In the above example, if the user has exited the screen before the network returns a result, the
liveData.value = result.toUiResult()
will never be called, because
viewModelScope
was cancelled. However, the
scope
in
ServiceImpl
is not cancelled. The
async
it launches runs until its completion.
u
btw, is there a way for the viewmodel to connect back to the getDataFromNetwork, in case viewmodel (new instance of the same) comes back sooner than it returns?
async is like launch that returns a T ?
s
No. you'd have to create code that hooks up to the
ServiceImpl
and let
ServiceImpl
manage wether to get the data from the network or wait if the network request is still going or getting it from the cache/db
async
returns a
Deferrable<T>
, which is a sub-class for a
Job
.
u
k, thanks, how would I do the connecting? pass result to a channel which viewmodel observes?
t
It seems to me that data-sync is better done with
WorkManager
: you schedule the task in the
ViewModel
, but it is run in the scope of the
Worker
. Therefore, the task is guaranteed to run to completion even if the
ViewModel
is cleared.
u
@tseisel why would I need some android api for this? its scoping, viewmodel is just an concrete example use case
s
Using a Channel is a way. But be careful, you'd have to cache the network-request based on a a key (depends on the input to the request). It is not trivial. Instead of a Channel you could also cache `Deferrable<T>`s returned by the
async
calls with the request's input as the key. You can call
await()
on a
Deferred
multiple times. It will only run the
async
once, the other times it will return the already obtained result.
u
you jhust need a scope that doesnt die with viewmodel (or whatever is the call site) like @streetsofboston demonstrated, however im not sure how to bridge those scopes yet
@streetsofboston okay so I need some kind of a mini database / store / map to keep the state and then expose channel / deffered somehow to the call site?
would that deffered contain erros as well?
s
Copy code
class ServiceImpl : Service {
    private val scope = CoroutineScope(SupervisorJob() + <http://Dispatchers.IO|Dispatchers.IO>)
    private val cachedResults = mutableMapOf<Any, Deferred<*>>()

    override suspend fun getDataFromNetwork(input: String): NetworkResult = coroutineScope {
        var deferredResult = cachedResult[input] as Deferred<NetworkResult>
        if (deferredResult == null) {
            deferredResult = scope.async { 
                val result = ... get result from network using 'input'...
                result
            }
            cachedResults[input] = deferredResult
        }
        deferredResult.await()
    }
}
And
await()
will throw an Exception if teh result was an exception
The above code is just an idea... may not entirely compile and may not be thread-safe with regards to
cachedResults
. Also, you'd need a way to clean up the cache when necessary.
u
Looks interesting, but im not sure about the call site, that would mean youd call getDataFromNetwork "locally" from some button click proxied function or whatever
but then again in viewmodel.init to get the possible cached value again, right?
s
The call-site, where
viewModelScope
launches its thing, is wherever you need it. I don't know where that would be, depends on your use-case. It could be in the
init { ... }
block of your ViewModel, or on a button-click when your Fragment/Activity calls to a method on your ViewModel, etc.
u
like this right?
Copy code
ViewModel {
	val liveData

	fun syncButtonClicked() {
		viewModelScope {
			val result = service.getDataFromNetwork()
			withContext(UI) {
				liveData.set(result)
			}
		}
	}
}
if so, then to reconnect youd need this
Copy code
ViewModel {
	val liveData

	init {
		viewModelScope {
			val result = service.getDataFromNetwork(SYNC_KEY)
			withContext(UI) {
				liveData.set(result)
			}
		}
	}

	fun syncButtonClicked() {
		viewModelScope {
			val result = service.getDataFromNetwork(SYNC_KEY)
			withContext(UI) {
				liveData.set(result)
			}
		}
	}
}
right? Isnt that a bit odd?
s
When do you need to reconnect, though?
u
say you want to display a progressbar while sync is in progress .. so after leaving the viewmodel and coming back to it again, it should show right away if sync in progress
s
But yes, something like that, if you need to reconnect. Remember, viewModelScope already runs in the UI thread, no need to do
withContext(Dispatchers.Main)
. Also, I don't see any call to
launch
and such in your code.... Note that your
init
block issues the request, even if the user has not pushed the button before.... you'll need another method on
Service
(and
ServiceImpl
) that queries if a cached Deferred<T> exists or not and if so, only then waits for it.
u
Yea, dammit..seems like a Channel with status of Idle, InProgress right? however, if I "sideffect" to the channel, I lose suspend function composability right? i.e. I exit the coroutine world?
s
You can also just add a method to
Service
, called something like
queryDataFromNetwork(...)
that just awaits a result if the
cachedResults
has a Deferred entry for the given input and returns immediatly
null
if has no such entry. Not sure what you mean with 'composability'. If you mean make it functional by composing lambdas/functions, I would do that later when stuff works. :-) If you need status updates, like 'idle', 'progress', 'loading', ..., 'result', then a channel is better suited.
u
So..not sure if its good, since you then "hardcode" that service/repository whoever to be the leaf node of the couroutine
s
Why is that not good?
u
by composability I mean being able to just call suspend functions as normaln imperative code
since If we were to use the channel, that means most likely that getDataWhatever (rename to fetchDataWhatever) should be nonsuspending and return Unit
no?
s
Your service would return a Channel (a ReceiveChannel), not Unit
For composabitilty: You can call suspend functions as normal sequential/imperative code... as long as you do it inside a Coroutine (the lambda of a
launc
or
async
)
u
no no I mean like this
Copy code
ViewModel {
	val liveData

	init {
		viewModelScope {
			service.syncStatusChannel.receive {
				withContext(UI) {
					liveData.set(result)
				}
			}
		}
	}

	fun syncButtonClicked() {
		service.fetchDataFromNetwork()
	}
}


class ServiceImpl : Service {
	private val _syncStatusChannel = MutableChannel
	syncStatusChannel : Channel
		get() = _syncStatusChannel

    private val scope = CoroutineScope(SupervisorJob() + <http://Dispatchers.IO|Dispatchers.IO>)

    override fun fetchDataFromNetwork(input: String): Unit = coroutineScope {
    	_syncStatusChannel.send(IN_PROGRESS)
        val result = ... get result from network...
        addResultToCache(result)
        _syncStatusChannel.send(IDLE)
    }
}
I dont know the coroutines api just yet so I made up the channels api 😄 hopefully its readable
s
Something like that would be possible, yes.
receive
would be
consumeEach
. And not
viewModeScope { ... }
, but
viewModelScope.launch { ... }
instead.
And no need to do
withContext(UI)
there either, since
viewModelScope
already uses the Main-UI dispatcher.
u
Okay thanks, but the main issue is that the trigger functions returns Unit and is nonsuspending..should it? since now it cannot be used in coroutines to "do something after fetch completed" right?
s
Creation and obtaining Channels don't need Coroutines/Suspend-funs. Writing to them and reading from them, though, can only happen in a Coroutine/Suspend-fun. Before using Channels, go to their documentation on JetBrains and use them appropriately. Lots of interesting ways of using them. And maybe you'd want to use a
Flow
instead of a
Channel
u
obtaining channel is fine, what I mean mainly is the trigger function, should it return plain nonsupsending Unit or some kind of coroutine thing?
what I fear is if I sideffect the status into the channel, i'll lose the composability of the trigger fetchData function
Which then hardcodes that service to be a leaf in that layer, which is fine if its only used by viewmodels (since those are only in lower scope)
but what if Id want some other repository to call the fetch and do somethiny after fetch completes (regular corutine use case)
s
Showing a loading/waiting spinner is a side-effect; you write something to an external device, the UI/screen in this case. There are other ways to do this without a Channel and separating the side-effects, impure code, from the 'pure' code. E.g. use a
suspend
fun to get the actual data in a Coroutine `launch`ed by your ViewModel and that code will then update a
LiveData
property as well that is tied to a waiting/loading-spinner.
z
@ursus why not call the
GlobalScope.launch {
inside a singleton instead of from ViewModel directly? Then it won't have a reference to your ViewModel and won't be able to leak. You could use something like an event bus (channels?!) to communicate back.
u
well..yes..but I thought you couldnt then have it as a suspend function, ill post some code later after I try it