ursus
07/16/2019, 1:04 PMstreetsofboston
07/16/2019, 1:09 PMursus
07/16/2019, 1:10 PMtseisel
07/16/2019, 1:23 PMursus
07/16/2019, 1:24 PMstreetsofboston
07/16/2019, 1:31 PMCoroutineScope(...)
) and switch to it for running and caching your request.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()
}
}
ursus
07/16/2019, 1:52 PMstreetsofboston
07/16/2019, 1:52 PMliveData.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.ursus
07/16/2019, 1:52 PMstreetsofboston
07/16/2019, 1:54 PMServiceImpl
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/dbasync
returns a Deferrable<T>
, which is a sub-class for a Job
.ursus
07/16/2019, 1:55 PMtseisel
07/16/2019, 1:56 PMWorkManager
: 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.ursus
07/16/2019, 1:57 PMstreetsofboston
07/16/2019, 1:58 PMasync
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.ursus
07/16/2019, 1:58 PMstreetsofboston
07/16/2019, 2:04 PMclass 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 exceptioncachedResults
.
Also, you'd need a way to clean up the cache when necessary.ursus
07/16/2019, 2:06 PMstreetsofboston
07/16/2019, 2:08 PMviewModelScope
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.ursus
07/16/2019, 2:09 PMViewModel {
val liveData
fun syncButtonClicked() {
viewModelScope {
val result = service.getDataFromNetwork()
withContext(UI) {
liveData.set(result)
}
}
}
}
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)
}
}
}
}
streetsofboston
07/16/2019, 2:11 PMursus
07/16/2019, 2:12 PMstreetsofboston
07/16/2019, 2:14 PMwithContext(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.ursus
07/16/2019, 2:19 PMstreetsofboston
07/16/2019, 2:27 PMService
, 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.ursus
07/16/2019, 2:27 PMstreetsofboston
07/16/2019, 2:27 PMursus
07/16/2019, 2:29 PMstreetsofboston
07/16/2019, 2:30 PMlaunc
or async
)ursus
07/16/2019, 2:33 PMViewModel {
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)
}
}
streetsofboston
07/16/2019, 2:38 PMreceive
would be consumeEach
. And not viewModeScope { ... }
, but viewModelScope.launch { ... }
instead.withContext(UI)
there either, since viewModelScope
already uses the Main-UI dispatcher.ursus
07/16/2019, 2:39 PMstreetsofboston
07/16/2019, 2:42 PMFlow
instead of a Channel
ursus
07/16/2019, 2:46 PMstreetsofboston
07/16/2019, 2:52 PMsuspend
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.zhuinden
07/16/2019, 4:07 PMGlobalScope.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.ursus
07/16/2019, 4:08 PM