ursus
07/16/2019, 1:04 PMstreetsofboston
07/16/2019, 1:09 PMursus
07/16/2019, 1:10 PMursus
07/16/2019, 1:11 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.streetsofboston
07/16/2019, 1:40 PMclass 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 PMursus
07/16/2019, 1:53 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/dbstreetsofboston
07/16/2019, 1:54 PMasync 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 PMursus
07/16/2019, 2:01 PMursus
07/16/2019, 2:02 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 exceptionstreetsofboston
07/16/2019, 2:05 PMcachedResults.
Also, you'd need a way to clean up the cache when necessary.ursus
07/16/2019, 2:06 PMursus
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)
}
}
}
}ursus
07/16/2019, 2:10 PMViewModel {
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)
}
}
}
}ursus
07/16/2019, 2:11 PMstreetsofboston
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 PMursus
07/16/2019, 2:29 PMursus
07/16/2019, 2:29 PMstreetsofboston
07/16/2019, 2:30 PMstreetsofboston
07/16/2019, 2:32 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)
}
}ursus
07/16/2019, 2:34 PMstreetsofboston
07/16/2019, 2:38 PMreceive would be consumeEach. And not viewModeScope { ... }, but viewModelScope.launch { ... } instead.streetsofboston
07/16/2019, 2:38 PMwithContext(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 Channelursus
07/16/2019, 2:46 PMursus
07/16/2019, 2:46 PMursus
07/16/2019, 2:49 PMursus
07/16/2019, 2:50 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