Kotlin SharedFlow and debounce operator - android

I want to debounce the items sent to a shared flow, and consume them after that. Something like this:
private var flow = MutableSharedFlow()
suspend fun search(query: String): Flow<Result> {
flow.emit(query)
return flow.debounce(1000).map{ executeSearch(it) }
}
The event that initiates the search is a user writing on a field. For each character, the search function is called. So I want to get a debounced result, to avoid many queries to the server.
It looks like the debounce operator returns a different flow instance each time, so that all the queries end up invoking the executeSearch() function, without dropping any of them as you could expect by using a debounce operator. How can I achieve a functionality like this, so that a client can invoke a function that returns a flow with debounced results?

You can try something like this:
private var flow = MutableSharedFlow()
init {
flow.debounce(1000)
.collect {
val result = executeSearch(it)
// Process the result (maybe send to the UI)
}
}
suspend fun search(query: String) {
flow.emit(query)
}

With two flows you could do it like this. One backing flow takes all the search inputs, and the second is a debounce version of it that runs the query. The search function doesn’t return a flow because the Flow is already available as a property and we aren’t creating new ones for each input.
private val searchInput = MutableSharedFlow<String>()
val searchResults = searchInput.debounce(1000)
.map { executeSearch(it) }
.shareIn(viewModelScope, SharingStarted.Eagerly)
fun submitSearchInput(query: String) {
searchInput.tryEmit(query)
}
You could alternatively do it with jobs that you extinguish when new inputs come in:
private val searchJob: Job? = null
private val _searchResults = MutableSharedFlow<SearchResultType>()
val searchResults = _searchResults.asSharedFlow()
fun submitSearchInput(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(1000)
_searchResults.emit(executeSearch(query))
}
}

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)
}

State flow is not collecting emitted items

Imagine following scenario:
I open Search View and SearchViewModel is initialized
class SearchViewModel(
usecase: Usecase
) : ViewModel() {
init {
viewModelScope.launch {
usecase.initialize()
}
}
fun search(query: String) = viewModelScope.launch {
usecase.search(query)
}
}
User start typing characters calling search
class UseCase(
private val dataSource: DataSource
private val store: Store
) {
private val searchQueryEmitter = MutableStateFlow<String>("") // 2 change to MutableSharedFlow
private val searchQuery = searchQueryEmitter
.mapLatest { query -> dataSource.search(query) }
.onEach { store.update(it) }
.launchIn(CoroutineScope(Dispatchers.Default)) // 1 comment
suspend fun search(query: String) {
searchQueryEmitter.emit(query)
}
override suspend fun initialize() {
// searchQuery.launchIn(CoroutineScope(Dispatchers.Default)) // > Emit only initial value
// searchQuery.collect() // > Emit only initial value
}
}
Flow emits first item "" and next items according to query value
PROBLEM:
I don't understand if we comment launchIn (1), and call it later in initialize() method, then exactly the same searchQuery.launchIn(...) or searchQuery.collect() cause issue - flow emits only first item "", but calling search with query doesn't trigger emission of next items.
If we change StateFlow to SharedFlow no items will be emitted in any case.
The problem was in ViewModel. I was using two different instances of usecase. The one passed in parameter was used in init while the second one came from import
so the desired case with
override suspend fun initialize() {
searchQuery.collect()
}
is working right now

one-shot operation with Flow in Android

I'm trying to show a user information in DetailActivity. So, I request a data and get a data for the user from server. but in this case, the return type is Flow<User>. Let me show you the following code.
ServiceApi.kt
#GET("endpoint")
suspend fun getUser(#Query("id") id: Int): Response<User>
Repository.kt
fun getUser(id: Int): Flow<User> = flow<User> {
val userResponse = api.getUser(id = id)
if (userResponse.isSuccessful) {
val user = userResponse.body()
emit(user)
}
}
.flowOn(Dispatchers.IO)
.catch { // send error }
DetailViewModel.kt
class DetailViewModel(
private val repository : Repository
) {
val uiState: StateFlow<User> = repository.getUser(id = 369).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = User() // empty user
)
}
DetailActivity.kt
class DetailActivity: AppCompatActivity() {
....
initObersevers() {
lifecycleScope.launch {
// i used the `flowWithLifecycle` because the data is just a single object.
viewModel.uiState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { state ->
// show data
}
}
}
...
}
But, all of sudden, I just realized that this process is just an one-shot operation and thought i can use suspend function and return User in Repository.kt.
So, i changed the Repository.kt.
Repository.kt(changed)
suspend fun getUser(id: Int): User {
val userResponse = api.getUser(id = id)
return if(userResponse.isSuccessful) {
response.body()
} else {
User() // empty user
}
}
And in DetailViewModel, i want to convert the User into StateFlow<User> because of observing from DetailActivity and I'm going to use it the same way as before by using flowWithLifecycle.
the concept is... i thought it's just one single data and i dind't need to use Flow in Repository. because it's not several items like List.
is this way correct or not??
Yeap, this one-time flow doesn't make any sense - it emits only once and that's it.
You've got two different ways. First - is to create a state flow in your repo and emit there any values each time you're doing your GET request. This flow will be exposed to the use case and VM levels. I would say that it leads to more difficult error handling (I'm not fond of this way, but these things are always arguable, haha), but it also has some pros like caching, you can always show/get the previous results.
Second way is to leave your request as a simple suspend function which sends a request, returns the result of it back to your VM (skipping error handling here to be simple):
val userFlow: Flow<User>
get() = _userFlow
private val _userFlow = MutableStateFlow(User())
fun getUser() = launch(viewModelScope) {
_userFlow.value = repository.getUser()
}
This kind of implementation doesn't provide any cache out of scope of this VM's lifecycle, but it's easy to test and use.
So it's not like there is only one "the-coolest-way-to-do-it", it's rather a question what suits you more for your needs.

How can I get the query result at once when I use Room?

I hope to get the total of all records with Room database at once. But, normally Room use background thread to query record asynchronously.
If I use getTotalOfVoiceAsLiveData() in Code A, it will return LiveData<Long>, you know that LiveData variable is lazy, maybe the result is null.
If I use getTotalOfVoice() in Code A, I will get error because I can't use return in viewModelScope.launch{ }.
How can I get the total of all records at once with Room database?
Code A
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
fun getTotalOfVoice():Long {
viewModelScope.launch {
return mDBVoiceRepository.getTotalOfVoice() //It will cause error
}
}
fun getTotalOfVoiceAsLiveData(): LiveData<Long>{
return mDBVoiceRepository.getTotalOfVoiceAsLiveData() //It's lazy, maybe the result is null.
}
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
suspend fun getTotalOfVoice() = mDBVoiceDao.getTotalOfVoice()
fun getTotalOfVoiceAsLiveData() = mDBVoiceDao.getTotalOfVoiceAsLiveData()
}
#Dao
interface DBVoiceDao{
#Query("SELECT count(id) FROM voice_table")
suspend fun getTotalOfVoice(): Long
//When Room queries return LiveData, the queries are automatically run asynchronously on a background thread.
#Query("SELECT count(id) FROM voice_table")
fun getTotalOfVoiceAsLiveData(): LiveData<Long>
}
Add content
To Tobi: Thanks!
Why it is important to you to get the data directly?
I need to generate a filename based the total of the records, such as "untitled0", "untitled1", "untitled2"...
If I can get the query result at once, I can use the following code easyly.
Added again
I hope to record a voice by filename based the total of query records when I click Start button. You know the total of records will change when a reocrd is added or deleted!
Code B
fun getTotalOfVoice():Long {
//Get the query result at once
...
}
fun createdFileanme(){
return "untitled"+getTotalOfVoice().toString()
}
btnStart.setOnClickListener{
recordVoice(createdFileanme()) //I will record voice by filename
}
fun addRecord(){
...
}
fun deleteRecord(){
...
}
New added content
Thanks!
I think 'You should also move all of that into the viewmodel class, without LiveData ' is good way, you can see Image A and How can I get the value of a LivaData<String> at once in Android Studio? .
Do you agree with it?
Image A
Question: at once meaning synchronous or what ? if yes, what happens if the function to get the result has to take a longer time? like network call? well you can decide to do that on another thread.
What I think is for you to use a mutable Object and use the postValue function to dispatch the result to the observers. It should look something like below:
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
private val voices = MutableLiveData<Long>()
fun getTotalOfVoiceAsLiveData(): LiveData<Long> {
voices.postValue(mDBVoiceRepository.getTotalOfVoiceAsLiveData().value)
return voices;
}
}
Making use of it in your Fragment will look like below:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (activity != null) {
val viewModel = ViewModelProvider(requireActivity())
viewModel.get(HomeViewModel::class.java).getTotalOfVoiceAsLiveData().observe(viewLifecycleOwner, Observer { voices: Long ? ->
voices // Sound of music ? be very free to use ...
})
}
}
Happy Coding.
I hope to get the result at once, but LiveData is lazy
Sorry to tell, but this is how the Room interface is designed.
You are right with the lazyness of the returned LiveData object. But this allows you to handle it on a different thread without having to manually handle different threads.
Based on your new information!
You basically have two options:
A) you could do the following:
load data from Room via LivaData
add observer that stores the current total amount
when the button is clicked you just read the local copy
In your View: (only one observer and one clickListener)
val totalVoiceCount: long
val viewModel = ViewModelProvider(requireActivity()).get(HomeViewModel::class.java)
viewModel.getTotalOfVoiceAsLiveData().observe(viewLifecycleOwner, Observer { totalOfVoice : Long ? ->
if (totalOfVoice != null)
totalVoiceCount = totalOfVoice
})
btnStart.setOnClickListener{
viewModel.recordVoice(totalVoiceCount)
}
In your ViewModel: (the logic and everything else)
fun recordVoice(totalVoiceCount : long){
val fileName = createdFileanme(totalVoiceCount)
// create your recording // depending on how you do this, it probably runs on a background thread anyways
}
fun createdFileName(totalVoiceCount : long){
return "untitled"+ String.valueOf(totalVoiceCount)
}
This works reliably because the LiveData has enough time to update the local copy of totalVoiceCount before the user has the chance to click the button.
B) Based on the answer in your parallel question you can of course outsource even more to a background thread. Then you also have the option to call the DAO query with a non-livedata return (as room returns non-livedata queries only on background threads). Is it worth to implement the threading suggestion of Ridcully? Not possible to answer without knowing what else is going on simultaneously... To me it seems like an overkill, but he is right that the more you do on background threads the better for your refresh rate..
You can return Deferred<Long> from viewModelScope.async. I recommend you to use:
val deferred = viewModelScope.async(Dispatchers.IO) {
return#async mDBVoiceRepository.getTotalOfVoice()
}
val value = deferred.await()
await() is suspend
Edit:
If you want to get a getter which will use in your activity or fragment
you need to write a suspend function like this:
suspend fun getTotalOfVoice(): Long {
return viewModelScope.async(Dispatchers.IO) {
return#async mDBVoiceRepository.getTotalOfVoice()
}.await()
}
But mvvm pattern allows you to create LiveData inside your ViewModel, which gives your fragment an observer.
In view model:
private val _totalOfVoiceLD: MutableLiveData<Long> = MutableLiveData()
val totalOfVoiceLD: LiveData<Long>
get() = _totalOfVoiceLD
fun updateTotalOfVoice() {
viewModelScope.launch(Dispatchers.IO) {
val totalOfVoice = mDBVoiceRepository.getTotalOfVoice()
_totalOfVoiceLD.postValue(totalOfVoice)
}
}
and in your fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.totalOfVoiceLD.observe(viewLifecycleOwner, Observer { totalOfVoice ->
totalOfVoiceTextView.text = totalOfVoice.toString()
})
}
You can use coroutineContext.async to get data from DB and wait for getting it's response with data by using .await function for a async dispatch.
suspend fun getAllVoices() : Long{
val awatingResults = viewModelScope.async(Dispatchers.IO) {
mDBVoiceRepository.getTotalOfVoice()
}
val records = awatingResults.await()
return records
}
It is necessary to call a Suspend function from a coroutine and
async.await() is always called in a suspended function so,
val voiceLiveData: MutableLiveData<Long> = MutableLiveData()
fun getAllVoicesFromDB() {
viewModelScope.launch(Dispatchers.IO) {
voiceLiveData.postValue(mDBVoiceRepository.getTotalOfVoice())
}
}
Now call it where ever you want to get your voice data from database and also remember do your further work inside your voiceLiveData observer where you get your response of voices :)
Live data is designed to be lazy, when the value of the live data changes internally it emits and wherever you are observing it, the onChange function will be invoked. It is designed to fire and forget.
Because room uses background thread to run the query.
You can't expect live data to behave like sharedpreference where you store key value pair.
If you want to achieve something like that.
I would suggest you to use
Paper Db or Realm.
If you need your Room result synchronously, your code should be execute in IO thread. In case of coroutines, you can use Dispatchers.IO. Your code can be changed to this to pass the error.
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
fun getTotalOfVoice():Long {
viewModelScope.launch(Dispatchers.IO) { // here
return mDBVoiceRepository.getTotalOfVoice()
}
}
}
If you must run the queries in the main thread, then:
Allow android room to execute queries in main thread.
val dbInstance = Room
.databaseBuilder(ctx, YourDBClass::class.java, "YourDBName")
.allowMainThreadQueries()
.build()
Define the dao method as follows
#Dao
interface DBVoiceDao{
#Query("SELECT count(id) FROM voice_table")
fun getTotalOfVoice(): Long
}
Access the method in the repository
fun getTotalOfVoice():Long {
return dao.getTotalOfVoice()
}

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