Kotlin - Test inner Coroutines with delay using runBlockingTest - android

I have this piece of code :
fun setBlocked(blocked: Boolean) = viewModelScope.launchSafe(::handleExceptionsActive) {
/**
* Show loading after 500 ms to prevent from showing it if cloud functions are hot.
*/
val loadingJob = launch { delay(500L); _loading.value = true }
participantDataSource.setBlockedUser(contact.userId, blocked)
loadingJob.cancelAndJoin()
_loading.value = false
}
It does a Firebase Cloud Function call that might take a few seconds or just a few milliseconds depending on whether the Cloud Function is idle or not. What this code does is it triggers a loading LiveData at true after 500ms and then puts it back at false.
If the call takes a few milliseconds, then loading.value = true will never be called.
If the call takes a few seconds, then loading.value = true will be called.
I want to test the function setBlocked().
The problem with this code is that I don't know how to test these two statements. I know I can test code using delay() with runBlockingTest and advanceTimeBy() but the thing is loadingJob.cancel() will be called before I call advanceTimeBy().
How can I test it ? If you have suggestions on modifying the code to make it testable I'll be glad to here it too.
Edit : replaced cancel() by cancelAndJoin() as Marko Topolnik suggests.

Related

Effect of using GlobalScope.launch while using coroutine

1.
I am using:
override fun updateNotification(mediaSession: MediaSessionCompat) {
if (!PlayerService.IS_RUNNING) return
GlobalScope.launch {
notificationManager.notify(NOTIFICATION_ID, buildNotification(mediaSession))
}
}
I could use:
override fun updateNotification(mediaSession: MediaSessionCompat) {
if (!BeatPlayerService.IS_RUNNING) return
CoroutineScope(Dispatchers.IO).launch {
notificationManager.notify(NOTIFICATION_ID, buildNotification(mediaSession))
}
}
2.
I am using:
GlobalScope.launch {
while (true) {
delay(100)
mediaMediaConnection.mediaController ?: continue
val newTime = mediaMediaConnection.mediaController?.playbackState?.position
if (state == BIND_STATE_BOUND) newTime?.toInt()?.let { update(it) }
if (state == BIND_STATE_CANCELED) break
}
}
I could use:
CoroutineScope(Dispatchers.IO).launch {
while (true) {
delay(100)
mediaMediaConnection.mediaController ?: continue
val newTime = mediaMediaConnection.mediaController?.playbackState?.position
if (state == BIND_STATE_BOUND) newTime?.toInt()?.let { update(it) }
if (state == BIND_STATE_CANCELED) break
}
}
I dont see any visible difference while using GlobalScope.launch versus CoroutineScope().launch in my music app.
Can someone explain which is better to use in my context of 1 and 2
I have seen:
Why not use GlobalScope.launch?
but don't quite understand fully especially in my use case.
Can someone explain which is better to use in my context of 1 and 2
Neither.
The problem with both of them is that they are pretty much unconstrained. They don't respect app's lifecycle which means that if your app gets closed (or user goes away from that screen) while some coroutines were running inside those scopes, they won't stop executing and will leak the coroutines(the app might even crash)
When can my app crash:
GlobalScope.launch {
fetchUserData() // --> this is a suspend function
updateUI() // e.g. progressBar.isVisible = false
}
Here if user navigates away from the screen while fetchUserData was suspended. Then that code won't stop executing and when fetchUserData returns, it will try to update the UI but since the screen has changed, it will throw an exception and your app will crash.
What we want is that coroutines should stop when they are no longer needed. If you use GlobalScope you loose all that control. For the case of a custom CoroutineScope, if you are cancelling it at the right time everything is good otherwise it's also harmful.
What's the alternative?
You can/should use the built-in scopes that android provides like lifecycleScope and viewModelScope. The first one follows the lifecycle of the activity (or fragment) while the second one gets cleaned up when view model is destoryed.
So, that was the general advice. Coming to your particular case,
In your first case, you don't even need a coroutine scope because notify is a very simple function (neither suspend nor blocking)
In the second case, you have an infinite loop that breaks when a particular condition is met. Since you have this code inside a ViewModel, you most likely don't want it to run after the view model gets cleared. If you use CoroutineScope() or GlobalScope you won't be able to control that. So you should use a viewModelScope here so that the infinite loop stops when the work is no longer required.

Synchronize coroutine from incoming messages

I'm using Android Kotlin with the SDK 30 and Coroutine 1.4.1.
I have a function that handles incoming messages to display them on my app in a form of temperature measurement. I use CoroutineScope to process the data and save it in the database. These messages are received from a socket.io connection. The problem is that the messages are not displayed in the correct order when a bulk of data is flowing in.
Now I've looked at my nodejs logs and these messages are sent in the correct order so it can't be that.
I'm using a standard Coroutine function.
See below.
fun receiveTmps(data){
CoroutineScope(IO).launch {
val usersJob = launch {
usersBg(data)
}
}
}
Now I know that with Coroutine I can add a join to wait for the job to finish before starting the next one. But because the messages do not come in at once, but flow continuously over a period of 5 to 20 seconds, it is possible that one message is completed faster than the older one. This causes incorrect order.
My question is, is there any way to handle these tasks 1 by 1 while adding multiple jobs to the list?
Any suggestion or idea is appreciated.
Thank you in advance.
UPDATED:
From what I read from the documentation you should also cancel the channel after its done. So that's going to be tricky to close the channel when messages are flowing in and since I don't have a clear number of what's flowing in I'm having a hard time defining that to the channel. I have tested several ways but most of the examples doesnt work or are outdated.
This is the most basic working example but it always has a defined repeat.
val channel = Channel<String>(UNLIMITED)
fun receiveTmps(data:String){
CoroutineScope(Dispatchers.Default).launch {
channel.send(data)
}
}
#ExperimentalCoroutinesApi
fun main() = runBlocking<Unit> {
launch {
// while(!channel.isClosedForReceive){
// val x = channel.receive()
// Log.d("deb", "Temperature.. "+ x)
// }
repeat(3) {
val x = channel.receive()
Log.d("deb", "Temperature.. "+ x)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
receiveTmps("10")
receiveTmps("30")
// Many more...
main()
}
If we need to process some events sequentially then typical solution is to create a queue of events and start a single consumer to process them. In the case of coroutines we can use Channel as a queue and launch a coroutine running in a loop that will be our consumer.
I'm not very familiar with Android, so I may miss something, but it should be something along lines:
fun receiveTmps(data:String){
channel.trySend(data).getOrThrow()
}
fun main() {
lifecycleScope.launch(Dispatchers.Default) {
for (tmp in channel) {
...
}
}
}
My assumption is that you want to stop processing events when the activity/service will be destroyed, ignoring all temps that are still waiting in the queue.

How do I cancel a kotlin coroutine for a blocking download operation

Here is the piece of code I have running on Dispatcher.IO thread pool
suspend fun download(rquest: YoutubleDLRequest){
withContext(Dispatchers.IO){
YoutubeDL.init().execute(request) // downloads media in background
}
}
Now, when user clicks a button, the download starts - it either fails due to network error, or completes.
My question is how do I cancel this download operation?
I have another button to allow the user to cancel the download operation.
If I wrap withContext in a launch and keep a reference to the job and then try to
cancel the job on the button click, it will not work. I know, I need another suspension point, I tried calling another suspend function with yield inside a while loop. But, the while loop does not let the download code execute at all.
I don't understand, how cancel would be implemented in this scenario. Any help would be appreciated.
This answer was written before the original poster changed his question, and is therefore not on point anymore. Sorry.
There are multiple options you could consider, depending on your usecase:
Let's assume your YoutubeDownloader throws a DownloadException when it is canceled:
suspend fun download(rquest: YoutubleDLRequest){
withContext(Dispatchers.IO){
try{
YoutubeDL.init().execute(request) // downloads media in background
Log.d("myTag", "Success"
} catch ( e: DownloadException){
Log.e("myTag", "Download-Failure: $e")
}
}
}
In case you'd like to actively cancel the coroutine, you could save it as a variable, like here in the basic-docs, with more examples here in the cancellation docs:
val myDownloadJob = withContext(Dispatchers.IO){ /* your code here */}
val myDownloadJob = someScope.launch(someDispatcher){ /* your code here */ }
// somewhere else:
myDownloadJob.cancel()

Android background process loading data from webpage every minute

I'm working on an Android app with a constant repeating background process.
From the moment the device starts it should load data off a webpage every minute. It uses XmlPullParser and a simple URL inputstream. It is but 10kb so it isn't that intensive. I believe this kind of task is called Deferred. The information loaded by the process has to be accessible to the Activity once that the user opens the app. The background process also needs to be abled to place a notification once the data shows certain results.
There seem to be multiple methods to achieve this in Android, eg. a JobScheduler, WorkManager or AlarmManager however everything I've tried so far seems to either stop once the activity closes or doesn't run at all. The timing, every minute, also seems to be an issue as for both a repeating job and worker the minimum interval is 15. This one minute doesn't have to be exact. I imagine instead of having a repeating process loading the data once it might be better to have a long running process sleeping for 1m in between loading the data.
I do not have access to the server the application is connecting to. so I can't do a FirebaseMessagingService.
What would be the best way to schedule such a background process?
How can the activity best exchange information with that process?
I'm open for all suggestions,
thank you for your time.
Easy with WorkManager, it's the most encouraged way for Scheduling Repeating background work in Android, see introduction.
As you say, the minimum repeating work request interval is restricted to 15 minutes, the only way to break it is to Repeatedly schedule the one-time work.
1. Setup Your Worker Class:
class ToastShower(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
withContext(Dispatchers.Main) { //ui related work must run in Main thread!!
Toast.makeText(applicationContext, "Hey, I'm Sam! This message will appear every 5 seconds.", Toast.LENGTH_SHORT).show()
}
return Result.success()
}
}
2. Setup Your Custom Application Class:
class WorkManagerApplication : Application() {
private val backgroundScope = CoroutineScope(Dispatchers.Default) //standard background thread
private val applicationContext = this
override fun onCreate() { //called when the app launches (same as Activity)
super.onCreate()
initWork()
}
private fun initWork() {
backgroundScope.launch { //all rnu in background thread
setupToastShowingWork(0) //no delay at first time
observeToastShowingWork() //observe work state changes, see below
}
}
private fun setupToastShowingWork(delayInSeconds: Long) { //must run in background thread
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) //when using WiFi
.build()
val oneTimeRequest = OneTimeWorkRequestBuilder<ToastShower>() //【for breaking 15 minutes limit we have to use one time request】
.setInitialDelay(delayInSeconds, TimeUnit.SECONDS) //customizable delay (interval) time
.setConstraints(constraints)
.build()
WorkManager.getInstance(applicationContext).enqueueUniqueWork( //【must be unique!!】
ToastShower::class.java.simpleName, //work name, use class name for convenient
ExistingWorkPolicy.KEEP, //if new work comes in with same name, discard the new one
oneTimeRequest
)
}
private suspend fun observeToastShowingWork() {
withContext(Dispatchers.Main) { //must run in Main thread for using observeForever
WorkManager.getInstance(applicationContext).getWorkInfosForUniqueWorkLiveData(ToastShower::class.java.simpleName).observeForever {
if (it[0].state == WorkInfo.State.SUCCEEDED) { //when the work is done
backgroundScope.launch { //prevent from running in Main thread
setupToastShowingWork(5) //every 5 seconds
}
}
}
}
}
}
3. Setup AndroidManifest File:
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.workmanagertest">
<application
android:name=".WorkManagerApplication" //【here, must!!!】
...
</application>
</manifest>
By setting up with above, the work (showing Toast in my example) will be executed (or more clearly, schedule and execute) every 5 seconds no matter the app is in foreground or background or killed by system. Only way to stop it is either uninstall or go inside the app's setting to force-close it.
Demo: https://youtu.be/7IsQQppKqFs

Cancel file upload (retrofit) started from coroutine kotlin android

I'm trying to get rid of RxJava2 in my project and replace it with kotlin coroutines. 90% of my RxJava code is no longer exists but I still can`t replace one network request.
User can send photo to backend in my App (multipart form data request using retrofit). User also can cancel photo upload if it was not loaded yet. With RxJava I was able to keep Disposable object of upload request and if it is not disposed yet I could dispose it if user clicked cancel upload button. As result of this action network request was canceled too. So we could save some user traffic and battery.
Is it possible to achieve same logic with kotlin coroutines? In official docs written that coroutines cancellation is cooperative and we need some suspending function in loop (or in between file parts send) in order to stop coroutine.
So is this the case when RxJava is better choice or did I missed something?
If you are using retrofit for your network calls, you can add their coroutines call adapter from here.
You cancel a coroutine's running part by cancelling it's job. For example if you are using launch to launch your coroutine, it returns a Job object which can be cancelled.
val job = launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
job.join() // waits for job's completion
println("main: Now I can quit.")
If you are not using retrofit and using another library, you can retrieve isActive inside you coroutine and cancel the request from your library.
fun main(args: Array<String>) = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val job = launch {
var nextPrintTime = startTime
var i = 0
while (isActive) { // cancellable computation loop
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
}

Categories

Resources