How to delay in fragment and cancel in onPause - android

I just want to delay a task in a fragment and if the app goes to the background while the delay is running the scope should never resume when the app comes to the foreground:
With following 2 approaches both will execute once the app comes back again, but I want that this never returns once the app was in the background. How to achieve that?
lifecycleScope.launch {
lifecycle.whenResumed {
Timber.d("before delay 1")
delay(15000)
Timber.d("after delay 1")
}
}
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
Timber.d("before delay 2")
delay(15000)
Timber.d("after delay 2")
}
}
kotlinx.coroutines.delay()

A Job launched by lifecycleScope will not be canceled until lifecycle destroy
https://developer.android.com/reference/kotlin/androidx/lifecycle/LifecycleCoroutineScope
If you need to run it once you have to write something like this
private var delayedJob: Job? = null
override fun onResume() {
if(delayedJob == null) {
delayedJob = lifecycleScope.launch {
Timber.d("before delay")
delay(15000)
Timber.d("after delay")
}
}
}
override fun onPause() {
delayedJob?.cancel()
}

Related

Kotlin coroutine that repeats with delay until break

I'm pretty new to Kotlin and I need coroutine that could be described as while loop like this:
while (true) {
if (SomeHardwareDevice.isInitialized()) {
updateUi() // UI thread
break
} else {
delay(50) // background thread
}
}
or just like this:
while (true) {
delay(50) // background thread
if (SomeHardwareDevice.isInitialized()) {
updateUi() // UI thread
break
}
}
At this moment I have something like this:
lifecycleScope.launch(Dispatchers.IO) {
var done = false;
while (!done) {
if (mainActivity!!.isRfInitialized) {
withContext(Dispatchers.Main) {
readRfPower()
unlockRfControlWidgets()
done = true;
}
}
delay(50)
}
}
But I don't trust it, because I think that delay does not guarantee that code that runs on Dispatchers.Main has finished.
Why do I need this?
I have MainActivity that in OnCreateView begins Some-Hardware-Device initialization (device is connected via UART, needs few seconds to initialize). During this initialization user may navigate to a Fragment that requires Some-Hardware-Device to be initialized to show data from it. If SomeHardwareDevice is not ready yet - I have to postpone initial loading data into Widgets on my Fragment.
I want to avoid event-driven mechanism that could notify my Fragment that SomeHardwareDevice has been initialized, because that would be complicated (Fragment may not exist yet when SomeHardwareDevice completes initialization.
I also want to avoid changes in my navigation, where I could block/disable navigation to fragment before Some-Hardware-Device is initialized.
I would write it like the following:
lifecycleScope.launch {
while (true) {
if (mainActivity?.isRfInitialized == true) {
readRfPower()
unlockRfControlWidgets()
break;
}
delay(50)
}
}
Please not that the coroutine is launched on Dispatchers.Main context, you don't need to switch contexts withContext(Dispatchers.Main).
If the reason why you used Dispatchers.IO is that mainActivity!!.isRfInitialized blocks the Main Thread then it can be rewritten as the following:
private suspend fun isDeviceInitialized() = withContext(Dispatchers.IO) {
return#withContext mainActivity?.isRfInitialized == true
}
lifecycleScope.launch {
while (true) {
if (isDeviceInitialized()) {
readRfPower()
unlockRfControlWidgets()
break;
}
delay(50)
}
}
Or we can even simplify it a little bit:
lifecycleScope.launch {
while (!isDeviceInitialized()) {
delay(50)
}
readRfPower()
unlockRfControlWidgets()
}

how to avoid using GlobalScope

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.

Kotlin Coroutine Flow: Limit the number of collector

Is there a way to limit the number of collector in a function that returns a Flow using flow builder?
I have this public method in a ViewModel
fun fetchAssets(limit: String) {
viewModelScope.launch {
withContext(Dispatchers.IO){
getAssetsUseCase(AppConfigs.ASSET_PARAMS, limit).onEach {
when (it) {
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
This method is called on ViewModel's init block, but can also be called manually on UI.
This flow emits value every 10 seconds.
Repository
override fun fetchAssets(
query: String,
limit: String
) = flow {
while (true) {
try {
interceptor.baseUrl = AppConfigs.ASSET_BASE_URL
emit(RequestStatus.Loading())
val domainModel = mapper.mapToDomainModel(service.getAssetItems(query, limit))
emit(RequestStatus.Success(domainModel))
} catch (e: HttpException) {
emit(RequestStatus.Failed(e))
} catch (e: IOException) {
emit(RequestStatus.Failed(e))
}
delay(10_000)
}
}
Unfortunately every time fetch() was invoke from UI, I noticed that it creates another collectors thus can ended up having tons of collector which is really bad and incorrect.
The idea is having a flow that emits value every 10 seconds but can also be invoke manually via UI for immediate data update without having multiple collectors.
You seem to misunderstand what does it mean to collect the flow or you misuse the collect operation. By collecting the flow we mean we observe it for changes. But you try to use collect() to introduce changes to the flow, which can't really work. It just starts another flow in the background.
You should collect the flow only once, so keep it inside init or wherever it is appropriate for your case. Then you need to update the logic of the flow to make it possible to trigger reloading on demand. There are many ways to do it and the solution will differ depending whether you need to reset the timer on manual update or not. For example, we can use the channel to notify the flow about the need to reload:
val reloadChannel = Channel<Unit>(Channel.CONFLATED)
fun fetchAssets(
query: String,
limit: String
) = flow {
while (true) {
try {
...
}
withTimeoutOrNull(10.seconds) { reloadChannel.receive() } // replace `delay()` with this
}
}
fun reload() {
reloadChannel.trySend(Unit)
}
Whenever you need to trigger the manual reload, do not start another flow or invoke another collect() operation, but instead just invoke reload(). Then the flow that is already being collected, will start reloading and will emit state changes.
This solution resets the timer on manual reload, which I believe is better for the user experience.
I ended up moving the timer on ViewModel as I can request on demand fetch while also not having multiple collectors that runs at the same time.
private var job: Job? = null
private val _assetState = defaultMutableSharedFlow<AssetState>()
fun getAssetState() = _assetState.asSharedFlow()
init {
job = viewModelScope.launch {
while(true) {
if (lifecycleState == LifeCycleState.ON_START || lifecycleState == LifeCycleState.ON_RESUME)
fetchAssets()
delay(10_000)
}
}
}
fun fetchAssets() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).onEach {
when(it){
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
override fun onCleared() {
job?.cancel()
super.onCleared()
}
Please correct me if this one is a code smell.

How to pause/stop collecting/emitting data in a Flow while app minimised?

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

how to pause and resume a kotlin coroutine when touching a view?

In my app I want to display a splash screen and after 4 seconds go to HomeActivity. However, I would like to pause the execution by touching imageview imageSplash and resume when releasing. How to do this?
GlobalScope.launch(context = Dispatchers.Main) {
imageSplash.setOnTouchListener { view, motionEvent ->
when(motionEvent.action){
MotionEvent.ACTION_DOWN ->{
true
}
MotionEvent.ACTION_UP->{
true
}
else ->{
false
}
}
}
delay(4000)
val intent = Intent(this#SplashActivity, HomeActivity::class.java)
startActivity(intent)
finish()
}
Kotlin coroutine is of a type kotlinx.coroutines.Job.
That class does not have methods to pause or resume a job.
Alternatively:
You need to write a logic with 2 coroutines.
One that starts HomeActivity after 4 seconds.
Second that listens ACTION_DOWN and ACTION_UP motion events. On ACTION_DOWN you cancel the first coroutine Job, and after ACTION_UP (after N seconds) you show HomeActivity from a second coroutine.
A bit late, but hopefully it will help.
As mentioned by #I.Step, coroutines do not offer functionality for pausing and resuming so, in this case, a simple flag like var isPaused: Boolean could work. You'll just need to check it while the coroutine is running.
Example:
private var isPaused = false
GlobalScope.launch(...) {
while (isActive)
{
if (!isPaused)
{
// do your work here
}
}
}
fun pause()
{
isPaused = true
}
fun resume()
{
isPaused = false
}

Categories

Resources