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();
}
}
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.
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.
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.
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.
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)
:
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:
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.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With