I have some recycler view code in a function that gets called several times as bluetooth devices are scanned. My code is working but I am wondering what unseen effects are occurring from having my recycler view initialization code in a function that gets repeated a lot? I eventually want to update the list rather than replace it via notifyDataSetChanged() but I am unsure how to do that with my current code structure. Any help would be appreciated!
#SuppressLint("MissingPermission", "NotifyDataSetChanged")
fun displayDevices(
scannedDevicesStrings: TreeSet<String>,
deviceMap: HashMap<String, String>
) {
val sortedDeviceMap = deviceMap.toSortedMap()
Log.d(TAG, "displayDevices: ${sortedDeviceMap.entries}")
// Set linear layout manager for the widget.
val linearLayoutManager = LinearLayoutManager(applicationContext)
binding.recyclerviewDevices.layoutManager = linearLayoutManager
// Specify an adapter.
listAdapter = CustomAdapter(scannedDevicesStrings.toList(), sortedDeviceMap, bluetoothManager)
binding.recyclerviewDevices.adapter = listAdapter
listAdapter.notifyDataSetChanged()
// Notify the view to update when data is changed.
if ( binding.recyclerviewDevices.isAttachedToWindow) {
binding.progressBarCyclic.visibility = GONE
}
}
This code calls my CustomAdapter() class which looks like this:
class CustomAdapter(
private val treeSet: List<String>,
private var hashMap: SortedMap<String, String>,
private val bluetoothManager: BluetoothManager
) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.textview_list_item)
val listLayout: FrameLayout = view.findViewById(R.id.item_layout)
val context: Context = view.context
val textView2: TextView = view.findViewById(R.id.textview_list_item_address)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.text_device_row_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val deviceList = hashMap.keys.toList()
val macAddressList = hashMap.values.toList()
holder.textView.text = deviceList.elementAt(position)
holder.textView2.text = macAddressList.elementAt(position)
val selectedDeviceString = deviceList.elementAt(position).toString()
val selectedDevice = bluetoothManager.adapter.getRemoteDevice(hashMap[selectedDeviceString])
val sharedPreferences = holder.context.getSharedPreferences("mSharedPrefs", Context.MODE_PRIVATE) ?: return
with(sharedPreferences.edit()) {
putString("selectedDeviceString", selectedDevice.toString())
apply()
}
holder.listLayout.setOnClickListener {
val intent = Intent(holder.context, DeviceActivity::class.java)
intent.putExtra("btDevice", selectedDevice)
intent.putExtra("btDeviceName", selectedDeviceString)
sharedPreferences.edit().putString("loadedFrom", "loadedFromCustomAdapter").apply()
sharedPreferences.edit().putString("selectedDeviceName", selectedDeviceString).apply()
sharedPreferences.edit().putString("selectedDeviceString", selectedDevice.toString()).apply()
holder.context.startActivity(intent)
}
}
override fun getItemCount() = treeSet.size
}
Setting a new adapter makes the RecyclerView reinitialise itself, and it'll create all the ViewHolders again, etc. You'd want to avoid that really. This is generally how you'd make it update:
class CustomAdapter(
private var data: List<Thing>
...
) {
fun setData(data: List<Thing>) {
// store the data and do any other setup work
this.data = data
// make sure the RecyclerView updates to show the new data
notifyDataSetChanged()
}
}
Then you just need to keep a reference to the adapter when you first create it in onCreate or whatever, and call theAdapter.setData(newData) whenever you get the new stuff. You're just setting up by creating an adapter to handle displaying your data in the list, and then you hand it data whenever it needs to update.
The actual "how things update" logic is in setData - it's the adapter's business how the adapter works internally, y'know? Right now it's the most basic notifyDataSetChanged() call, i.e. "refresh everything", but you could change that later - the outside world doesn't need to care about that though.
I noticed in onBindViewHolder you're doing this:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val deviceList = hashMap.keys.toList()
val macAddressList = hashMap.values.toList()
That function runs every time a ViewHolder needs to display some new information (just before it scrolls onto the screen, basically) so you're creating a lot of lists whenever you scroll. Really you should do that once, when the data is set - derive your lists from the source and keep them around:
class CustomAdapter(
initialData: List<Thing> // not a var now - see init block
...
) {
// these might need to be lateinit since you're init-ing through a function
private var data: List<Thing>
private var deviceList: List<String>
private var macAddressList: List<String>
init {
setData(initialData)
}
fun setData(data: List<Thing>) {
// Now you're storing the data and deriving the other lists
// You might not even need to store the 'data' object if you're not using it?
this.data = data
deviceList = data.keys.toList()
macAddressList = data.values.toList()
// make sure the RecyclerView updates to show the new data
notifyDataSetChanged()
}
}
So now setData takes some source data, derives the lists you need to use and stores those, and calls the update function. Because that setup has to be done, you need to call this function every time the source data is set - including when you first create the adapter. That's why the data parameter in the constructor isn't a var, it's just used in the initialisation, passed to the setData call.
Or alternatively, don't pass any data in the constructor at all - just create the adapter, and immediately call setData on it. Initialise the variables to emptyList() etc, and then you don't need to handle the "setup at construction time" case at all!
Just another couple of tips - I don't know what treeSet is for, but you're using its size in getItemCount. You shouldn't do that, it should usually reflect the size of the data set you're actually displaying, which is the contents of hashSet - when you get a position in onBindViewHolder, you're looking up an element in hashSet, not treeSet, so that should be your source for the number of items
The other thing is, all that stuff in onBindViewHolder... you're doing a lot of setup work that should really only happen when the item is actually clicked. Usually you'd set up the click listener once, in onCreateViewHolder, and when binding you'd set a field on the viewholder telling it which position it's currently displaying. If the click listener fires, then you can look up the current position in the data, create Intents, etc
Even if you don't move that into the VH, at least move the setup code into the onClickListener so it doesn't run every time a new item scrolls into view. That sharedPreferences bit is especially a problem - that gets overwritten every time a new item is bound (and they can be bound when they're still off-screen) so it probably isn't set to what you expect
Setting the adapter multiple times should be avoided. Doing so causes its scroll position to be lost and reset to the top, and causes it to have to reinflate all of its views and ViewHolders. Instead, you should update the model your adapter points at and notifyDataSetChanged() on it (or better yet, use DiffUtil to update individual items).
I finished updating my code and it works great! The data no longer jumps to the top when new data is added. Thought I would post the code for anyone who is interested.
Here is my adapter:
class CustomAdapter(
private val bluetoothManager: BluetoothManager
) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
private var sortedMap = emptyMap<String, String>()
private var deviceList = emptyList<String>()
private var macAddressList = emptyList<String>()
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.textview_list_item)
val listLayout: FrameLayout = view.findViewById(R.id.item_layout)
val context: Context = view.context
val textView2: TextView = view.findViewById(R.id.textview_list_item_address)
}
#SuppressLint("NotifyDataSetChanged")
fun setData(sortedMap: SortedMap<String, String>) {
this.sortedMap = sortedMap
deviceList = sortedMap.keys.toList()
macAddressList = sortedMap.values.toList()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.text_device_row_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = deviceList.elementAt(position)
holder.textView2.text = macAddressList.elementAt(position)
holder.listLayout.setOnClickListener {
val selectedDeviceString = deviceList.elementAt(position).toString()
val selectedDevice = bluetoothManager.adapter.getRemoteDevice(sortedMap[selectedDeviceString])
val sharedPreferences = holder.context.getSharedPreferences("mSharedPrefs", Context.MODE_PRIVATE) ?: return#setOnClickListener
with(sharedPreferences.edit()) {
putString("selectedDeviceString", selectedDevice.toString())
apply()
}
val intent = Intent(holder.context, DeviceActivity::class.java)
intent.putExtra("btDevice", selectedDevice)
intent.putExtra("btDeviceName", selectedDeviceString)
sharedPreferences.edit().putString("loadedFrom", "loadedFromCustomAdapter").apply()
sharedPreferences.edit().putString("selectedDeviceName", selectedDeviceString).apply()
sharedPreferences.edit().putString("selectedDeviceString", selectedDevice.toString()).apply()
holder.context.startActivity(intent)
}
}
override fun getItemCount() = sortedMap.size
}
And the activity onCreate() code:
// Specify an adapter.
listAdapter = CustomAdapter(bluetoothManager)
binding.recyclerviewDevices.adapter = listAdapter
And my function that updates the data:
#SuppressLint("MissingPermission", "NotifyDataSetChanged")
fun displayDevices(
deviceMap: HashMap<String, String>
) {
val sortedDeviceMap = deviceMap.toSortedMap()
listAdapter.setData(sortedDeviceMap)
Log.d(TAG, "displayDevices: ${sortedDeviceMap.entries}")
// Notify the view to update when data is changed.
if ( binding.recyclerviewDevices.isAttachedToWindow) {
binding.progressBarCyclic.visibility = GONE
}
}
Related
In my case I am trying to get a list of String from my Adapter and use it in my Fragment but upon debugging using Logs I found that the list is getting updated inside the onBindViewHolder but not outside it. So when I try to access the list from my Fragment I am getting an empty list of String.
I have spent few hours trying to figure this but can't find a feasible solution.
My Approach: I am thinking of an approach to save this list in a room table and then query it back in the Fragment. Though it may solve the issue but is it the only way? Are there any other ways to achieve this result?
My Adapter
class FloorProfileDialogAdapter() : RecyclerView.Adapter<FloorProfileDialogAdapter.MyViewHolder>() {
var floors = emptyList<String>()
inner class MyViewHolder(val binding: ScheduleFloorDialogItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ScheduleFloorDialogItemBinding.inflate(inflater, parent, false)
return MyViewHolder(binding)
}
private val checkedFloors: MutableList<String> = mutableListOf()
//List of uniquely selected checkbox to be observed from New Schedule Floor Fragment
var unique: List<String> = mutableListOf()
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val currentFloor = floors[position]
Timber.d("Current floor: $currentFloor")
holder.binding.floorCheckBox.text = "Floor $currentFloor"
//Checks the checked boxes and updates the list
holder.binding.floorCheckBox.setOnCheckedChangeListener { buttonView, isChecked ->
if (buttonView.isChecked) {
Timber.d("${buttonView.text} checked")
checkedFloors.add(buttonView.text.toString())
} else if (!buttonView.isChecked) {
Timber.d("${buttonView.text} unchecked")
checkedFloors.remove(buttonView.text)
}
unique = checkedFloors.distinct().sorted()
Timber.d("List: $unique")
}
}
fun returnList(): List<String> {
Timber.d("$unique")
return unique
}
override fun getItemCount(): Int {
return floors.size
}
#SuppressLint("NotifyDataSetChanged")
fun getAllFloors(floorsReceived: List<String>) {
Timber.d("Floors received : $floorsReceived")
this.floors = floorsReceived
notifyDataSetChanged()
}
}
Fragment code where I am trying to read it
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Chosen Floors
val chosenFloors = floorProfileDialogAdapter.returnList()
Timber.d("Chosen floors : $chosenFloors")
}
Note: The list I am trying to receive is var unique: List<String> = mutableListOf. I tried to get it using the returnList() but the log in that function shows that list is empty. Similarly the Log in fragment shows that it received an empty list.
Edit 1 :
Class to fill the Adapter Floors using getAllFloors()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val floorList: MutableList<String> = mutableListOf()
var profileName: String? = ""
profileName = args.profileName
//Profile name received
Timber.d("Profile name : $profileName")
//Getting list of all floors
createProfileViewModel.totalFloors.observe(viewLifecycleOwner) {
Timber.d("List of floors received : $it")
val intList = it.map(String::toInt)
val maxFloorValue = intList.last()
var count = 0
try {
while (count <= maxFloorValue) {
floorList.add(count.toString())
count++
}
} catch (e: Exception) {
Timber.d("Exception: $e")
}
floorProfileDialogAdapter.getAllFloors(floorList)
Timber.d("Floor List : $floorList")
}
When you first set up your Fragment and create your Adapter, unique is empty:
var unique: List<String> = mutableListOf()
(if you have some checked state you want to save and restore, you'll have to initialise this with your checked data)
In onViewCreated, during Fragment setup, you get a reference to this (empty) list:
// Fragment onViewCreated
val chosenFloors = floorProfileDialogAdapter.returnList()
// Adapter
fun returnList(): List<String> {
return unique
}
So chosenFloors is a reference to this initial entry list. But when you actually update unique in onBindViewHolder
unique = checkedFloors.distinct().sorted()
you're replacing the current list with a new list object. You're not updating the existing list (even though you made it a MutableList). So you never actually add anything to that empty list you started with, and chosenFloors is left pointing at a list that contains nothing, while the Adapter has discarded it and unique holds a completely different object.
The solution there is to make unique a val (so you can't replace it) and just change its contents, e.g.
unique.clear()
unique += checkedFloors.distinct().sorted()
But I don't feel like that's your problem. Like I pointed out, that list is initially empty anyway, and you're grabbing it in your Fragment during initialisation just so you can print out its contents, as though you expect it to contain something at that point. Unless you initialise it with some values, it's gonna be empty.
If you're not already storing/restoring them, you'll need to handle that! I posted some code to do that on another answer so I'll just link that instead of repeating myself. That code is storing indices though, not text labels like you're doing. Indices are much cleaner and avoid errors - the text is more of a display thing, a property of the item the specific (and unique) index refers to. (But you can store a string array in SharedPreferences if you really want to.)
Also you're not actually updating your ViewHolder to display the checked state for the current item in onBindViewHolder. So whatever ViewHolder you happen to have been given (there's only a few of them for the list, they get reused) it's just showing whatever its checkbox was last set to, by you poking at it. Check an item, then scroll the list and see what happens!
So you need to check or uncheck the box so it's correct for the item you're displaying. This is pretty easy if you're storing the checked items by indices:
// explicitly set the checked state, depending on whether the item at 'position' is checked
holder.binding.floorCheckBox.checked = checkedItems[position]
You can work out something similar for your text label approach, but again I wouldn't recommend doing things that way.
I'm currently experimenting with a RecyclerViewer, but stumbled upon a problem: When I update the data and call notifyDataSetChanged, the RecyclerViewer updates it's view, but not with the new data, but rather with the old data.
I've searched through Stackoverflow for the problem, but in most cases the problem is that they either created two instances of the adapter (reference) or that they don't have a layout manager, and I believe that neither of those is my problem.
Here is my code for creating and updating the RecyclerViewer in the fragment in which it's hosted:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_player_list_list, container, false)
data = getPlayersAsList(requireContext(), gameUUID)
if(data.isEmpty()){
data = listOf(SharedPreferencesManager.Companion.Player("John", false))
}
// Set the adapter
if(view is LinearLayout){
view.children.forEach {
if (it is RecyclerView) {
with(it) {
layoutManager = when {
columnCount <= 1 -> LinearLayoutManager(context)
else -> GridLayoutManager(context, columnCount)
}
Log.i("DEBUG", "The first adapter was called")
adapter = MyItemRecyclerViewAdapter(data)
}
}
}
}
fun notifyDataUpdate(position: Int? = null) {
if(view is LinearLayout){
(view as LinearLayout).children.forEach {
if(it is RecyclerView){
data = getPlayersAsList(requireContext(), gameUUID)
Log.i("DATA UPDATE", "Player list is now $data")
it.adapter?.notifyDataSetChanged()
}
}
}
}
And here is the code of the adapter:
import android.util.Log
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import com.chuaat.hideandseek.databinding.FragmentPlayerListBinding
import com.google.android.material.button.MaterialButton
import java.util.*
/**
* [RecyclerView.Adapter] that can display a [PlaceholderItem].
* TODO: Replace the implementation with code for your data type.
*/
class MyItemRecyclerViewAdapter(
private val values: List<SharedPreferencesManager.Companion.Player>
) : RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
FragmentPlayerListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = values[position]
Log.i("RECYCLER VIEWER", "SETTING VALUE OF $item")
if(item.isSeeker){
holder.buttonView.setIconResource(R.drawable.ic_baseline_search_24)
}
else{
holder.buttonView.setIconResource(R.drawable.ic_outline_visibility_off_24)
}
holder.contentView.text = item.name
}
override fun getItemCount(): Int = values.size
inner class ViewHolder(binding: FragmentPlayerListBinding) :
RecyclerView.ViewHolder(binding.root) {
val buttonView: MaterialButton = binding.toggleSeekerButton as MaterialButton
val contentView: TextView = binding.content
override fun toString(): String {
return super.toString() + " '" + contentView.text + "'"
}
}
}
The log output I get when calling notifiyDataUpdate is:
I/DATA UPDATE: Player list is now [Player(name=Joe, isSeeker=false)]
I/RECYCLER VIEWER: SETTING VALUE OF Player(name=John, isSeeker=false)
As you can see the updated data is with a Player named Joe, but in onBindViewHolder the only value is the default Player ("John").
What is the problem I'm missing?
You're not actually updating the data in your adapter
Here's how you initialise it:
data = getPlayersAsList(requireContext(), gameUUID)
if(data.isEmpty()){
data = listOf(SharedPreferencesManager.Companion.Player("John", false))
}
...
adapter = MyItemRecyclerViewAdapter(data)
and that parameter in your adapter's constructor is your data source for the RecyclerView
class MyItemRecyclerViewAdapter(
private val values: List<SharedPreferencesManager.Companion.Player>
At this point, your Fragment has a list called data which contains your current data, and the Adapter has a reference to that same list. They're both looking at the same object - let's call it list 1.
Then you update your data in the Fragment, and notify the Adapter:
data = getPlayersAsList(requireContext(), gameUUID)
it.adapter?.notifyDataSetChanged()
But what you've done is create a new list, list 2, with that getPlayersAsList call. You assign that to data. So data points to list 2, the new data - but values in your adapter still points to list 1, the old list. So for the adapter, nothing's changed! It can't see the new data, so even though you notify it, it will still look the same.
You have two options here. Firstly, since you're already using this shared list that the Fragment and Adapter are both looking at, you can just update that list - which is what you should be doing if they're both sharing it, right?
// clear the old data
data.clear()
// replace it with the new items
// this is the same as data.addAll(getPlayersAsList(...))
data += getPlayersAsList(requireContext(), gameUUID)
// now the list the adapter is using has been updated, you can notify it
it.adapter?.notifyDataSetChanged()
This way you're updating the actual list the adapter uses as its dataset, so you'll see the changes when it refreshes.
The second way, and the one I'd recommend, is to completely separate the Adapter's data from anything the Fragment is holding onto. Having a shared list like this can be a source of bugs, where one component changes it in the background, affecting the state of another component. If you ever use a DiffUtil in a RecyclerView for example, mutating the current list will stop it from working, because it won't be able to compare for changes.
You could make the values property a public var and update that externally, then notify the adapter - but honestly, it's better to let the Adapter handle those details internally. A setter function is a lot cleaner to me:
// in the Adapter
private var values = emptyList<SharedPreferencesManager.Companion.Player>()
fun setData(items: List<SharedPreferencesManager.Companion.Player>) {
// as a safety measure, creating a new list like this *ensures* that if the one
// that was passed in is mutated, this internal one won't change. (The items
// in the list can still be mutated of course!)
values = items.toList()
// now the -adapter- can decide on how/if it should update, based on its own
// internal state and the new data. The Fragment shouldn't be concerned with those details
notifyDataSetChanged()
}
Then when you want to update the adapter, just pass it the new data list:
// assuming you still want to keep a local reference to this data (if you don't need it, don't!)
data = getPlayersAsList(requireContext(), gameUUID)
// I'd really recommend just keeping a reference to your adapter when you create it,
// so you don't need to go searching for it and casting it like this
(it.adapter as? MyItemRecyclerViewAdapter)?.setData(data)
To initialise the adapter, you can either use this method:
// seriously, just store this in a `lateinit var adapter: MyItemRecyclerViewAdapter`
adapter = MyItemRecyclerViewAdapter()
adapter.setData(data)
recyclerView.adapter = adapter
Or you could keep the constructor parameter (which we're not using as a property to store the data, remember!) and use it to call the setData function:
class MyItemRecyclerViewAdapter(
data: List<SharedPreferencesManager.Companion.Player> // no val
) : RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
init {
setData(data)
}
And just as a hint - the way you're accessing your RecyclerView is complicated and not how you generally do things in Android. Give it an id in your layout XML file (R.layout.fragment_player_list_list) and then just do
val recyclerView = view.findViewById<RecyclerView>(R.id.whatever)
That's it! No need to loop through the hierarchy searching for it. If you store your Adapter in a variable, you probably won't need to touch the RV itself after setting it up - just call setData on your adapter reference
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
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.
Um.. I think you should try to manually notify your RecyclerView to redraw with notifyDataSetChanged()
If it doesn't work ListAdapter you are using as adapter does diff computation and dispatches the result to the RecyclerView. Maybe diff is not correct and the adapter does not see a difference between the old list and the new one - in this case it won't update UI. You may try to check diff behaviour and maybe comparing behavior of your data to change it.
it is a known issue that ListAdapter (actually the AsyncListDiffer from its implementation) does not update the list if the new list only has modified items but has the same instance. The updates do not work on new instance list either if you use the same objects inside.
For all of this to work, you have to create a hard copy of the entire list and objects inside.
Easiest way to achieve this:
items.toMutableList().map { it.copy() }
But I am facing a rather weird issue. I have a parse function in my ViewModel that finally posts the items.toMutableList().map { it.copy() } to the LiveData and gets observes in the fragment. Even with the hard copy, DiffUtil does not work. If I move the hard copy inside the fragment, then it works.
To get this easier, if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items)
... then, it doesn't work. But if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items.toMutableList().map { it.copy() })
... then it works.
Can anybody explain why this doesn't work?
In the mean time, I have opened an issue on the Google Issue Tracker because maybe they will fix the AsyncListDiffer not updating same instance lists or items. It defeats the purpose of the new adapter. The AsyncListDiffer SHOULD ALWAYS accept same instance lists or items, and fully update using the diff logic that the user customises in the adapter.
I made a quick sample using DiffUtil.Callback and ListAdapter<T, K> (so I called submitList(...) on the adapter), and had no issues.
Then I modified the adapter to be a normal RecyclerView.Adapter and constructed an AsyncDiffUtil inside of it (using the same DiffUtil.Callback from above).
The architecture is:
Activity -> Fragment (contains RecyclerView).
Adapter
ViewModel
"Fake Repository" that simply holds a val source: MutableList<Thing> = mutableListOf()
Model
I've created a Thing object: data class Thing(val name: String = "", val age: Int = 0).
For readability I added typealias Things = List<Thing> (less typing). ;)
Repository
It's fake in the sense that items are created like:
private fun makeThings(total: Int = 20): List<Thing> {
val things: MutableList<Thing> = mutableListOf()
for (i in 1..total) {
things.add(Thing("Name: $i", age = i + 18))
}
return things
}
But the "source" is a mutableList of (the typealias).
The other thing the repo can do is "simulate" a modification on a random item. I simply create a new data class instance, since it's obviously all immutable data types (as they should be). Remember this is just simulating a real change that may have come from an API or DB.
fun modifyItemAt(pos: Int = 0) {
if (source.isEmpty() || source.size <= pos) return
val thing = source[pos]
val newAge = thing.age + 1
val newThing = Thing("Name: $newAge", newAge)
source.removeAt(pos)
source.add(pos, newThing)
}
ViewModel
Nothing fancy here, it talks and holds the reference to the ThingsRepository, and exposes a LiveData:
private val _state = MutableLiveData<ThingsState>(ThingsState.Empty)
val state: LiveData<ThingsState> = _state
And the "state" is:
sealed class ThingsState {
object Empty : ThingsState()
object Loading : ThingsState()
data class Loaded(val things: Things) : ThingsState()
}
The viewModel has two public methods (Aside from the val state):
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
_state.postValue(ThingsState.Loaded(repository.fetchAllTheThings()))
}
}
fun modifyData(atPosition: Int) {
repository.modifyItemAt(atPosition)
fetchData()
}
Nothing special, just a way to modify a random item by position (remember this is just a quick hack to test it).
So FetchData, launches the async code in IO to "fetch" (in reality, if the list is there, the cached list is returned, only the 1st time the data is "made" in the repo).
Modify data is simpler, calls modify on the repo and fetch data to post the new value.
Adapter
Lots of boilerplate... but as discussed, it's just an Adapter:
class ThingAdapter(private val itemClickCallback: ThingClickCallback) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
The ThingClickCallback is just:
interface ThingClickCallback {
fun onThingClicked(atPosition: Int)
}
This Adapter now has an AsyncDiffer...
private val differ = AsyncListDiffer(this, DiffUtilCallback())
this in this context is the actual adapter (needed by the differ) and DiffUtilCallback is just a DiffUtil.Callback implementation:
internal class DiffUtilCallback : DiffUtil.ItemCallback<Thing>() {
override fun areItemsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.age == newItem.age && oldItem.name == oldItem.name
}
nothing special here.
The only special methods in the adapter (aside from onCreateViewHolder and onBindViewHolder) are these:
fun submitList(list: Things) {
differ.submitList(list)
}
override fun getItemCount(): Int = differ.currentList.size
private fun getItem(position: Int) = differ.currentList[position]
So we ask the differ to do these for us and expose the public method submitList to emulate a listAdapter#submitList(...), except we delegate to the differ.
Because you may be wondering, here's the ViewHolder:
internal class ViewHolder(itemView: View, private val callback: ThingClickCallback) :
RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.thingName)
private val age: TextView = itemView.findViewById(R.id.thingAge)
fun bind(data: Thing) {
title.text = data.name
age.text = data.age.toString()
itemView.setOnClickListener { callback.onThingClicked(adapterPosition) }
}
}
Don't be too harsh, I know i passed the click listener directly, I only had about 1 hour to do all this, but nothing special, the layout it's just two text views (age and name) and we set the whole row clickable to pass the position to the callback. Nothing special here either.
Last but not least, the Fragment.
Fragment
class ThingListFragment : Fragment() {
private lateinit var viewModel: ThingsViewModel
private var binding: ThingsListFragmentBinding? = null
private val adapter = ThingAdapter(object : ThingClickCallback {
override fun onThingClicked(atPosition: Int) {
viewModel.modifyData(atPosition)
}
})
...
It has 3 member variables. The ViewModel, the Binding (I used ViewBinding why not it's just 1 liner in gradle), and the Adapter (which takes the Click listener in the ctor for convenience).
In this impl., I simply call the viewmodel with "modify item at position (X)" where X = the position of the item clicked in the adapter. (I know this could be better abstracted but this is irrelevant here).
there's only two other implemented methods in this fragment...
onDestroy:
override fun onDestroy() {
super.onDestroy()
binding = null
}
(I wonder if Google will ever accept their mistake with Fragment's lifecycle that we still have to care for this).
Anyway, the other is unsurprisingly, onCreateView.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.things_list_fragment, container, false)
binding = ThingsListFragmentBinding.bind(root)
viewModel = ViewModelProvider(this).get(ThingsViewModel::class.java)
viewModel.state.observe(viewLifecycleOwner) { state ->
when (state) {
is ThingsState.Empty -> adapter.submitList(emptyList())
is ThingsState.Loaded -> adapter.submitList(state.things)
is ThingsState.Loading -> doNothing // Show Loading? :)
}
}
binding?.thingsRecyclerView?.adapter = adapter
viewModel.fetchData()
return root
}
Bind the thing (root/binding), get the viewModel, observe the "state", set the adapter in the recyclerView, and call the viewModel to start fetching data.
That's all.
How does it work then?
The app starts, the fragment is created, subscribes to the VM state LiveData, and triggers the Fetch of data.
The ViewModel calls the repo, which is empty (new), so makeItems is called the list now has items and cached in the repo's "source" list. The viewModel receives this list asynchronously (in a coroutine) and posts the LiveData state.
The fragment receives the state and posts (submit) to the Adapter to finally show something.
When you "click" on an Item, ViewHolder (which has a click listener) triggers the "call back" towards the fragment which receives a position, this is then passed onto the Viewmodel and here the data is mutated in the Repo, which again, pushes the same list, but with a different reference on the clicked item that was modified. This causes the ViewModel to push a new LIveData state with the same list reference as before, towards the fragment, which -again- receives this, and does adapter.submitList(...).
The Adapter asynchronously calculates this and the UI updates.
It works, I can put all this in GitHub if you want to have fun, but my point is, while the concerns about the AsyncDiffer are valid (and may be or been true), this doesn't seem to be my (super limited) experience.
Are you using this differently?
When I tap on any row, the change is propagated from the Repository
UPDATE: forgot to include the doNothing function:
val doNothing: Unit
get() = Unit
I've used this for a while, I normally use it because it reads better than XXX -> {} to me. :)
While doing
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
you are creating a new list but items remains the same. You have to store that new list into a variable or passing that operation directly as a param to postItem.
I want to update at any time some values in my RecyclerView.
Here is my data class ParameterText:
data class ParameterText(
var parameterName: String?,
var parameterValue: String?
)
Here is my ViewHolder class ParameterTextViewHolder:
class ParameterTextViewHolder(itemView: View) : ViewHolder(itemView) {
val parameterName: TextView = itemView.findViewById(R.id.parameterName)
val parameterText: TextView = itemView.findViewById(R.id.parameterValue)
}
Here is my Adapter (in my Activity):
// Adapter
private val parametersTextFoundList = emptyDataSourceTyped<ParameterText>()
And here is my RecyclerView setup (also in my Activity):
rv_parameters_text.setup {
withDataSource(parametersTextFoundList)
withItem<ParameterText, ParameterTextViewHolder>(R.layout.parameter_text) {
onBind(::ParameterTextViewHolder) { _, item ->
parameterName.text = item.parameterName
parameterText.text = item.parameterValue
}
}
}
I tried this:
private fun updateValue(index: Int, value: String) {
parametersTextFoundList[index].parameterValue = value
}
But it doesn't work. I read that I should also use the notifyDataSetChanged() method but I don't know where to use it. Can you help me?
There is an entire suite of notify API's, including notifyItemInserted(), notifyItemRemoved(), notifyItemChanged(), which are designed to more efficiently update a RecyclerView.
when changing the contents of one existing row in your RecyclerView, its more efficient to use adapter.notifyItemChanged(row), as notifyDataSetChanged() will reload the entire RecyclerView. I recommend:
private fun updateValue(index: Int, value: String)
{
parametersTextFoundList[index].parameterValue = value
rv_parameters_text.adapter?.notifyItemChanged(index)
}
You need to use notifyDataSetChanged() method with the update like this
rv_parameters_text.adapter?.notifyDataSetChanged()