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
}
}
So, i want to create a button on Android Studio that updates my list in a sorting order, Ascending, etc., but i've been running in to some problems with the code and i can wrap my head around it. When i click the button nothing happends, it doesn't sort my list at all
Using Room Database FrameWork from Andriod Studio.
This is what i using to do the sorting:
//'Produto' is the list, 'nome' is a element on that list that i want to sort
#Entity
#Parcelize
data class Produto(
#PrimaryKey(autoGenerate = true)
val id: Long,
val nome: String)
#Query("SELECT * FROM Produto")
fun buscaTodos() : List<Produto>
//This is the code that i use to do the sorting
#Query("SELECT * FROM Produto ORDER BY nome ASC")
fun getAllSortedByName(): List<Produto>
This is the code to i'm using to do the sorting after a press the button
class ListaProdutosAdapter(
private val context: Context,
produtos: List<Produto> = emptyList(),
var quandoClicaNoItem: (produto: Produto) -> Unit = {}
) : RecyclerView.Adapter<ListaProdutosAdapter.ViewHolder>() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
private val adapter = ListaProdutosAdapter(context = this)
val db = AppDatabase.instancia(this)
val produtoDao = db.produtoDao()
//menu_ordem_asc_id being the button id
when (item.itemId) {
R.id.menu_ordem_asc_id -> {
produto?.let { produtoDao.getAllSortedByName()}
adapter.atualiza(produtoDao.buscaTodos())
//This is in another class, but i put it here so it's easier to understand
fun atualiza(produtos: List<Produto>) {
this.produtos.clear()
this.produtos.addAll(produtos)
notifyDataSetChanged()
}
}
return super.onOptionsItemSelected(item)
}
Well, produtoDao.getAllSortedByName() doesn't sort the items in place, it returns a list of sorted items. So when you do produto?.let { produtoDao.getAllSortedByName()} you don't do anything with the result which is the sorted list.
On the next line you call adapter.atualiza(produtoDao.buscaTodos()) and as you mentioned in your comments produtoDao.buscaTodos() returns an unsorted list of products. And this is what you populate your adapter with.
In order to populate the adapter with the sorted list you should call adapter.atualiza(produtoDao.getAllSortedByName()) instead.
I am drawing a list of devices
#Composable
fun DeviceListScreen(){
val model: DeviceListViewModel = hiltViewModel()
val myDevices: List<MyDevice> by model.myDevices.observeAsState(emptyList())
for(device in myDevices)
Device(device)
}
In model I have a livedata
private val items: List<MyDevice> = ArrayList()
private val _myDevices = MutableLiveData<List<MyDevice>> (emptyList())
val myDevices: LiveData<List<MyDevice>> = _myDevices
I change content of an item then update live data
items[0].signal = 54
_myDevices.value = items
However data is not updating in ui.
I guess this is because the pointer to list was not changed and number of items in the list also is not changes and thus compose does not update this data.
Update 12-08-2022
I've just encountered the same problem you had. I have successfully solved it, so the following solution that I have adapted for your problem should also work fine:
fun updateSignal(idOfSelectedDevice: Int) {
_myDevices.value = myDevices.value?.map { device ->
if (device.id == idOfSelectedDevice) device.copy(
signal = <YOUR_DESIRED_VALUE>
) else device
}
}
You could try wrapping items in its own data class:
data class ItemsList(val devices: List<MyDevice> = ArrayList())
Then change items to private val items: ItemsList = ItemsList()
Since you are using your own data class for items, you can then access the copy function which copies an existing object into a new object and should therefore trigger the update of the liveData object:
_myDevices.value = _myDevices.value?.copy(items = items.devices.apply {
this[0].signal = 54
})
In my Fragment for my Android app, I'm using Firebase Realtime Database and Moshi to save and load the data I get from my RecyclerView.
These are the functions I use for this task:
private fun saveData() {
val moshi = Moshi.Builder().add(BigDecimalAdapter).add(KotlinJsonAdapterFactory()).build()
val listMyData = Types.newParameterizedType(List::class.java, ItemCard::class.java)
val jsonAdapter: JsonAdapter<ArrayList<ItemCard>> = moshi.adapter(listMyData)
val json = jsonAdapter.toJson(dataList)
userInfo.child("jsonData").setValue(json)
}
private fun loadData(json: String) = lifecycleScope.launch(Dispatchers.IO) {
if (json != "") {
val type: Type = object : TypeToken<List<ItemCard>>() {}.type
val moshi = Moshi.Builder().add(BigDecimalAdapter).add(KotlinJsonAdapterFactory()).build()
val jsonAdapter: JsonAdapter<ArrayList<ItemCard>> = moshi.adapter(type)
dataList = jsonAdapter.fromJson(json)!!
if (dataList == null) {
dataList = arrayListOf<ItemCard>()
}
}
}
private fun buildRecyclerView() {
recyclerView = rootView.findViewById(R.id.main_recycler_view)
recyclerView.setHasFixedSize(true)
recyclerViewLayoutManager = LinearLayoutManager(this#Main.requireContext())
adapter = MainAdapter(dataList, this)
recyclerView.layoutManager = recyclerViewLayoutManager
recyclerView.adapter = adapter
}
In my onViewCreated, I having this for loading the data and building the RecyclerView:
userInfo.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for (postSnapshot in dataSnapshot.children) {
when (postSnapshot.key) {
"jsonData" -> {
loadData(postSnapshot.value.toString())
buildRecyclerView()
}
}
}
}
override fun onCancelled(error: DatabaseError) {}
})
Everything works as I want, however, there's a delay/lag when I go to this specific fragment. There are a total of three fragments in my app. The other two work smoothly with no delay/lag, but when I click on the button or slide the screen to go to this fragment, there's a delay in the change of the UI.
What can I do to make the performance better? Where should I put my addValueEventListener? I only want it to get triggered when the fragment is first created and when the jsonData child gets changed. I believe in my onViewCreated the listener is being triggered multiple times. Is there anything else I can add to my code or modify to make the performance better when saving and loading the RecyclerView data?
when you create a listerner in onViewCreated run then make sure that you remove listener when fragment is not attach.Realtime event listerner return a string that you can easily = to your pojo class like:
ItemCard message = messageSnapshot.getValue(ItemCard.class);
this way is to saving manual converting the list effort.Last important thing that if recyclerview is initialize then don't initialize when data change only notifyDataSetChange.when you adding data in the list then make sure that the
list.clear();
otherwise you data is duplicate because on addValueEventListener return the whole data.
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()