WindowManager内RecyclerView submitList视图不更新问题
问题现象
实现应用内全局通知悬浮层时,选择将RecyclerView直接添加到WindowManager上展示应用级重要事件通知,初始加载状态正常,但存在列表更新失效问题:
- 调用
show()方法时,预置的not1、not2两条初始通知可正常展示 - 调用
removeNotification方法时,通知对象已从数据源移除,也调用了适配器的submitList提交更新后列表,但屏幕视图未同步刷新 - 如果在
submitList后追加windowManager.updateViewLayout(recyclerView, layoutParams),可强制刷新视图,但会完全丢失RecyclerView内置的动画效果
初始实现代码如下:
class NotificationOverlay(private val context: Context) { private val windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager private val layoutParams = WindowManager.LayoutParams().apply { gravity = android.view.Gravity.TOP or android.view.Gravity.CENTER_HORIZONTAL width = WindowManager.LayoutParams.MATCH_PARENT height = WindowManager.LayoutParams.WRAP_CONTENT format = PixelFormat.TRANSLUCENT dimAmount = 0.5f flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL } private val notifications = mutableListOf<NotificationItem>().also { it.addAll(listOf( NotificationItem(title = "not 1", message = "first notification"), NotificationItem(title = "not 2", message = "second notification") )) } private val notificationsAdapter = NotificationAdapter(::removeNotification) private val recyclerView = RecyclerView(context).apply { layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) adapter = notificationsAdapter } private fun removeNotification(notification: NotificationItem){ notifications.remove(notification) notificationsAdapter.submitList(notifications) if(notificationsAdapter.currentList.isEmpty()){ windowManager.removeView(recyclerView) } } fun show(){ windowManager.addView(recyclerView, layoutParams) notificationsAdapter.submitList(notifications) } }
初步排查
最初怀疑是WindowManager的使用逻辑问题,后续排查排除了这个方向,将问题定位到DiffUtil相关逻辑,但自定义的DiffCallback是多次使用的标准实现,未发现明显错误,适配器代码如下:
class NotificationAdapter(private val onCloseClicked: (NotificationItem) -> Unit): ListAdapter<NotificationItem, NotificationAdapter.NotificationViewHolder>(DiffCallback()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationViewHolder { val binding = ItemNotificationOverlayBinding.inflate(LayoutInflater.from(parent.context), parent, false) return NotificationViewHolder(binding, onCloseClicked) } override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) { holder.bind(currentList[position]) } class NotificationViewHolder(private val itemBinding: ItemNotificationOverlayBinding, private val onCloseClicked: (NotificationItem) -> Unit): RecyclerView.ViewHolder(itemBinding.root) { fun bind(item: NotificationItem) { itemBinding.title.text = item.title itemBinding.message.text = item.message itemBinding.close.setOnClickListener { onCloseClicked.invoke(item) } } } class DiffCallback : DiffUtil.ItemCallback<NotificationItem>() { override fun areItemsTheSame(oldItem: NotificationItem, newItem: NotificationItem) = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: NotificationItem, newItem: NotificationItem) = oldItem == newItem } }
排查到这一步时一度打算放弃DiffUtil,改回传统的notifyItemRemoved方法实现列表更新。
根因定位
最终确认问题和WindowManager完全无关,是AsyncListDiffer的使用误区:androidx.recyclerview.widget.AsyncListDiffer的submitList方法开头存在引用相等校验逻辑:
if (newList == mList) { // nothing to do (Note - still had to inc generation, since may have ongoing work) if (commitCallback != null) { commitCallback.run(); } return; }
之前的实现中,每次提交给submitList的都是同一个notifications可变列表对象,和Differ内部持有的列表引用完全一致,因此差异计算流程直接被跳过,不会触发视图刷新。
修复方案
调用submitList时传入原列表的新副本即可,在提交参数后追加toList()调用,创建新的列表实例:
// 修改removeNotification中的提交逻辑 private fun removeNotification(notification: NotificationItem){ notifications.remove(notification) // 传入列表副本,触发diff计算 notificationsAdapter.submitList(notifications.toList()) if(notificationsAdapter.currentList.isEmpty()){ windowManager.removeView(recyclerView) } } // show方法中的初始提交也统一使用该写法 fun show(){ windowManager.addView(recyclerView, layoutParams) notificationsAdapter.submitList(notifications.toList()) }
toList()会基于原列表创建新的ArrayList实例,和原列表引用不同,可正常触发Differ的差异计算,同时保留RecyclerView的内置动画效果。
注意:使用
ListAdapter+AsyncListDiffer时,禁止直接提交可变的原列表对象,必须每次传入新的列表实例,否则会因为引用相等校验直接跳过diff流程,导致列表不更新。
内容的提问来源于stack exchange,提问作者Andranik

