How to cancel a coroutine if already running? - android

I'm looking for filter a list on user input (SearchView)
fun onQuery(query: String) {
viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
}
If the user tap quickly on the keyboard the function will be called many times and sometimes before the previous call is finished. So I'm looking to stop the coroutine if a new call is done and the coroutine is already running. How can I achieved this please ?
What I tried :
fun onQuery(query: String) {
val job = viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
if (job.isActive) job.cancel()
job.start()
}

If there is supposed to be only one instance of that job running, then I would try taking a reference to it outside of that code block and storing it as a private variable inside the class in which the method onQuery operates:
private var job: Job? = null
...
fun onQuery(query: String) {
job?.run { if (isActive) cancel() }
job = viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
}

Related

Kotlin running code in multiple Scopes works, but not when ran in the same one

I am trying to poplulate values from a datastore. I only want to recover them from the datastore once, which is why I am canceling the job after 1 second. This prevents it constantly updating.
This does not work. (1)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
}
This does not work. (2)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
}
scope.launch {
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
}
scope.launch {
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
}
This does work! (3)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val job2 = Job()
val job3 = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
val scope2 = CoroutineScope(job2 + Dispatchers.IO)
val scope3 = CoroutineScope(job3 + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
}
scope2.launch {
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
}
scope3.launch {
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
job2.cancel()
job3.cancel()
}
Here is the TempDataStore class (4)
class TempDataStore(private val context: Context) {
companion object{
private val Context.dataStore by preferencesDataStore(name = "TempDataStore")
val DISPLAY_TEXT_KEY = stringPreferencesKey("display_text")
val DIE_ONE = stringPreferencesKey("die_one")
val DIE_TWO = stringPreferencesKey("die_two")
}
val getDisplayText: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DISPLAY_TEXT_KEY] ?: "Roll to start!"
}
suspend fun saveDisplayText(text: String) {
context.dataStore.edit { preferences ->
preferences[DISPLAY_TEXT_KEY] = text
}
}
val getDieOne: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DIE_ONE] ?: "1"
}
suspend fun saveDieOne(dieOne: Int) {
context.dataStore.edit { preferences ->
preferences[DIE_ONE] = dieOne.toString()
}
}
val getDieTwo: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DIE_TWO] ?: "2"
}
suspend fun saveDieTwo(dieTwo: Int) {
context.dataStore.edit { preferences ->
preferences[DIE_TWO] = dieTwo.toString()
}
}
suspend fun resetDataStore() {
context.dataStore.edit { preferences ->
preferences.clear()
}
}
}
It is being called from a composable screen.
LaunchedEffect(true) {
sharedViewModel.setRoles()
sharedViewModel.saveChanges()
sharedViewModel.setupDataStore(context)
}
I was expecting (1) to work. It should run all of them at the same time and return the results accordingly. Instead of populating all of the values, it only populates the first one called. (3), works but I want to understand why it works and not (1) and (2).
Calling collect on a Flow suspends the coroutine until the Flow completes, but a Flow from DataStore never completes because it monitors for changes forever. So your first call to collect prevents the other code in your coroutine from ever being reached.
I'm not exactly why your second and third attempts aren't working, but they are extremely hacky anyway, delaying and cancelling as a means to avoid collecting forever.
Before continuing, I think you should remove the nullability from your Flow types:
val getDieOne: Flow<String?>
should be
val getDieOne: Flow<String>
since you are mapping to a non-nullable value anyway.
I don't know exactly what you're attempting, but I guess it is some initial setup in which you don't need to repeatedly update from the changing values in the Flows, so you only need the first value of each flow. You can use the first value to get that. Since these are pulling from the same data store, there's not really any reason to try to do it in parallel. So your function is pretty simple:
suspend fun setupDataStore(context: Context) {
with(TempDataStore(context)) {
die1.value = getDieOne.first().toInt()
displayText.value = getDisplayText.first()
die2.value = getDieTwo.first().toInt()
}
}
If you want just the first value, why not using the .first() method of Flow? And then you shouldn't need those new scopes!
Try out something like this:
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
die1.value = tempDataStore.getDieOne.first().toInt()
displayText.value = tempDataStore.getDisplayText.first()
die2.value = tempDataStore.getDieTwo.first().toInt()
}
EDIT:
Thanks Tenfour04 for the comment! You're right. I've fixed my code.

StateFlow collect not firing for list type

#HiltViewModel
class HistoryViewModel #Inject constructor(private val firebaseRepository: FirebaseRepository) :
ViewModel() {
private val translateList: MutableList<Translate> = mutableListOf()
private val _translateListState: MutableStateFlow<List<Translate>> =
MutableStateFlow(translateList)
val translateListState = _translateListState.asStateFlow()
init {
listenToSnapshotData()
}
private suspend fun addItemToList(translate: Translate) {
Log.d("customTag", "item added adapter $translate")
translateList.add(translate)
_translateListState.emit(translateList)
}
private suspend fun removeItemFromList(translate: Translate) {
Log.d("customTag", "item removed adapter $translate")
val indexOfItem = translateList.indexOfFirst {
it.id == translate.id
}
if (indexOfItem != -1) {
translateList.removeAt(indexOfItem)
_translateListState.emit(translateList)
}
}
private suspend fun updateItemFromList(translate: Translate) {
Log.d("customTag", "item modified adapter $translate")
val indexOfItem = translateList.indexOfFirst {
it.id == translate.id
}
if (indexOfItem != -1) {
translateList[indexOfItem] = translate
_translateListState.emit(translateList)
}
}
private fun listenToSnapshotData() {
viewModelScope.launch {
firebaseRepository.translateListSnapshotListener().collect { querySnapshot ->
querySnapshot?.let {
for (document in it.documentChanges) {
val translateData = document.document.toObject(Translate::class.java)
when (document.type) {
DocumentChange.Type.ADDED -> {
addItemToList(translate = translateData)
}
DocumentChange.Type.MODIFIED
-> {
updateItemFromList(translate = translateData)
}
DocumentChange.Type.REMOVED
-> {
removeItemFromList(translate = translateData)
}
}
}
}
}
}
}
}
Here data comes properly in querySnapshot in listenToSnapshotData function. And post that it properly calls corresponding function to update the list.
But after this line _translateListState.emit(translateList) flow doesn't go to corresponding collectLatest
private fun observeSnapShotResponse() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
historyViewModel.translateListState.collectLatest {
Log.d("customTag", "calling submitList from fragment")
translateListAdapter.submitList(it)
}
}
}
}
calling submitList from fragment is called once at the start, but as & when data is modified in list viewmodel, callback doesn't come to collectLatest
This is from StateFlow documentation:
Values in state flow are conflated using Any.equals comparison in a similar way to distinctUntilChanged operator. It is used to conflate incoming updates to value in MutableStateFlow and to suppress emission of the values to collectors when new value is equal to the previously emitted one.
You are trying to emit the same instance of List all the time, which has no effect because of what is written in the docs. You will have to create new instance of the list every time.

Coroutine StateFlow.collect{} not firing

I'm seeing some odd behavior. I have a simple StateFlow<Boolean> in my ViewModel that is not being collected in the fragment. Definition:
private val _primaryButtonClicked = MutableStateFlow(false)
val primaryButtonClicked: StateFlow<Boolean> = _primaryButtonClicked
and here is where I set the value:
fun primaryButtonClick() {
_primaryButtonClicked.value = true
}
Here is where I'm collecting it.
repeatOnOwnerLifecycle {
launch(dispatchProvider.io()) {
freeSimPurchaseFragmentViewModel.primaryButtonClicked.collect {
if (it) {
autoCompletePlacesStateFlowModel.validateErrors()
formValidated = autoCompletePlacesStateFlowModel.validateAddress()
if (formValidated) {
freeSimPurchaseFragmentViewModel
.sumbitForm(autoCompletePlacesStateFlowModel.getStateFlowCopy())
}
}
}
}
}
repeatOnOwnerLifecycle:
inline fun Fragment.repeatOnOwnerLifecycle(
state: Lifecycle.State = Lifecycle.State.RESUMED,
crossinline block: suspend CoroutineScope.() -> Unit
) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(state) {
block()
}
}
What am I doing wrong? The collector never fires.
Does this make sense?
val primaryButtonClicked: StateFlow<Boolean> = _primaryButtonClicked.asStateFlow()
Also I couldn't understand the inline function part, because under the hood seems you wrote something like this
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch(dispatchProvider.io()) {
freeSimPurchaseFragmentViewModel.primaryButtonClicked.collect {
if (it) {
autoCompletePlacesStateFlowModel.validateErrors()
formValidated = autoCompletePlacesStateFlowModel.validateAddress()
if (formValidated) {
freeSimPurchaseFragmentViewModel
.sumbitForm(autoCompletePlacesStateFlowModel.getStateFlowCopy())
}
}
}
}
}
}
Why are you launching one coroutine in another and collect the flow from IO dispatcher? You need to collect the values from the main dispatcher.

Coroutine scope cancel

I know that there are a lot of posts "How to cancel Coroutines Scope" but I couldn't find the answer for my case.
I have an Array of objects that I want to send each of them to Server using Coroutines.
What I need is, if one of my requests returns error, canceling others.
Here is my code:
private fun sendDataToServer(function: () -> Unit) {
LiabilitiesWizardSessionManager.getLiabilityAddedDocuments().let { documents ->
if (documents.isEmpty().not()) {
CoroutineScope(Dispatchers.IO).launch {
documents.mapIndexed { index, docDetail ->
async {
val result = uploadFiles(docDetail)
}
}.map {
var result = it.await()
}
}
} else function.invoke()
}
}
Below is my uploadFiles() function:
private suspend fun uploadFiles(docDetail: DocDetail): ArchiveFileResponse? {
LiabilitiesWizardSessionManager.mCreateLiabilityModel.let { model ->
val file = File(docDetail.fullFilePath)
val crmCode = docDetail.docTypeCode
val desc = docDetail.docTypeDesc
val id = model.commitmentMember?.id
val idType = 1
val createArchiveFileModel = CreateArchiveFileModel(108, desc, id, idType).apply {
this.isLiability = true
this.adaSystem = 3
}
val result = mRepositoryControllerKotlin.uploadFile(file, createArchiveFileModel)
return when (result) {
is ResultWrapper.Success -> {
result.value
}
is ResultWrapper.GenericError -> {
null
}
is ResultWrapper.NetworkError -> {
null
}
}
}
}
I know, I'm missing something.

Android: How to detect how long workmanager is already in enqueue mode?

I want to detect, how long a specific work is already in enqueue mode. I need this information, in order to inform the user about his state (e.g when workmanager is longer than 10 seconds in enqueue mode -> cancel work -> inform user that he needs to do X in order to achieve Y). Something like this:
Pseudo Code
workInfo.observe(viewLifecylceOwner) {
when(it.state) {
WorkInfo.State.ENQUEUED -> if(state.enqueue.time > 10) cancelWork()
}
}
I didn't find anything about this anywhere. Is this possible?
I appreciate every help.
I have managed to create a somewhat robust "Workmanager watcher". My intention was the following: When the Workmanager is not finished within 7 seconds, tell the user that an error occurred. The Workmanager itself will never be cancelled, furthermore my function is not even interacting with the Workmanager itself. This works in 99% of all cases:
Workerhelper
object WorkerHelper {
private var timeStamp by Delegates.notNull<Long>()
private var running = false
private var manuallyStopped = false
private var finished = false
open val maxTime: Long = 7000000000L
// Push the current timestamp, set running to true
override fun start() {
timeStamp = System.nanoTime()
running = true
manuallyStopped = false
finished = false
Timber.d("Mediator started")
}
// Manually stop the WorkerHelper (e.g when Status is Status.Success)
override fun stop() {
if (!running) return else {
running = false
manuallyStopped = true
finished = true
Timber.d("Mediator stopped")
}
}
override fun observeMaxTimeReachedAndCancel(): Flow<Boolean> = flow {
try {
coroutineScope {
// Check if maxTime is not passed with => (System.nanoTime() - timeStamp) <= maxTime
while (running && !finished && !manuallyStopped && (System.nanoTime() - timeStamp) <= maxTime) {
emit(false)
}
// This will be executed only when the Worker is running longer than maxTime
if (!manuallyStopped || !finished) {
emit(true)
running = false
finished = true
this#coroutineScope.cancel()
} else if (finished) {
this#coroutineScope.cancel()
}
}
} catch (e: CancellationException) {
}
}.flowOn(Dispatchers.IO)
Then in my Workmanager.enqueueWork function:
fun startDownloadDocumentWork() {
WorkManager.getInstance(context)
.enqueueUniqueWork("Download Document List", ExistingWorkPolicy.REPLACE, downloadDocumentListWork)
pushNotification()
}
private fun pushNotification() {
WorkerHelper.start()
}
And finally in my ViewModel
private fun observeDocumentList() = viewModelScope.launch {
observerWorkerState(documentListWorkInfo).collect {
when(it) {
is Status.Loading -> {
_documentDataState.postValue(Status.loading())
// Launch another Coroutine, otherwise current viewmodelscrope will be blocked
CoroutineScope(Dispatchers.IO).launch {
WorkerHelper.observeMaxTimeReached().collect { lostConnection ->
if (lostConnection) {
_documentDataState.postValue(Status.failed("Internet verbindung nicht da"))
}
}
}
}
is Status.Success -> {
WorkerHelper.finishWorkManually()
_documentDataState.postValue(Status.success(getDocumentList()))
}
is Status.Failure -> {
WorkerHelper.finishWorkManually()
_documentDataState.postValue(Status.failed(it.message.toString()))
}
}
}
}
I've also created a function that converts the Status of my workmanager to my custom status class:
Status
sealed class Status<out T> {
data class Success<out T>(val data: T) : Status<T>()
class Loading<T> : Status<T>()
data class Failure<out T>(val message: String?) : Status<T>()
companion object {
fun <T> success(data: T) = Success<T>(data)
fun <T> loading() = Loading<T>()
fun <T> failed(message: String?) = Failure<T>(message)
}
}
Function
suspend inline fun observerWorkerState(workInfoFlow: Flow<WorkInfo>): Flow<Status<Unit>> = flow {
workInfoFlow.collect {
when (it.state) {
WorkInfo.State.ENQUEUED -> emit(Status.loading<Unit>())
WorkInfo.State.RUNNING -> emit(Status.loading<Unit>())
WorkInfo.State.SUCCEEDED -> emit(Status.success(Unit))
WorkInfo.State.BLOCKED -> emit(Status.failed<Unit>("Workmanager blocked"))
WorkInfo.State.FAILED -> emit(Status.failed<Unit>("Workmanager failed"))
WorkInfo.State.CANCELLED -> emit(Status.failed<Unit>("Workmanager cancelled"))
}
}
}

Categories

Resources