Is it safe to use Coroutines runBlocking() inside onDataSetChanged of RemoteViewsFactory?

1k Views Asked by At

I am trying to fetch some latest movie images and display them in the Android app widgets, As per the requirement, the widget should refresh every hour to get the latest movies images.

Since I am using coroutines throughout the app, I need to use the existing retrofit services with suspend function to get the data.

As per the Android documentation, it's safe to do heavy operations inside onDataSetChanged of RemoteViewsFactory So I am using this method itself to fetch data from service instead of WorkManger as it has its own drawbacks. See here (https://commonsware.com/blog/2018/11/24/workmanager-app-widgets-side-effects.html)

Called when notifyDataSetChanged() is triggered on the remote adapter. This allows a RemoteViewsFactory to respond to data changes by updating any internal references. Note: expensive tasks can be safely performed synchronously within this method. In the interim, the old data will be displayed within the widget.

In the Below code snippet, I am using runBlocking() to make a blocking retrofit call... Otherwise get view will be triggered before the data is fetched. Is this safe?

Note:Also the adapter is noticed in the onUpdate method of MyWidgetProvider every hour

override fun onUpdate(
    context: Context,
    appWidgetManager: AppWidgetManager,
    appWidgetIds: IntArray
) {
   ....
   appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.listViewMovies)
   .....
 }

Code Sample: MyRemoteViewsFactory : RemoteViewsFactory {

override fun onDataSetChanged() {
  try {
     fetchFromServer()
  } catch (e: Exception) {
    clear()      
  }
}

private fun fetchFromServer() = runBlocking {
 job = launch {
     service.getMovieList?.also {
       data = it
     }
   }
}

private fun clear(){
    job?.cancel()
    job = null
    data = null
    log("Cleared")
}

 override fun onDestroy() {
    clear()
 }

Console log shows onDestory() s being called... but in different binder.

 onCreate _main
 1 _Binder:11940_5
 2 _Binder:11940_5
 3 _Binder:11940_5
 4 _Binder:11940_5
 5 _Binder:11940_5
 6 _Binder:11940_5
 7 _Binder:11940_5
 8 _Binder:11940_5
 9 _Binder:11940_5
 10 _Binder:11940_5

 11 _Binder:11940_5
 onDestroy _Binder:11940_2
 Clear called _Binder:11940_2

So My queries:

  1. Is it safe to use runBlocking inside onDataSetChanged Method?
  2. Is it safe to create nested coroutine inside runBlocking to cancel the Coroutines? is there any better way?
  3. Is it safe to fetch the data from the network inside onDataSetChanged().
  4. I can't make async call inside onDataSetChanged() .. So is there anyway we can handle without using runBlocking()

NOTE: This question specially For Android App Widget RemoteViewsFactory onDataSetChanged() implementation.

2

There are 2 best solutions below

2
On

1- Is it safe to use runBlocking inside onDataSetChanged Method?

3- Is it safe to fetch the data from the network inside onDataSetChanged().

The documentation specifically says that "expensive tasks can be safely performed synchronously within this method. In the interim, the old data will be displayed within the widget."

So the answer to questions 1 and 3 is yes, it's ok to use runBlocking there and to make a network call.

2- Is it safe to create nested coroutine inside runBlocking to cancel the Coroutines? is there any better way?

There might not be a real reason to do that, because runBlocking blocks the thread until the child coroutines are done, so you wouldn't give control back to the caller anyway until the work is finished. See next question.

4- I can't make async call inside onDataSetChanged() .. So is there anyway we can handle without using runBlocking()

If you want to launch coroutines that you can later cancel (in case of concurrent use of onDataSetChanged and clear - which is yet to be confirmed), you should instead declare a CoroutineScope (as a property of your class) and launch coroutines using that scope instead of using runBlocking, and then cancel the scope when/where appropriate.

2
On

use viewModel/lifeCycle scopes, runBlocking will block the UI thread.