4

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.

Jess
  • 85
  • 1
  • 8
  • @CommonsWare Is there any safe way to pass the data to RemoteServiceView? – Jess Jul 24 '22 at 10:47
  • launching inside `runBlocking` doesn't do what you think it does. `runBlocking` waits for all child coroutines to finish, so the `job` you're trying to get a hold on will always be completed when `refreshData()` returns – Joffrey Jul 24 '22 at 13:36
  • @Joffrey In that case I don't have to keep a reference of job and cancel it.?. Will it anyway canceled after the runBlocking() returns? But what happens if server takes too much time and RemoteServiceView calls it OnDestroy() will there be any leak?? Do you see any other issues in above code? – Jess Jul 24 '22 at 14:04
  • Can `onDestroy` really be called if `onDataSetChanged()` is still holding the thread anyway? – Joffrey Jul 24 '22 at 17:28
  • Is `fetchFromServer` different from `refreshData` in your code? If not, where is `refreshData` called from? – Joffrey Jul 24 '22 at 17:28
  • @Joffrey 1.onDestory() seems to be called.. attached log in above question 2. fetchFromServer & refreshData both are same.. updated question – Jess Jul 25 '22 at 04:20
  • @CommonsWare Could you please look into this question? – Jess Jul 27 '22 at 06:29

2 Answers2

2

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.

Joffrey
  • 32,348
  • 6
  • 68
  • 100
  • I can't launch the another coroutine without blocking the current thread because If I do onDataSetChangedMethod will return and the getView() method of service will be called immediately before the data is fetched from the server and there is no easy way to refresh the view after data is fetched – Jess Jul 25 '22 at 04:28
  • Any Info on these? – Jess Jul 28 '22 at 05:03
1

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

Md. Rafsan Biswas
  • 128
  • 1
  • 2
  • 8
  • 1
    RemoteServiceView Doesn't have any Lifecycle scope I guess..and onDataSetChanged will run on binder thread not on UI thread... – Jess Jul 24 '22 at 11:49
  • yes, @Jess this is evident from the line that we can do heavy lifting task in OnDataSetchanged method – Vikas Pandey Oct 13 '22 at 15:05