Android live data transformations on a background thread - android

I saw this but I'm not sure how to implement it or if this is the same issue, I have a mediator live data that updates when either of its 2 source live datas update or when the underlying data (Room db) updates, it seems to work fine but if the data updates a lot it refreshes a lot in quick succession and I get an error
Cannot run invalidation tracker. Is the db closed?
Cannot access database on the main thread since it may potentially lock the UI for a long period of time
this doesn't happen everytime, only when there are a lot of updates to the database in very quick succession heres the problem part of the view model,
var search: MutableLiveData<String> = getSearchState()
val filters: MutableLiveData<MutableSet<String>> = getCurrentFiltersState()
val searchPokemon: LiveData<PagingData<PokemonWithTypesAndSpeciesForList>>
val isFiltersLayoutExpanded: MutableLiveData<Boolean> = getFiltersLayoutExpanded()
init {
val combinedValues =
MediatorLiveData<Pair<String?, MutableSet<String>?>?>().apply {
addSource(search) {
value = Pair(it, filters.value)
}
addSource(filters) {
value = Pair(search.value, it)
}
}
searchPokemon = Transformations.switchMap(combinedValues) { pair ->
val search = pair?.first
val filters = pair?.second
if (search != null && filters != null) {
searchAndFilterPokemonPager(search, filters.toList())
} else null
}.distinctUntilChanged()
}
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemonPager(search: String, filters: List<String>): LiveData<PagingData<PokemonWithTypesAndSpeciesForList>> {
return Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
maxSize = 60
)
) {
if (filters.isEmpty()){
searchPokemonForPaging(search)
} else {
searchAndFilterPokemonForPaging(search, filters)
}
}.liveData.cachedIn(viewModelScope)
}
#SuppressLint("DefaultLocale")
private fun getAllPokemonForPaging(): PagingSource<Int, PokemonWithTypesAndSpecies> {
return repository.getAllPokemonWithTypesAndSpeciesForPaging()
}
#SuppressLint("DefaultLocale")
private fun searchPokemonForPaging(search: String): PagingSource<Int, PokemonWithTypesAndSpeciesForList> {
return repository.searchPokemonWithTypesAndSpeciesForPaging(search)
}
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemonForPaging(search: String, filters: List<String>): PagingSource<Int, PokemonWithTypesAndSpeciesForList> {
return repository.searchAndFilterPokemonWithTypesAndSpeciesForPaging(search, filters)
}
the error is actually thrown from the function searchPokemonForPaging
for instance it happens when the app starts which does about 300 writes but if I force the calls off the main thread by making everything suspend and use runBlocking to return the Pager it does work and I don't get the error anymore but it obviously blocks the ui, so is there a way to maybe make the switchmap asynchronous or make the searchAndFilterPokemonPager method return a pager asynchronously? i know the second is technically possible (return from async) but maybe there is a way for coroutines to solve this
thanks for any help

You can simplify combining using combineTuple (which is available as a library that I wrote for this specific purpose) (optional)
Afterwards, you can use the liveData { coroutine builder to move execution to background thread
Now your code will look like
val search: MutableLiveData<String> = getSearchState()
val filters: MutableLiveData<Set<String>> = getCurrentFiltersState()
val searchPokemon: LiveData<PagingData<PokemonWithTypesAndSpeciesForList>>
val isFiltersLayoutExpanded: MutableLiveData<Boolean> = getFiltersLayoutExpanded()
init {
searchPokemon = combineTuple(search, filters).switchMap { (search, filters) ->
liveData {
val search = search ?: return#liveData
val filters = filters ?: return#liveData
withContext(Dispatchers.IO) {
emit(searchAndFilterPokemonPager(search, filters.toList()))
}
}
}.distinctUntilChanged()
}

Related

How can I get data and initialize a field in viewmodel using kotlin coroutines and without a latenite of null field

I have a common situation of getting data. I use the Kotlin Coroutines.
1 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
lateinit var data: List<String>
init {
viewModelScope.launch {
data = gettingData.get()
}
}
}
2 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = MutableStateFlow<List<String>?>(null)
init {
viewModelScope.launch {
data.emit(gettingData.get())
}
}
}
How can I initialize a data field not delayed, but immediately, with the viewModelScope but without a lateinit or nullble field? And without LiveData, my progect uses Coroutine Flow
I can't return a result of viewModelScope job in .run{} or by lazy {}.
I cant return a result drom fun:
val data: List<String> = getData()
fun getData(): List<String> {
viewModelScope.launch {
data = gettingData.get()
}
return ???
}
Also I can't make suspend fun getData() because I can't create coroutineScope in initialisation'
You're describing an impossibility. Presumably, gettingData.get() is defined as a suspend function, meaning the result literally cannot be retrieved immediately. Since it takes a while to retrieve, you cannot have an immediate value.
This is why apps and websites have loading indicators in their UI.
If you're using Flows, you can use a Flow with a nullable type (like in your option 2 above), and in your Activity/Fragment, in the collector, you show either a loading indicator or your data depending on whether it is null.
Your code 2 can be simplified using the flow builder and stateIn with a null default value:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = flow<List<String>?> { emit(gettingData.get()) }
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
}
In your Activity or Fragment:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.data
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { list ->
if(list == null) {
// Show loading indicator in UI
} else {
// Show the data
}
}
}
If your data loads pretty quickly, instead of making the type nullable, you can just make the default value emptyList(). Then your collector can just not do anything when the list is empty. This works if the data loads quickly enough that the user isn't going to wonder if something is wrong because the screen is blank for so long.
You have to use SharedFlow with replay 1 (to store last value and replay it for a new subscriber) to implement it.
My sample:
interface DataSource {
suspend fun getData(): Int
}
class DataViewModel(dataSource: DataSource): ViewModel() {
val dataField =
flow<Int> {
emit(dataSource.getData())
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(1000), 1)
}

Kotlin KMM stop coroutine flow with infinite loop properly

I'm building a KMM app for retrieving news.
My app fetches news every 30 seconds and save it in a local database. User must be logged for use it. When user want to logout i need to stop refreshing news and delete the local database.
How do i stop a flow with an infinite loop properly without use static variabile?
I designed the app like follows:
ViewModel (separate for Android and iOS)
UseCase (shared)
Repository (shared)
Data source (shared)
Android Jetpack compose single activity
iOS SwiftUI
Android ViewModel:(iOS use ObservableObject, but logic is the same)
#HiltViewModel
class NewsViewModel #Inject constructor(
private val startFetchingNews: GetNewsUseCase,
private val stopFetchingNews: StopGettingNewsUseCase,
) : ViewModel() {
private val _mutableNewsUiState = MutableStateFlow(NewsState())
val newsUiState: StateFlow<NewsState> get() = _mutableNewsUiState.asStateFlow()
fun onTriggerEvent(action: MapEvents) {
when (action) {
is NewsEvent.GetNews -> {
getNews()
}
is MapEvents.StopNews -> {
//????
}
else -> {
}
}
}
private fun getNews()() {
startFetchingNews().collectCommon(viewModelScope) { result ->
when {
result.error -> {
//update state
}
result.succeeded -> {
//update state
}
}
}
}
}
UseCase:
class GetNewsUseCase(
private val newsRepo: NewsRepoInterface) {
companion object {
private val UPDATE_INTERVAL = 30.seconds
}
operator fun invoke(): CommonFlow<Result<List<News>>> = flow {
while (true) {
emit(Result.loading())
val result = newsRepo.getNews()
if (result.succeeded) {
// emit result
} else {
//emit error
}
delay(UPDATE_INTERVAL)
}
}.asCommonFlow()
}
Repository:
class NewsRepository(
private val sourceNews: SourceNews,
private val cacheNews: CacheNews) : NewsRepoInterface {
override suspend fun getNews(): Result<List<News>> {
val news = sourceNews.fetchNews()
//.....
cacheNews.insert(news) //could be a lot of news
return Result.data(cacheNews.selectAll())
}
}
Flow extension functions:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun collectCommon(
coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
callback: (T) -> Unit, // callback on each emission
) {
onEach {
callback(it)
}.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
}
I tried to move the while loop inside repository, so maybe i can break the loop with a singleton repository, but then i must change the getNews method to flow and collect inside GetNewsUseCase (so a flow inside another flow).
Thanks for helping!
When you call launchIn on a Flow, it returns a Job. Hang on to a reference to this Job in a property, and you can call cancel() on it when you want to stop collecting it.
I don't see the point of the CommonFlow class. You could simply write collectCommon as an extension function of Flow directly.
fun <T> Flow<T>.collectCommon(
coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
callback: (T) -> Unit, // callback on each emission
): Job {
return onEach {
callback(it)
}.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
// ...
private var fetchNewsJob: Job? = null
private fun getNews()() {
fetchNewsJob = startFetchingNews().collectCommon(viewModelScope) { result ->
when {
result.error -> {
//update state
}
result.succeeded -> {
//update state
}
}
}
}
In my opinion, collectCommon should be eliminated entirely because all it does is obfuscate your code a little bit. It saves only one line of code at the expense of clarity. It's kind of an antipattern to create a CoroutineScope whose reference you do not keep so you can manage the coroutines running in it--might as well use GlobalScope instead to be clear you don't intend to manage the scope lifecycle so it becomes clear you must manually cancel the Job, not just in the case of the news source change, but also when the UI it's associated with goes out of scope.

How to refactor a coroutine and not use Deferred

I am trying to see how I can simply this code to use the latest way, I was wondering if there is a better way to fetch the images, and also not use Deferred
override suspend fun getImages(): List<Images> = coroutineScope {
val today = LocalDate.now()
val deferredImages = mutableListOf<Deferred<Images>>()
for (i in 0 until numberOfImages) {
val day = today.minusDays(i.toLong())
val image = async { api.getImages(day.format(DateTimeFormatter.ISO_DATE)) }
deferredImages.add(image)
}
deferredImages.map { it.await() }
}
I would do something like this:
override suspend fun getImages(): List<Images> = coroutineScope {
val today = LocalDate.now()
List(numberOfImages) { index ->
val day = today.minusDays(index.toLong())
async {
api.getImages(day.format(DateTimeFormatter.ISO_DATE))
}
}.awaitAll()
}
The functional approach is so that you don't have to add to the list of Deferred manually (but it's not absolutely necessary as #broot pointed out). Also, it technically still is a list of Deferred here :)
The important bit is awaitAll, which fails earlier in case of error in one of the tasks. Using map { it.await() } is less efficient because if the last task fails, you will wait for all the others to finish before throwing the exception, instead of cancelling everything and throwing immediately.
Also to clarify a bit what's going on, you can extract pieces in different functions:
override suspend fun getImages(): List<Images> {
val daysToFetch = windowOfDaysBackFromToday(size = numberOfImages)
return fetchImages(daysToFetch)
}
private suspend fun fetchImages(daysToFetch: List<LocalDate>) = coroutineScope {
daysToFetch
.map { day ->
async { api.getImages(day.format(DateTimeFormatter.ISO_DATE)) }
}
.awaitAll()
}
/**
* Returns a window of [size] days, starting from today (included) and going back.
*/
private fun windowOfDaysBackFromToday(size: Int): List<LocalDate> {
val today = LocalDate.now()
return List(size) { today.minusDays(it.toLong()) }
}
It's longer but the names make it easier to grasp, and also if someone doesn't need to go down a level of abstraction, they can just read getImages and stop there.

How to complete a Kotlin Flow in Android Worker

I'm investigating the use of Kotlin Flow within my current Android application
My application retrieves its data from a remote server via Retrofit API calls.
Some of these API's return 50,000 data items in 500 item pages.
Each API response contains an HTTP Link header containing the Next pages complete URL.
These calls can take up to 2 seconds to complete.
In an attempt to reduce the elapsed time I have employed a Kotlin Flow to concurrently process each page
of data while also making the next page API call.
My flow is defined as follows:
private val persistenceThreadPool = Executors.newFixedThreadPool(3).asCoroutineDispatcher()
private val internalWorkWorkState = MutableStateFlow<Response<List<MyPage>>?>(null)
private val workWorkState = internalWorkWorkState.asStateFlow()
private val myJob: Job
init {
myJob = GlobalScope.launch(persistenceThreadPool) {
workWorkState.collect { page ->
if (page == null) {
} else managePage(page!!)
}
}
}
My Recursive function is defined as follows that fetches all pages:-
private suspend fun managePages(accessToken: String, response: Response<List<MyPage>>) {
when {
result != null -> return
response.isSuccessful -> internalWorkWorkState.emit(response)
else -> {
manageError(response.errorBody())
result = Result.failure()
return
}
}
response.headers().filter { it.first == HTTP_HEADER_LINK && it.second.contains(REL_NEXT) }.forEach {
val parts = it.second.split(OPEN_ANGLE, CLOSE_ANGLE)
if (parts.size >= 2) {
managePages(accessToken, service.myApiCall(accessToken, parts[1]))
}
}
}
private suspend fun managePage(response: Response<List<MyPage>>) {
val pages = response.body()
pages?.let {
persistResponse(it)
}
}
private suspend fun persistResponse(myPage: List<MyPage>) {
val myPageDOs = ArrayList<MyPageDO>()
myPage.forEach { page ->
myPageDOs.add(page.mapDO())
}
database.myPageDAO().insertAsync(myPageDOs)
}
My numerous issues are
This code does not insert all data items that I retrieve
How do complete the flow when all data items have been retrieved
How do I complete the GlobalScope job once all the data items have been retrieved and persisted
UPDATE
By making the following changes I have managed to insert all the data
private val persistenceThreadPool = Executors.newFixedThreadPool(3).asCoroutineDispatcher()
private val completed = CompletableDeferred<Int>()
private val channel = Channel<Response<List<MyPage>>?>(UNLIMITED)
private val channelFlow = channel.consumeAsFlow().flowOn(persistenceThreadPool)
private val frank: Job
init {
frank = GlobalScope.launch(persistenceThreadPool) {
channelFlow.collect { page ->
if (page == null) {
completed.complete(totalItems)
} else managePage(page!!)
}
}
}
...
...
...
channel.send(null)
completed.await()
return result ?: Result.success(outputData)
I do not like having to rely on a CompletableDeferred, is there a better approach than this to know when the Flow has completed everything?
You are looking for the flow builder and Flow.buffer():
suspend fun getData(): Flow<Data> = flow {
var pageData: List<Data>
var pageUrl: String? = "bla"
while (pageUrl != null) {
TODO("fetch pageData from pageUrl and change pageUrl to the next page")
emitAll(pageData)
}
}
.flowOn(Dispatchers.IO /* no need for a thread pool executor, IO does it automatically */)
.buffer(3)
You can use it just like a normal Flow, iterate, etc. If you want to know the total length of the output, you should calculate it on the consumer with a mutable closure variable. Note you shouldn't need to use GlobalScope anywhere (ideally ever).
There are a few ways to achieve the desired behaviour. I would suggest to use coroutineScope which is designed specifically for parallel decomposition. It also provides good cancellation and error handling behaviour out of the box. In conjunction with Channel.close behaviour it makes the implementation pretty simple. Conceptually the implementation may look like this:
suspend fun fetchAllPages() {
coroutineScope {
val channel = Channel<MyPage>(Channel.UNLIMITED)
launch(Dispatchers.IO){ loadData(channel) }
launch(Dispatchers.IO){ processData(channel) }
}
}
suspend fun loadData(sendChannel: SendChannel<MyPage>){
while(hasMoreData()){
sendChannel.send(loadPage())
}
sendChannel.close()
}
suspend fun processData(channel: ReceiveChannel<MyPage>){
for(page in channel){
// process page
}
}
It works in the following way:
coroutineScope suspends until all children are finished. So you don't need CompletableDeferred anymore.
loadData() loads pages in cycle and posts them into the channel. It closes the channel as soon as all pages have been loaded.
processData fetches items from the channel one by one and process them. The cycle will finish as soon as all the items have been processed (and the channel has been closed).
In this implementation the producer coroutine works independently, with no back-pressure, so it can take a lot of memory if the processing is slow. Limit the buffer capacity to have the producer coroutine suspend when the buffer is full.
It might be also a good idea to use channels fan-out behaviour to launch multiple processors to speed up the computation.

How to enqueue sequential coroutines blocks

What I'm trying to do
I have an app that's using Room with Coroutines to save search queries in the database. It's also possible to add search suggestions and later on I retrieve this data to show them on a list. I've also made it possible to "pin" some of those suggestions.
My data structure is something like this:
#Entity(
tableName = "SEARCH_HISTORY",
indices = [Index(value = ["text"], unique = true)]
)
data class Suggestion(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "suggestion_id")
val suggestionId: Long = 0L,
val text: String,
val type: SuggestionType,
#ColumnInfo(name = "insert_date")
val insertDate: Calendar
)
enum class SuggestionType(val value: Int) {
PINNED(0), HISTORY(1), SUGGESTION(2)
}
I have made the "text" field unique to avoid repeated suggestions with different states/types. E.g.: A suggestion that's a pinned item and a previously queried text.
My Coroutine setup looks like this:
private val parentJob: Job = Job()
private val IO: CoroutineContext
get() = parentJob + Dispatchers.IO
private val MAIN: CoroutineContext
get() = parentJob + Dispatchers.Main
private val COMPUTATION: CoroutineContext
get() = parentJob + Dispatchers.Default
And my DAOs are basically like this:
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(obj: Suggestion): Long
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(objList: List<Suggestion>): List<Long>
I also have the following public functions to insert the data into the database:
fun saveQueryToDb(query: String, insertDate: Calendar) {
if (query.isBlank()) {
return
}
val suggestion = Suggestion(
text = query,
insertDate = insertDate,
type = SuggestionType.HISTORY
)
CoroutineScope(IO).launch {
suggestionDAO.insert(suggestion)
}
}
fun addPin(pin: String) {
if (pin.isBlank()) {
return
}
val suggestion = Suggestion(
text = pin,
insertDate = Calendar.getInstance(),
type = SuggestionType.PINNED
)
CoroutineScope(IO).launch {
suggestionDAO.insert(suggestion)
}
}
fun addSuggestions(suggestions: List<String>) {
addItems(suggestions, SuggestionType.SUGGESTION)
}
private fun addItems(items: List<String>, suggestionType: SuggestionType) {
if (items.isEmpty()) {
return
}
CoroutineScope(COMPUTATION).launch {
val insertDate = Calendar.getInstance()
val filteredList = items.filterNot { it.isBlank() }
val suggestionList = filteredList.map { History(text = it, insertDate = insertDate, suggestionType = suggestionType) }
withContext(IO) {
suggestionDAO.insert(suggestionList)
}
}
}
There are also some other methods, but let's focus on the ones above.
EDIT: All of the methods above are part of a lib that I made, they're are not made suspend because I don't want to force a particular type of programming to the user, like forcing to use Rx or Coroutines when using the lib.
The problem
Let's say I try to add a list of suggestions using the addSuggestions() method stated above, and that I also try to add a pinned suggestion using the addPin() method. The pinned text is also present in the suggestion list.
val list = getSuggestions() // Getting a list somewhere
addSuggestions(list)
addPin(list.first())
When I try to do this, sometimes the pin is added first and then it's overwritten by the suggestion present in the list, which makes me think I might've been dealing with some sort of race condition. Since the addSuggestions() method has more data to handle, and both methods will run in parallel, I believe the addPin() method is completing first.
Now, my Coroutines knowledge is pretty limited and I'd like to know if there's a way to enqueue those method calls and make sure they'll execute in the exact same order I invoked them, that must be strongly guaranteed to avoid overriding data and getting funky results later on. How can I achieve such behavior?
I'd follow the Go language slogan "Don't communicate by sharing memory; share memory by communicating", that means instead of maintaining atomic variables or jobs and trying to synchronize between them, model your operations as messages and use Coroutines actors to handle them.
sealed class Message {
data AddSuggestions(val suggestions: List<String>) : Message()
data AddPin(val pin: String) : Message()
}
And in your class
private val parentScope = CoroutineScope(Job())
private val actor = parentScope.actor<Message>(Dispatchers.IO) {
for (msg in channel) {
when (msg) {
is Message.AddSuggestions -> TODO("Map to the Suggestion and do suggestionDAO.insert(suggestions)")
is Message.AddPin -> TODO("Map to the Pin and do suggestionDAO.insert(pin)")
}
}
}
fun addSuggestions(suggestions: List<String>) {
actor.offer(Message.AddSuggestions(suggestions))
}
fun addPin(pin: String) {
actor.offer(Message.AddPin(pin))
}
By using actors you'll be able to queue messages and they will be processed in FIFO order.
By default when you call .launch{}, it launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job. The coroutine is canceled when the resulting job is canceled.
It doesn't care or wait for other parts of your code it just runs.
But you can pass a parameter to basically tell it to run immediately or wait for other Coroutine to finish(LAZY).
For Example:
val work_1 = CoroutineScope(IO).launch( start = CoroutineStart.LAZY ){
//do dome work
}
val work_2 = CoroutineScope(IO).launch( start = CoroutineStart.LAZY ){
//do dome work
work_1.join()
}
val work_3 = CoroutineScope(IO).launch( ) {
//do dome work
work_2.join()
}
When you execute the above code first work_3 will finish and invoke work_2 when inturn invoke Work_1 and so on,
The summary of coroutine start options is:
DEFAULT -- immediately schedules coroutine for execution according to its context
LAZY -- starts coroutine lazily, only when it is needed
ATOMIC -- atomically (in a non-cancellable way) schedules coroutine for execution according to its context
UNDISPATCHED -- immediately executes coroutine until its first suspension point in the current thread.
So by default when you call .launch{} start = CoroutineStart.DEFAULT is passed because it is default parameter.
Don't launch coroutines from your database or repository. Use suspending functions and then switch dispatchers like:
suspend fun addPin(pin: String) {
...
withContext(Dispatchers.IO) {
suggestionDAO.insert(suggestion)
}
}
Then from your ViewModel (or Activity/Fragment) make the call:
fun addSuggestionsAndPinFirst(suggestions: List<Suggestion>) {
myCoroutineScope.launch {
repository.addSuggestions(suggestions)
repository.addPin(suggestions.first())
}
}
Why do you have a separate addPin() function anyways? You can just modify a suggestion and then store it:
fun pinAndStoreSuggestion(suggestion: Suggestion) {
myCoroutineScope.launch {
repository.storeSuggestion(suggestion.copy(type = SuggestionType.PINNED)
}
}
Also be careful using a Job like that. If any coroutine fails all your coroutines will be cancelled. Use a SupervisorJob instead. Read more on that here.
Disclaimer: I do not approve of the solution below. I'd rather use an old-fashioned ExecutorService and submit() my Runnable's
So if you really want to synchronize your coroutines in a way that the first function called is also the first one to write to your database. (I'm not sure it is guaranteed since your DAO functions are also suspending and Room uses it's own threads too). Try something like the following unit test:
class TestCoroutineSynchronization {
private val jobId = AtomicInteger(0)
private val jobToRun = AtomicInteger(0)
private val jobMap = mutableMapOf<Int, () -> Unit>()
#Test
fun testCoroutines() = runBlocking {
first()
second()
delay(2000) // delay so our coroutines finish
}
private fun first() {
val jobId = jobId.getAndIncrement()
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
delay(1000) // intentionally delay your first coroutine
withContext(Dispatchers.IO) {
submitAndTryRunNextJob(jobId) { println(1) }
}
}
}
private fun second() {
val jobId = jobId.getAndIncrement()
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
submitAndTryRunNextJob(jobId) { println(2) }
}
}
private fun submitAndTryRunNextJob(jobId: Int, action: () -> Unit) {
synchronized(jobMap) {
jobMap[jobId] = action
tryRunNextJob()
}
}
private fun tryRunNextJob() {
var action = jobMap.remove(jobToRun.get())
while (action != null) {
action()
action = jobMap.remove(jobToRun.incrementAndGet())
}
}
}
So what I do on each call is increment a value (jobId) that is later used to prioritize what action to run first. Since you are using suspending function you probably need to add that modifier to the action submitted too (e.g. suspend () -> Unit).

Categories

Resources