Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Widget ListView not refreshing when onDatasetChanged method contains async network call Android

I would like to load a list of cars from the internet into a ListView in a Widget. When I am requesting the cars in the onDatasetChanged method, it does not update the list. When I however manually click my refresh button, it does refresh it. What could be the problem? In order to provide as much information as possible, I put the complete source into a ZIP file, which can be unzipped and be opened in Android Studio. I will embed it down below too. Note: I am using RxJava for the async requests and the code is written in Kotlin, although the question is not per sé language bound.

CarsWidgetProvider

class CarsWidgetProvider : AppWidgetProvider() {
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
        Log.d("CAR_LOG", "Updating")
        val widgets = appWidgetIds.size

        for (i in 0 until widgets) {
            Log.d("CAR_LOG", "Updating number $i")
            val appWidgetId = appWidgetIds[i]

            // Get the layout
            val views = RemoteViews(context.packageName, R.layout.widget_layout)
            val otherIntent = Intent(context, ListViewWidgetService::class.java)
            views.setRemoteAdapter(R.id.cars_list, otherIntent)

            // Set an onclick listener for the action and the refresh button
            views.setPendingIntentTemplate(R.id.cars_list, getPendingSelfIntent(context, "action"))
            views.setOnClickPendingIntent(R.id.refresh, getPendingSelfIntent(context, "refresh"))

            // Tell the AppWidgetManager to perform an update on the current app widget
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }

    private fun onUpdate(context: Context) {
        val appWidgetManager = AppWidgetManager.getInstance(context)
        val thisAppWidgetComponentName = ComponentName(context.packageName, javaClass.name)
        val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidgetComponentName)
        appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.cars_list)
        onUpdate(context, appWidgetManager, appWidgetIds)
    }

    private fun getPendingSelfIntent(context: Context, action: String): PendingIntent {
        val intent = Intent(context, javaClass)
        intent.action = action
        return PendingIntent.getBroadcast(context, 0, intent, 0)
    }

    @SuppressLint("CheckResult")
    override fun onReceive(context: Context?, intent: Intent?) {
        super.onReceive(context, intent)
        if (context != null) {
            if ("action" == intent!!.action) {
                val car = intent.getSerializableExtra("car") as Car

                // Toggle car state
                Api.toggleCar(car)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe {
                            Log.d("CAR_LOG", "We've got a new state: $it")
                            car.state = it
                            onUpdate(context)
                        }

            } else if ("refresh" == intent.action) {
                onUpdate(context)
            }
        }
    }
}

ListViewWidgetService

class ListViewWidgetService : RemoteViewsService() {
    override fun onGetViewFactory(intent: Intent): RemoteViewsService.RemoteViewsFactory {
        return ListViewRemoteViewsFactory(this.applicationContext, intent)
    }
}

internal class ListViewRemoteViewsFactory(private val context: Context, intent: Intent) : RemoteViewsService.RemoteViewsFactory {
    private var cars: ArrayList<Car> = ArrayList()

    override fun onCreate() {
        Log.d("CAR_LOG", "Oncreate")
        cars = ArrayList()
    }

    override fun getViewAt(position: Int): RemoteViews {
        val remoteViews = RemoteViews(context.packageName, R.layout.widget_list_item_car)

        // Get the current car
        val car = cars[position]
        Log.d("CAR_LOG", "Get view at $position")
        Log.d("CAR_LOG", "Car: $car")

        // Fill the list item with data
        remoteViews.setTextViewText(R.id.title, car.title)
        remoteViews.setTextViewText(R.id.description, car.model)
        remoteViews.setTextViewText(R.id.action, "${car.state}")
        remoteViews.setInt(R.id.action, "setBackgroundColor",
                if (car.state == 1) context.getColor(R.color.colorGreen) else context.getColor(R.color.colorRed))

        val extras = Bundle()
        extras.putSerializable("car", car)

        val fillInIntent = Intent()
        fillInIntent.putExtras(extras)

        remoteViews.setOnClickFillInIntent(R.id.activity_chooser_view_content, fillInIntent)
        return remoteViews
    }

    override fun getCount(): Int {
        Log.d("CAR_LOG", "Counting")
        return cars.size
    }

    @SuppressLint("CheckResult")
    override fun onDataSetChanged() {
        Log.d("CAR_LOG", "Data set changed")

        // Get a token
        Api.getToken()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe { token ->
                    if (token.isNotEmpty()) {
                        Log.d("CAR_LOG", "Retrieved token")
                        DataModel.token = token

                        // Get the cars
                        Api.getCars()
                                .subscribeOn(Schedulers.io())
                                .observeOn(AndroidSchedulers.mainThread())
                                .subscribe {
                                    cars = it as ArrayList<Car>

                                    Log.d("CAR_LOG", "Found cars: $cars")
                                }
                    } else {
                        Api.getCars()
                                .subscribeOn(Schedulers.io())
                                .observeOn(AndroidSchedulers.mainThread())
                                .subscribe {
                                    cars = it as ArrayList<Car>

                                    Log.d("CAR_LOG", "Found cars: $cars")
                                }
                    }
                }
    }

    override fun getViewTypeCount(): Int {
        Log.d("CAR_LOG", "Get view type count")
        return 1
    }

    override fun getItemId(position: Int): Long {
        Log.d("CAR_LOG", "Get item ID")
        return position.toLong()
    }

    override fun onDestroy() {
        Log.d("CAR_LOG", "On Destroy")
        cars.clear()
    }

    override fun hasStableIds(): Boolean {
        Log.d("CAR_LOG", "Has stable IDs")
        return true
    }

    override fun getLoadingView(): RemoteViews? {
        Log.d("CAR_LOG", "Get loading view")
        return null
    }
}

DataModel

object DataModel {
    var token: String = ""
    var currentCar: Car = Car()
    var cars: ArrayList<Car> = ArrayList()

    init {
        Log.d("CAR_LOG", "Creating data model")
    }

    override fun toString(): String {
        return "Token : $token \n currentCar: $currentCar \n Cars: $cars"
    }
}

Car

class Car : Serializable {
    var id : String = ""
    var title: String = ""
    var model: String = ""
    var state: Int = 0
}


EDIT

After I received some answers, I started figuring out a temporary fix, which is calling .get() on an AsyncTask, which makes it synchronous

@SuppressLint("CheckResult")
override fun onDataSetChanged() {
    cars = GetCars().execute().get() as ArrayList<Car>
}

class GetCars : AsyncTask<String, Void, List<Car>>() {
    override fun doInBackground(vararg params: String?): List<Device> {
        return Api.getCars().blockingFirst();
    }
}
like image 928
Jason Avatar asked Jul 29 '18 13:07

Jason


People also ask

Why doesn't my listview refresh after changing the adapter?

If the adapter is already set, setting it again will not refresh the listview. Instead first check if the listview has a adapter and then call the appropriate method. I think its not a very good idea to create a new instance of the adapter while setting the list view.

Why is my listview empty after notifyappwidgetviewdatachanged?

As you can see, after notifyAppWidgetViewDataChanged () onDataSetChange () will be called, but your call is asynchronous, that's why RemoteViewFactory going further to FetchMetaData (check image). So, on first launch of the widget, you initialise cars as ArrayList (), that's why your ListView is empty.

What does ondatasetchanged() do in Android?

As you can see, onDataSetChanged () is intended for populate some data from database or something like that. You have some options how to update your widget: Save in database and fetch in onDataSetChange () by calling notifyAppWidgetViewDataChanged () in IntentService.

How to refresh listview items in itemadapter?

You are assigning reloaded items to global variable items in onResume (), but this will not reflect in ItemAdapter class, because it has its own instance variable called 'items'. For refreshing ListView, add a refresh () in ItemAdapter class which accepts list data i.e items Show activity on this post.


2 Answers

As said @ferini your rx chain is an asynchronous call, so what actually is going on when you call appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.cars_list) : enter image description here

As you can see, after notifyAppWidgetViewDataChanged() onDataSetChange() will be called, but your call is asynchronous, that's why RemoteViewFactory going further to FetchMetaData (check image). So, on first launch of the widget, you initialise cars as ArrayList(), that's why your ListView is empty. When you send refresh action, your cars not empty, cuz your asynchronous call that you start in onDataSetChange(), already fetch data and populate your cars, that's why on refresh action you populate ListView by last asynchronous call.

As you can see, onDataSetChanged() is intended for populate some data from database or something like that. You have some options how to update your widget:

  1. Save in database and fetch in onDataSetChange() by calling notifyAppWidgetViewDataChanged() in IntentService. Process full update and make new intent for listview adapter with parcelable data and setRemoteAdapter() on RemoteView, so in onCreate RemoteViewsService, pass intent to RemoteViewsFactory where you can get from intent your arraylist of cars and set it to RemoteViewsFactory, in this case you don't need onDataSetChange() callback at all.

  2. Just make all synchronous (blocking) calls in onDataSetChange() since it's not a UI thread.

You can easily check this by making blocking rx call in onDataSetChanged() and your widget will work as you want.

override fun onDataSetChanged() {
    Log.d("CAR_LOG", "Data set changed")
    cars = Api.getCars().blockingFirst() as ArrayList<Car>
}

UPDATE: Actually if you will log current thread in RemoteViewFactory callbacks, u will see that only onCreate() is on main thread, other callbacks working on thread of binder thread pool. So it's ok to make synchronous calls in onDataSetChanged() since it's not on UI thread, so making blocking calls it's ok, u will not get ANR

like image 154
HeyAlex Avatar answered Sep 18 '22 08:09

HeyAlex


The problem seems to be in the async request. The views are refreshed after onDataSetChanged() completes. However, since the request in asynchronous, the results come after that.

As per https://developer.android.com/guide/topics/appwidgets/#fresh:

"Note that you can perform processing-intensive operations synchronously within the onDataSetChanged() callback. You are guaranteed that this call will be completed before the metadata or view data is fetched from the RemoteViewsFactory."

Try to make your request synchronous.

like image 39
ferini Avatar answered Sep 19 '22 08:09

ferini