当 onDatasetChanged 方法包含异步网络调用 Android 时,小部件 ListView 不刷新
Posted
技术标签:
【中文标题】当 onDatasetChanged 方法包含异步网络调用 Android 时,小部件 ListView 不刷新【英文标题】:Widget ListView not refreshing when onDatasetChanged method contains async network call Android 【发布时间】:2019-01-05 22:30:16 【问题描述】:我想将互联网上的汽车列表加载到小部件中的 ListView 中。当我在 onDatasetChanged 方法中请求汽车时,它不会更新列表。但是,当我手动单击刷新按钮时,它会刷新它。可能是什么问题呢?为了提供尽可能多的信息,我放了complete source into a ZIP file,可以在android Studio中解压并打开。我也会把它嵌入到下面。 注意:我将 RxJava 用于异步请求,并且代码是用 Kotlin 编写的,尽管问题不受语言限制。
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
数据模型
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"
汽车
class Car : Serializable
var id : String = ""
var title: String = ""
var model: String = ""
var state: Int = 0
编辑
收到一些答案后,我开始想出一个临时修复方法,即在 AsyncTask 上调用 .get(),使其同步
@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();
【问题讨论】:
强烈建议您不要在 AppWidgetProvider 中进行 api 调用。 AppWidgetProvider 只是一个 BroadcastReceiver,如果 api 需要几秒钟才能完成,它可能会在完成之前终止。 developer.android.com/guide/topics/appwidgets/… 【参考方案1】:正如@ferini 所说,您的 rx 链是一个异步调用,所以当您调用 appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.cars_list)
时实际发生了什么:
如您所见,在 notifyAppWidgetViewDataChanged() 之后将调用 onDataSetChange(),但您的调用是异步的,这就是 RemoteViewFactory 进一步转到 FetchMetaData 的原因(查看图片)。因此,在第一次启动小部件时,您将 cars 初始化为 ArrayList(),这就是您的 ListView 为空的原因。当您发送 refresh 操作时,您的汽车不是空的,因为您在 onDataSetChange() 中启动的异步调用已经获取数据并填充您的 cars,这就是刷新操作的原因您通过上次异步调用填充 ListView。
如您所见,onDataSetChanged() 旨在从数据库或类似的东西中填充一些数据。您有一些选择如何更新您的小部件:
保存在数据库中并通过调用获取 onDataSetChange() IntentService 中的 notifyAppWidgetViewDataChanged()。处理完整更新并为列表视图适配器制定新意图 RemoteView 上的 parcelable 数据和 setRemoteAdapter(),所以在 onCreate RemoteViewsService,将 Intent 传递给 RemoteViewsFactory,您可以从 Intent 中获取您的汽车数组列表并将其设置为 RemoteViewsFactory,在这种情况下你不需要 onDataSetChange() 完全回调。
只需在 onDataSetChange() 中进行所有同步(阻塞)调用,因为它不是 UI 线程。
您可以通过在 onDataSetChanged() 中进行阻塞 rx 调用轻松检查这一点,您的小部件将按您的意愿工作。
override fun onDataSetChanged()
Log.d("CAR_LOG", "Data set changed")
cars = Api.getCars().blockingFirst() as ArrayList<Car>
更新: 实际上,如果您将在 RemoteViewFactory 回调中记录当前线程,您将看到只有 onCreate() 在主线程上,其他回调在活页夹线程池的线程上工作。所以可以在 onDataSetChanged() 中进行同步调用,因为它不在 UI 线程上,所以进行阻塞调用是可以的,你不会得到 ANR
【讨论】:
随着数据的快速变化,数据库解决方案不是一个选择。我不太明白你所说的第二个是什么意思,你是在告诉我以全新的意图完全刷新服务吗?使用blockingFirst() 确实可以解决问题,但它不会也阻塞UI,这可能会导致未来的ANR?当然,它也会给 NetworkOnMainThread 异常,所以.. 因为我不建议使用blockingFirst() :D - 这只是为了了解为什么您的列表没有根据您的需要更新。最好的解决方案(我也在生产中使用它)是从 IntentService 更新并将您的 parceble 数据传递给 RemoteFactory。 会调查的。现在我正在使用它,尽管调用 .get() 当然不应该这样做! (见编辑) @jason 我错了,所以我的回答有一些更新。它不会给出 NetworkOnMainThread 异常,如果你要进行阻塞调用,哪里没有 ANR,因为 RemoteViewFactory 的所有方法(onCreate 除外)的调用将由绑定线程池完成。实际上喜欢你的问题,希望现在很清楚:D【参考方案2】:问题似乎出在异步请求中。 onDataSetChanged()
完成后会刷新视图。但是,由于请求是异步的,结果是在那之后。
根据https://developer.android.com/guide/topics/appwidgets/#fresh:
“请注意,您可以在 onDataSetChanged() 回调中同步执行处理密集型操作。您可以保证在从 RemoteViewsFactory 获取元数据或视图数据之前完成此调用。”
尝试使您的请求同步。
【讨论】:
在我看来,使请求同步会阻塞 UI 并最终导致 ANR? 我没有尝试过,但由于文档说在那里做密集工作是可以的,我猜onDataSetChanged()
不会在主/UI线程上运行。以上是关于当 onDatasetChanged 方法包含异步网络调用 Android 时,小部件 ListView 不刷新的主要内容,如果未能解决你的问题,请参考以下文章
Android 小部件:updateAppWidget() 未调用 onDataSetChanged
每个 notifyAppWidgetViewDataChanged() 仅调用一次 RemoteViewFactory onDataSetChanged() [重复]