Kotlin : How to update a value of a RecyclerView - android

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

Related

Kotlin RecyclerView Adapter multiple callback functions

How do I have multiple callback function back to the Activity/Fragment of a RecyclerView?
I have multiple options for each item in the RecyclerView (Edit, Delete, CheckedAsComplete, View) and I would like to have callback function for each of them in the Activity/Fragment of the RecyclerView.
Here is a link of how I got one callback in the Adapter: https://www.geeksforgeeks.org/kotlin-lambda-functions-for-recyclerview-adapter-callbacks-in-android/
I just need to know if it is possible to have multiple callbacks in the adapter and if so, how do I implement it?
My Activity's Adapter Code:
val adapter = ProductAdapter(this) {
deleteProduct(it),
editProduct(it),
viewProduct(it),
checkAsComplete(it)
}
Here is my Adapter's Constructor:
class ProductAdapter(
private var context: Context,
private val deleteProduct: (ItemTable) -> Unit,
private val editProduct: (ItemTable) -> Unit,
private val viewProduct: (ItemTable) -> Unit,
private val checkedAsComplete: (ItemTable) -> Unit
): RecyclerView.Adapter<ProductAdapter.ItemProductViewHolder>() {
// Rest of RecyclerView Adapter Code
}
I'm pretty new to kotlin so I would really appreciate your help!
You can use curly braces out only for the last callback of the list.
Assuming you declared the following methods in your activity :
fun deleteProduct(itemTable: ItemTable)
fun editProduct(itemTable: ItemTable)
fun checkAsComplete(itemTable: ItemTable)
fun viewProduct(itemTable: ItemTable)
You can use named parameters and you have two choices
With method reference
val adapter = ProductAdapter(
context = this,
deleteProduct = ::deleteProduct,
editProduct = ::editProduct,
viewProduct = ::viewProduct,
checkAsComplete = ::checkAsComplete
)
With lambda
val adapter = ProductAdapter(
context = this,
deleteProduct = { deleteProduct(it) },
editProduct = { editProduct(it) },
viewProduct = { viewProduct(it) },
checkAsComplete = { checkAsComplete(it) }
)
You can use different approach. This does not depend on how many events you have. For example with enum class you can use single callback with many options
class ProductAdapter(private val clickEvent: (ClickEvent, ItemTable) -> Unit):
RecyclerView.Adapter<ProductAdapter.ItemProductViewHolder>() {
enum class ClickEvent {
DELETE,
EDIT,
VIEW,
COMPLETE
}
}
Usage:
val adapter = ProductAdapter{ event, item ->
when(event){
DELETE -> deleteProduct(item)
....//All other enum values
}
}

What happens if my RecyclerView adapter is reinitialized several times?

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

live data observer triggered when another variable

sorry for my English
I just started learning android and I'm trying to make a library app that uses Room db, recView and fragments
in the app there are 3 lists, all books, already read and Wishlist - the already read and Wishlist are just books that its property isAlreadyRead is true
in the viewModel i created 3 lists members, one for each list
--Book class--
#Entity(tableName = "books_table")
data class Book(
#PrimaryKey(autoGenerate = true)
val id: Int = 1,
val name: String,
val author: String,
val desc: String,
val imgUrl: String,
#ColumnInfo(defaultValue = "0")
val isAlreadyRead: Boolean = false,
#ColumnInfo(defaultValue = "0")
val isWishlist: Boolean = false
)
--repository--
val allBooks = bookDao.getAllBooks().asLiveData()
val alreadyReadBooks = bookDao.getAlreadyReadBooks().asLiveData()
val wishlistBooks = bookDao.getWishlistBooks().asLiveData()
--viewModel--
val allBooks = repository.allBooks
val alreadyReadBooks = repository.alreadyReadBooks
val wishlistBooks = repository.wishlistBooks
(the repository and viewModel is just the important section of the code, if you need the whole code i will update the question)
the problem mainly occurs when i delete an object, i added the ability to delete using swipe with ItemTouchHelper.SimpleCallback
private val itemTouchHelperCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (viewHolder !is BookRecViewAdapter.BookViewHolder) {
return
}
bookViewModel.removeFromWishlistBooks(viewHolder.bookId)
adapter.notifyDataSetChanged()
}
}
when i delete in the already books, the bookViewModel.alreadyReadBooks observer is being called, but i noticed that also the bookViewModel.allBooks observer is being called, here is the code of the observers
--observer code inside onViewCreated of the fragments--
bookViewModel.allBooks.observe(requireActivity(), Observer { allBooks ->
Log.d("observer:", "show all books observer")
// Update the cached copy of the allBooks in the adapter.
allBooks?.let { adapter.books = it }
if (allBooks.isNotEmpty()) {
binding.listEmptyTextView.visibility = View.INVISIBLE
} else {
binding.listEmptyTextView.visibility = View.VISIBLE
}
})
(the code for the already read fragment and the wishist fragment is the same just that i called bookViewModel.alreadyReadBooks)
it crashes the app because in the observer i also want to update the visibility of a textView which just say that the list if empty (so there will be no white screen with no information), but binding is null because the fragment is in the background (i think that is the reason)
if you need any more info or code say, first time writing in stack overflow
thanks in advance
BTW: if you have any advice for me i will be happy to hear
My guess is that you are using requireActivity() instead of viewLifecycleOwner when you observe the bookViewModel. If I didn't guess right, you should show us the code that you have in your Fragment class.

PositionalDataSource returns nothing from Parse Server databse

I'm trying to load Products items from a Parse Server database using data source type of PositionalDataSource()to be able to fetch pages of items while scrolling down.
This what i've done so far :
My PositionalDataSource() class
class ParsePositionalDataSource: PositionalDataSource<Product>() {
private fun getQuery() : ParseQuery<Product> {
return ParseQuery.getQuery(Product::class.java).orderByDescending("createdAt")
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Product>) {
val query = getQuery()
query.limit = params.loadSize
query.skip = params.startPosition
val products = query.find()
callback.onResult(products)
}
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Product>) {
val query = getQuery()
query.limit = params.requestedLoadSize
query.skip = params.requestedStartPosition
val count = query.count()
val products = query.find()
callback.onResult(products, params.requestedStartPosition, count)
}
}
And then a data source factory :
class ParseDataSourceFactory : DataSource.Factory<Int, Product>() {
override fun create(): DataSource<Int, Product> {
return ParsePositionalDataSource()
}
}
And finally my ViewModal class
class CategoryTabViewModel() : ViewModel() {
private var pagedListConfig: PagedList.Config = PagedList.Config.Builder().setEnablePlaceholders(true)
.setPrefetchDistance(5)
.setInitialLoadSizeHint(5)
.setPageSize(5).build()
private val sourceFactory = ParseDataSourceFactory()
private val _productList = LivePagedListBuilder(sourceFactory, pagedListConfig).build()
val x = _productList
//val productList: LiveData<PagedList<Product>> = _productList
fun getProductList(): LiveData<PagedList<Product>> {
return _productList
}
}
I've checked if loadinitial() returns data by setting a breakpoint after : val products = query.find()and yes, productscontains a list of Product
But in my ViewModal, _productList it's always null or empty list.
Do I miss something ?
I've found the solution by my self while digging in debugger console output.
I saw a line that says :
E/RecyclerView: No layout manager attached; skipping layout
And it works just fine after i added the layoutmanager in my RecyclerView
<androidx.recyclerview.widget.RecyclerView
...
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
... />
And the documentation says :
A LayoutManager is responsible for measuring and positioning item
views within a RecyclerView as well as determining the policy for when
to recycle item views that are no longer visible to the user. By
changing the LayoutManager a RecyclerView can be used to implement a
standard vertically scrolling list, a uniform grid, staggered grids,
horizontally scrolling collections and more. Several stock layout
managers are provided for general use.
And
When writing a RecyclerView.LayoutManager you almost always want to
use layout positions whereas when writing an RecyclerView.Adapter, you
probably want to use adapter positions.
It seems that RecyclerView.Adapter needs RecyclerView.LayoutManager to be set.

AirBnb Epoxy - Views are duplicated instead of replaced

I am rendering a form based on JSON response that I fetch from the server.
My use case involves listening to a click from a radio button, toggling the visibility of certain text fields based on the radioButton selection, and refreshing the layout with the visible textView.
The expected output should be to update the same view with the textView now visible, but I'm now seeing the same form twice, first with default state, and second with updated state.
Have I somehow created an entirely new model_ class and passing it to the controller? I just want to change the boolean field of the existing model and update the view.
My Model Class
#EpoxyModelClass(layout = R.layout.layout_panel_input)
abstract class PanelInputModel(
#EpoxyAttribute var panelInput: PanelInput,
#EpoxyAttribute var isVisible: Boolean,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var context: Context,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var textChangedListener: InputTextChangedListener,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var radioButtonSelectedListener: RadioButtonSelectedListener,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var validationChangedListener: ValidationChangedListener
) : EpoxyModelWithHolder<PanelInputModel.PanelInputHolder>() {
#EpoxyAttribute var imageList = mutableListOf<ImageInput>()
override fun bind(holder: PanelInputHolder) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
generateViews(holder, inflater, panelInput.elements) // Generates textViews, radioButtons, etc, based on ElementType enum inside Panel input
}
fun generateRadioButtonView(element: Element) {
// Created a custom listener and calling its function
radioButtonSelectedListener.radioButtonSelected(chip.id, chip.text.toString())
}
fun generateTextView() {
// Show/hide textView based on isVisible value
}
My Controller Class
class FormInputController(
var context: Context,
var position: Int, // Fragment Position in PagerAdapter
var textChangedListener: InputTextChangedListener,
var radioButtonSelectedListener: RadioButtonSelectedListener,
var validationChangedListener: ValidationChangedListener
) : TypedEpoxyController<FormInput>() {
override fun buildModels(data: FormInput?) {
val panelInputModel = PanelInputModel_(
data as PanelInput,
data.isVisible,
context,
textChangedListener,
radioButtonSelectedListener,
validationChangedListener
)
panelInputModel.id(position)
panelInputModel.addTo(this)
}
}
My fragment implements the on radio button checked listener, modifies the formInput.isVisible = true and calls formInputController.setData(componentList)
Please help me out on this, thanks!
I don't think you are using Epoxy correctly, that's not how it's supposed to be.
First of all, let's start with the Holder: you should not inflate the view inside of bind/unbind, just set your views there. Also, the view is inflated for you from the layout file you are specifying at R.layout.layout_panel_input, so there is no need to inflate at all.
You should copy this into your project:
https://github.com/airbnb/epoxy/blob/master/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/helpers/KotlinEpoxyHolder.kt
And create your holder in this way:
class PanelInputHolder : KotlinHolder() {
val textView by bind<TextView>(R.id.your_text_view_id)
val button by bind<Button>(R.id.your_button_id)
}
Let's move to your model class: you should remove these variables from the constructor as they are going to be a reference for the annotation processor to create the actual class.
Also, don't set your layout res from the annotation as that will not be allowed in the future.
Like so:
#EpoxyModelClass
class PanelInputModel : EpoxyModelWithHolder<PanelInputHolder>() {
#EpoxyAttribute
lateinit var text: String
#EpoxyAttribute(DoNotHash)
lateinit var listener: View.OnClickListener
override fun getDefaultLayout(): Int {
return R.layout.layout_panel_input
}
override fun bind(holder: PanelInputHolder) {
// here set your views
holder.textView.text = text
holder.textView.setOnClickListener(listener)
}
override fun unbind(holder: PanelInputHolder) {
// here unset your views
holder.textView.text = null
holder.textView.setOnClickListener(null)
}
}
Loop your data inside the controller not inside the model:
class FormInputController : TypedEpoxyController<FormInput>() {
override fun buildModels(data: FormInput?) {
data?.let {
// do your layout as you want, with the models you specify
// for example a header
PanelInputModel_()
.id(it.id)
.text("Hello WOrld!")
.listener { // do something here }
.addTo(this)
// generate a model per item
it.images.forEach {
ImageModel_()
.id(it.imageId)
.image(it)
.addTo(this)
}
}
}
}
When choosing your id, keep in mind that Epoxy will keep track of those and update if the attrs change, so don't use a position, but a unique id that will not get duplicated.

Categories

Resources