当 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() [重复]

C# 异步编程

Python怎么测试异步接口

当更新我的 UI 以响应异步操作时,我应该在哪里调用 DispatchQueue?

WCF 资源上的异步调用