In my code, I have a time-out functionality and I want to use a countdown timer but after a lot of research, I couldn't find similar functionality as a countdown timer in Kotlin coroutine (able to start, cancel and catch finish callback). Then I decided to use GlobalScope.launch. I know this is a bad solution but my code is working perfectly.
Here is my code
viewModelScope.launch {
val timer = object: CountDownTimer(Constants.PAYMENT_TIMER, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
GlobalScope.launch {
_eventFlow.emit(UIPaymentEvent.NavigateToBack)
}
}
}
timer.start()
collectPaymentIntentUseCase.invoke(currentPaymentIntent!!).onEach { result ->
when (result) {
is Resource.Success -> {
timer.cancel()
if (result.data?.exception == null) {
My question is how can find a 100% similar function to avoid using GlobalScope but be able to use the countdown timer (start, cancel,onComplete callback)?
Note: I am using GlobalScope.lanch to be able to emit UIPaymentEvent.NavigateToBack event to my view
You don't need a CountDownTimer here. Just use the delay() suspend function.
viewModelScope.launch {
val job = launch {
delay(Constants.PAYMENT_TIMER) // Wait for timeout
_eventFlow.emit(UIPaymentEvent.NavigateToBack)
}
collectPaymentIntentUseCase.invoke(currentPaymentIntent!!).onEach { result ->
when (result) {
is Resource.Success -> {
job.cancel() // Cancel the timer
if (result.data?.exception == null) {
You can use callbackFlow for listen your timer. I just code this editor. I hope it will be helpful.
fun timerFlow() = callbackFlow<UIPaymentEvent> {
val timer = object : CountDownTimer(10, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
CoroutineScope().launch {
_eventFlow.emit(UIPaymentEvent.NavigateToBack)
}
}
}
timer.start()
awaitClose()
}
Coroutines are launched inside a CoroutineScope which are similar to lifecycle for android. As such, Android automatically provide coroutine's scope for components like activity or fragment and bound them to theirs lifecycle.
While it's not recommended to use the global scope that starts and ends with android's process. There are no restriction on creating your own and limiting it to a specific view of time. Creating one starts its life and cancelling it stops all tasks inside.
In your case a countdown can be done with only coroutines. As stated in this answer.
But without changing too much of your existing code you could reuse the viewModelScope that launched your timer to emit your event.
viewModelScope.launch {
_eventFlow.emit(UIPaymentEvent.NavigateToBack)
}
Beware of the life of your scope. If the viewmodelScope is dead when the timer finish, the event will never be sent.
Related
I want to animate the progress bar so i am setting its progress using for loop but the loop is too much faster that i can't see the animation . I want the code to add a delay in the loop , i tried using thread delay but not working -
here is the code
private fun showProgress() {
for(i in 0..100){
Thread{
binding.customProgressBar.progress=i
Thread.sleep(100)
}
}
}
Solution : Was not calling start method , but if there any other approach then please let me know
private fun showProgress() {
Thread {
for (i in 0..100) {
binding.customProgressBar.progress = i
Thread.sleep(100)
}
}.start()
}
now i want to terminate the thread when fragment is on pause state .
how to achieve that ?
since you are using kotlin it is better to use coroutine, you can achieve your goal with something like this:
private suspend fun someProgress(scope: CoroutineScope) {
val job = scope.launch {
for (i in 0..100) {
binding.customProgress.progress = i
delay(100)
}
}
// use job.cancel() for cancelling the job or use job.join() for waiting for the job to finish
}
you can learn more about coroutine and how it works in here.
I have a kotlin flow timer. Here is my code:
class CountDownTimer {
suspend fun startTimer(value: Int, onTick: OnTickCallback, onFinish: OnFinishCallback) {
onTick.invoke(0)
(1..timerValue)
.asSequence()
.asFlow()
.onEach {
delay(DELAY)
onTick.invoke(it)
}
.onCompletion {
onFinish.invoke()
}
.cancellable()
.collect()
}
}
Everything working well, but there are situations when I start a new timer, but the current one has not yet completed. So I want to cancel the timer if I start a new one. I know that for this I need to get a Job and call a cancel() on it.
But I can't create job, because I haven't CoroutineScope.
Yes, I could inject the scope in the constructor of my class CountDownTimer, but I need the timer to be attached to the viewModelScope.
Therefore, I start the timer in the view model in viewModelScope.
viewModelScope.launch {
countDownTimer.startTimer(
60,
onTick= { // some logic },
onFinish= { // some logic }
)
}
Now I can get Job inside view model and before start timer cancel() job.
But it turns out I have to store the job object in the view model, will it be correct? Perhaps there is some more automated way to cancel a job. Or perhaps I'd better inject some other CoroutineScope into my CountDownTimer, then the question is what should the CoroutineScope be?
Please, help me.
viewModelScope.launch {} is a job. You can save it into a variable and cancel it anytime. It is ok to store it in a viewmodel.
val job = viewModelScope.launch { ... }
job.cancel()
So when I press a button I need to wait 3 seconds before executing another method, I worked that out with the followin
val job = CoroutineScope(Dispatchers.Main).launch(Dispatchers.Default, CoroutineStart.DEFAULT) {
delay(THREE_SECONDS)
if (this.isActive)
product?.let { listener?.removeProduct(it) }
}
override fun onRemoveProduct(product: Product) {
job.start()
}
now, if I press a cancel button right after I start the job I stop the job from happening and that is working fine
override fun onClick(v: View?) {
when(v?.id) {
R.id.dismissBtn -> {
job.cancel()
}
}
}
The problem is that when I execute again the onRemoveProduct that executes the job.start() it will not start again, seems like that job.isActive never yields to true, why is this happening ?
A Job once cancelled cannot be started again. You need to do that in a different way. One way is to create a new job everytime onRemoveProduct is called.
private var job: Job? = null
fun onRemoveProduct(product: Product) {
job = scope.launch {
delay(THREE_SECONDS)
listener?.removeProduct(product) // Assuming the two products are same. If they aren't you can modify this statement accordingly.
}
}
fun cancelRemoval() { // You can call this function from the click listener
job?.cancel()
}
Also, in this line of your code CoroutineScope(Dispatchers.Main).launch(Dispatchers.Default, CoroutineStart.DEFAULT),
You shouldn't/needn't create a new coroutine scope by yourself. You can/should use the already provided viewModelScope or lifecycleScope. They are better choices as they are lifecycle aware and get cancelled at the right time.
Dispatchers.Main is useless because it gets replaced by Dispatchers.Default anyways. Dispatchers.Default is also not required here because you aren't doing any heavy calculations (or calling some blocking code) here.
CoroutineStart.DEFAULT is the default parameter so you could have skipped that one.
And you also need not check if (this.isActive) because
If the [Job] of the current coroutine is cancelled or completed while delay is waiting, it immediately resumes with [CancellationException].
I have collect flow from shared viewmodel in fragment :
private val viewModel: MyViewModel by sharedViewModel()
private fun observeViewModelStateFlowData() {
job = lifecycleScope.launch {
viewModel.stateFlowData.collect {
when (it) {
is ViewStates.Success -> handleSuccess(it.data)
}
}
}
}
in ViewModel :
private val _stateFlowData = MutableStateFlow<ViewStates<Model>>(ViewStates.Idle)
val stateFlowData: StateFlow<ViewStates<Model>> get() = _stateFlowData
but when I go to next fragment and back to this fragment again, flow collect again.
I cancel the job in onStop() lifecycle method of fragment :
override fun onStop() {
job?.cancel()
super.onStop()
}
but not cancel and collect again!!!
This happens even when I leave the activity (when the viewmodel is cleared) and come back to activity again!!!
How can I do this so that I can prevent the collecting of flow ?
Well you have to know something about coroutine. If we just call cancel, it doesn’t mean that the coroutine work will just stop. If you’re performing some relatively heavy computation, like reading from multiple files, there’s nothing that automatically stops your code from running.
You need to make sure that all the coroutine work you’re implementing is cooperative with cancellation, therefore you need to check for cancellation periodically or before beginning any long running work. Try to add check before handling a result.
job = lifecycleScope.launch {
viewModel.stateFlowData.collect {
ensureActive()
when (it) {
is ViewStates.Success -> handleSuccess(it.data)
}
}
}
}
For more info take a look on this article https://medium.com/androiddevelopers/cancellation-in-coroutines-aa6b90163629
I have a UseCase and remote repository that return Flow in a loop and I collect the result of UseCase in the ViewModel like this:
viewModelScope.launch {
useCase.updatePeriodically().collect { result ->
when (result.status) {
Result.Status.ERROR -> {
errorModel.value = result.errorModel
}
Result.Status.SUCCESS -> {
items.value = result.data
}
Result.Status.LOADING -> {
loading.value = true
}
}
}
}
the problem is when the app is in the background (minimized) flow continues working. so can I pause it when the app is in the background and resume it when the app comes back to the foreground?
and also I don't want to observe the data in my view (fragment or activity).
I'd play around with the stateIn operator and the way I'm currently consuming the flow in the view.
Something like:
val state = useCase.updatePeriodically().map { ... }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed, initialValue)
And consume it from the View like:
viewModel.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
}
.launchIn(lifecycleScope)
For other potential ways on how to collect flows from the UI: https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
EDIT:
If you don't want to consume it from the view, you still have to signal for the VM that your View is in the background currently.
Something like:
private var job: Job? = null
fun start(){
job = viewModelScope.launch {
state.collect { ... }
}
}
fun stop(){
job?.cancel()
}
Even if the viewModelScope is cancelled, the flow will continue to collect because it is not cooperative to cancellation.
To make a flow cancellable, you can do one of the following things:
In the collect lambda, call currentCoroutineContext().ensureActive() to make sure the context in which the flow is being collected is still active. This will however throw a CancellableException, which you will need to catch, if the coroutine scope was cancelled already (viewModel scope for your case.)
You can use cancellable() operator as follows:
myFlow.cancellable().collect { //do stuff here.. }
And you can call cancel() whenever you want to cancel the flow.
For official documentation on cancelling the flow see:
https://kotlinlang.org/docs/flow.html#flow-cancellation-checks
I believe you want something like this
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
state.collect {
}
}
}
Here's an execellent article on repeatOnLifecyle: https://medium.com/androiddevelopers/repeatonlifecycle-api-design-story-8670d1a7d333