androidandroid-recyclerviewandroid-windowmanagerandroid-diffutils

RecyclerView inside WindowManager does not update


I try to create a notification overlay inside my application that can show notifications about certain application wise important events. I decided to use a RecyclerView which will be drown directly on WindowManager. This works fine for showing initial items, however the the items don't get updated. Below is my implementation. So when I call start the not1 and not2 are shown, but when removeNotification function get called, the notification is actually being removed and a correct list is being submitted to the adapter, but the view on screen does not update. However if I add windowManager.updateViewLayout(recyclerView, layoutParams) after submitList inside removeNotification, everything seems to work as expected, but this time I am loosing RV animations..

As this is the first time I work with WindowManager directly, I am quite confused. Can someone help me to figure out what's going on and how can I achieve what I want to, if only that's possible.

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)
    }
}

Edited

Well, I found out that the issue is not in WindowManager but rather in DiffUtils, but cannot understand what's wrong with it, as it is very simple one, and I implemented such DiffUtils a lot of times, anyways, I'll post the code here if you could have any idea on why this does not work:

    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
        }
    }

What can every be wrong in such a simple construction? I am going crazy already and want to throw away this DiffUtils and implement the old school notifyItemRemoved

Edited 2

So the answer offered by @IamFr0ssT fixed the issue. I dig a bit deeper to see why this happens and the reason is in androidx.recyclerview.widget.AsyncListDiffer class in main submitList function. It is doing the following check there:

if (newList == mList) {
            // nothing to do (Note - still had to inc generation, since may have ongoing work)
            if (commitCallback != null) {
                commitCallback.run();
            }
            return;
        }

so my diff was never being even tried to be calculated as I was submitting the same reference of the list.

what the additional toList() function suggested by @IamFr0ssT did, is created a new instance of the list thus making the differ to calculate my diff. If you go deeper inside toList() function, it eventually creates a new instance of an ArrayList based on provided list ...return ArrayList(this)

So well, this issue had nothing to do with WindowManager just the DiffUtil


Solution

  • You are passing the MutableList notifications to the adapter, and the adapter is not making a copy of the list, it is just using the same list.

    When you edit the list in your removeNotification callback, you are editing the list that the adapter is using.

    Because of that, when the diff is being calculated, it is comparing the list that it thinks is currently displayed, but is not, to itself. Thus no diff and no notifyItemRemoved or other events.

    What you can do to fix it, I think, is just call .toList() on the mutable list when you call submitList():

    class NotificationOverlay(private val context: Context) {
        ...
    
        private fun removeNotification(notification: NotificationItem){
            notifications.remove(notification)
            notificationsAdapter.submitList(notifications.toList())
            if(notificationsAdapter.currentList.isEmpty()){
                windowManager.removeView(recyclerView)
            }
        }
    
        fun show(){
            windowManager.addView(recyclerView, layoutParams)
    
            notificationsAdapter.submitList(notifications.toList())
        }
    }
    

    Also, how do you get NotificationItem.id? It should be different for each entry.