RecyclerView views updated even when there is no difference based on DiffUtil? - android

I'm using ListAdapter with a DiffUtil.ItemCallback and I have made the DiffUtil callback methods return true for all items to inspect its behavior. And it seems that the RecyclerView views are still getting updated and showing the new result ('D', 'E', 'F'). I did not expect this, Shouldn't the RecyclerView only update views/items that have changed?
Adapter:
class ItemsAdapter : ListAdapter<Item, ItemViewHolder>(ItemDiff) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_item, parent, false))
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
object ItemDiff : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return true
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return true
}
}
class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvName = itemView.findViewById<TextView>(R.id.tvName)
fun bind(item: Item) {
tvName.text = item.name
}
}
data class Item(val name: String)
MainActivity:
class MainActivity : AppCompatActivity() {
private val adapter = ItemsAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rvItems.layoutManager = LinearLayoutManager(this)
rvItems.adapter = adapter
adapter.submitList(listOf(Item("A"), Item("B"), Item("C")))
adapter.submitList(listOf(Item("D"), Item("E"), Item("F")))
}
}

I figured out the problem and it seems like it was a concurrency issue, Since I was calling the submitList method twice and ListAdapter calculates the diff on the background thread.
To test this I changed my activity code to the following and the list was not updated.
class MainActivity : AppCompatActivity() {
private val adapter = ItemsAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rvItems.layoutManager = LinearLayoutManager(this)
rvItems.adapter = adapter
adapter.submitList(listOf(Item("A"), Item("B"), Item("C")))
thread {
Thread.sleep(1000)
runOnUiThread {
adapter.submitList(listOf(Item("D"), Item("E"), Item("F")))
Toast.makeText(this, "second submitList", Toast.LENGTH_LONG).show()
}
}
}
}

Related

ListAdapter submitlist not update after deleting the data instead its giving me duplicate data

when i try to delete first row i get first row back and with duplicate second row
enter image description here
after delete any row from the list gives me duplicate data like this
enter image description here
This is my Adapter class. On my deleteItem function position is passed from fragment class and
it is supposed to delete an item from the given position and update the list but its rendering the duplicate data.
class MyListAdapter :ListAdapter<Article,MyListAdapter.MyViewHolder>(MyDiffUtil()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.recycler_item,parent,false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
holder.onBind(item)
// to click each item of list
holder.itemView.setOnClickListener {
onItemClickListener?.let {it(item) }
}
}
class MyViewHolder(itemView :View) : RecyclerView.ViewHolder(itemView)
{
fun onBind(article: Article) = with(itemView)
{
Glide.with(this)
.load(article.urlToImage)
.error(R.drawable.notfound)
.into(imageView)
webSource.text = article.source?.name
newsDate.text = article.publishedAt
newsTitle.text = article.title
newsDescription.text = article.description
}
}
// lamda function for handling web view
private var onItemClickListener : ((Article) -> Unit)? = null
fun setOnItemClickListener(listener : (Article) ->Unit)
{
onItemClickListener = listener
}
// implementing diffutil class
class MyDiffUtil : DiffUtil.ItemCallback<Article>()
{
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
fun deleteItem(pos : Int)
{
var myl = ArrayList<Article>()
val currentList= currentList.toMutableList()
currentList.removeAt(pos)
myl.addAll(currentList)
submitList(myl)
}
}
`
I tried to delete the saved news from the ListAdapter and update the recycler view with animation of DiffUtill class but its not updating the recycler view and
giving duplicate data . How can i delete data with diffutil animation as its normal way. Thanks in advance.
this is my fragment
package com.example.news.Fragments
class SavedNews : Fragment() {
lateinit var mymainViewModel: MainViewModel
lateinit var databaseObject: MyDataBase
lateinit var myAdapter: MyListAdapter
lateinit var convertedArticleList : ArrayList<Article>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
myAdapter = MyListAdapter()
convertedArticleList = ArrayList()
return inflater.inflate(R.layout.fragment_saved_news, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
savedNewsRecyclerView.layoutManager = LinearLayoutManager(activity,LinearLayoutManager.VERTICAL,
false)
savedNewsRecyclerView.adapter = myAdapter
ItemTouchHelper(object :ItemTouchHelper.SimpleCallback(0,ItemTouchHelper.RIGHT){
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
// this method is called
// when the item is moved.
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// this method is called when we swipe our item to right direction.
// on below line we are getting the item at a particular position.
val deletedCourse: Article =
convertedArticleList.get(viewHolder.adapterPosition)
myAdapter.deleteItem(viewHolder.adapterPosition)
mymainViewModel.deleteSavedNews(deletedCourse.id!!)
// myAdapter.notifyItemRemoved(viewHolder.adapterPosition)
// myAdapter.submitList(myCurrentList)
// Snackbar.make(savedNewsRecyclerView,"Deleted" + deletedCourse.title,
// Snackbar.LENGTH_LONG).setAction(
// "Undo",
// View.OnClickListener {
//
// convertedArticleList.add(position,deletedCourse)
// myAdapter.notifyItemInserted(position)
// }
// ).show()
}
}).attachToRecyclerView(savedNewsRecyclerView)
}
override fun onResume() {
super.onResume()
Log.d("LIFE","ON Resume")
val myInterfaceObject = ApiInterface.MyObject.getInstance()
databaseObject = MyDataBase.MyObjectDB.getDBInstance(activity as MainActivity)
val myRepository = Repository(myInterfaceObject, databaseObject)
mymainViewModel = ViewModelProvider(
this,
MainViewModelFactory(myRepository)
).get(MainViewModel::class.java)
//listAdapter things
// show saved news in savednews fragment
lifecycleScope.launch(Dispatchers.Main) {
//abstract data from saved room database and converting likedartcile datacass object to
// Artticle data class article
mymainViewModel.abstractSavedNews().observe(viewLifecycleOwner, Observer {
it.forEach { eachLikedArticle ->
val obj = toArticle(eachLikedArticle)
convertedArticleList.add(obj)
}
myAdapter.submitList(convertedArticleList)
})
//clicking the item of save news
myAdapter.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("article", it)
}
convertedArticleList.clear()
findNavController().navigate(R.id.action_savedNews_to_article, bundle)
}
}
}
private fun toArticle(rawObject: LikedArticle) = Article(
rawObject.author, rawObject.content,
rawObject.description, rawObject.publishedAt, rawObject.source, rawObject.title,
rawObject.url, rawObject.urlToImage, rawObject.id
)
}
There is not enough information to actually replicate how you're trying to use this in your Fragment/Activity but without changing your code too much, this works (note I used view binding):
class MyListAdapter :
ListAdapter<Article, MyViewHolder>(MyDiffUtil()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = MyViewHolder(
RecyclerItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
holder.onBind(item)
}
fun deleteItem(pos: Int) {
var myl = ArrayList<Article>()
val currentList = currentList.toMutableList()
currentList.removeAt(pos)
myl.addAll(currentList)
submitList(myl)
}
}
class MyViewHolder(private val binding: RecyclerItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun onBind(article: Article) = with(binding) {
textView.text = article.url
}
}
class MyDiffUtil : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
Overall this should work.
You can also refactor deleteItem to something more meaningful:
fun deleteItem(pos: Int) {
val oldList = currentList.toMutableList()
oldList.removeAt(pos)
val updatedList = oldList
submitList(updatedList)
}

how to add onClickListener in recyclerview?

I don't know how to add onclicklistener to here,
I've tried many ways but it doesn't work,
i made this with recyclerview,
I've tried to add a new variable in the adapter section but error,
I've also made activity details so I just need to hit recyclerview and go to details page, I gave the code that has not been added onclicklistener
this MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var adapter: MakeUpAdapter
#SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rv_kamu.setHasFixedSize(true)
rv_kamu.layoutManager = LinearLayoutManager(this)
adapter = MakeUpAdapter()
adapter.notifyDataSetChanged()
rv_kamu.adapter = adapter
getDataFromApi()
}
private fun getDataFromApi() {
Retrofit.instance.getMakeUpList()
.enqueue(object : Callback<ArrayList<MakeUpModel>>{
override fun onResponse(
call: Call<ArrayList<MakeUpModel>>,
response: Response<ArrayList<MakeUpModel>>
) {
if(response.isSuccessful) {
showData(response.body()!!)
}
}
override fun onFailure(call: Call<ArrayList<MakeUpModel>>, t: Throwable) {
Log.e("Fail", "Be Fail")
}
})
}
private fun showData(data: ArrayList<MakeUpModel>) {
val result = data
adapter.setData(result)
}
}
and this adapter
class MakeUpAdapter : RecyclerView.Adapter<MakeUpAdapter.MakeUpViewHolder>(){
private val data = ArrayList<MakeUpModel>()
inner class MakeUpViewHolder (itemView: View) : RecyclerView.ViewHolder(itemView){
fun bind(data: MakeUpModel) {
//lib3
Glide.with(itemView.context)
.load(data.Picture)
.apply(
RequestOptions()
.override(100, 100)
)
.into(itemView.iv_item)
itemView.tv_Name.text = data.Name
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MakeUpViewHolder {
val view: View =
LayoutInflater.from(parent.context).inflate(R.layout.item_makeup, parent, false)
return MakeUpViewHolder(view)
}
override fun onBindViewHolder(holder: MakeUpViewHolder, position: Int) {
holder.bind(data[position])
}
override fun getItemCount(): Int = data.size
#SuppressLint("NotifyDataSetChanged")
//buat mainac
fun setData(list: ArrayList<MakeUpModel>) {
data.clear()
data.addAll(list)
notifyDataSetChanged()
}
}
This is my way.
first, create a interface to listen click event:
interface RecycleViewOnClickListener {
fun onItemClick(pos :Int)
}
and in your adapter class, put interface in param, then call listener in onBindViewHolder:
class MakeUpAdapter(private val clickListener: RecycleViewOnClickListener) : RecyclerView.Adapter<MakeUpAdapter.MakeUpViewHolder>(){
override fun onBindViewHolder(holder: MakeUpViewHolder, position: Int) {
holder.bind(data[position])
holder.itemView.setOnClickListener {
clickListener.onItemClick(position)
}
}
}
}
In your activity, implement listener like this:
class MainActivity : AppCompatActivity(), RecycleViewOnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
adapter = MakeUpAdapter(this)
}
override fun onItemClick(pos: Int) {
//do anything with your position here, such as get item from result
}
}
Hope it's helpful for you
on onBindViewHolder() you pass the position when calling the callback method:
override fun onBindViewHolder(holder: Holder?, position: Int) {
var item : MyObject = objects[position]
// Calling the clickListener sent by the constructor
holder.vieww.setOnClickListener {
// click event
}
}
There are a few ways one could add a click listener, one way writing this more idiomatically might be:
class MainActivity : AppCompatActivity() {
private val adapter by lazy { MakeUpAdapter() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rv_kamu.layoutManager = LinearLayoutManager(this)
adapter.notifyDataSetChanged()
adapter.listener = { /* TODO handle click event */ }
rv_kamu.adapter = adapter
getDataFromApi()
}
}
class MakeUpAdapter : RecyclerView.Adapter<MakeUpAdapter.MakeUpViewHolder>(){
var listener: (MakeUpModel) -> Unit = {}
private val data = ArrayList<MakeUpModel>()
inner class MakeUpViewHolder (itemView: View) : RecyclerView.ViewHolder(itemView){
fun bind(data: MakeUpModel) {
itemView.setOnClickListener { listener(data) }
//bind data
}
}
// plug in rest of your code
}
You might also want to consider moving the api call with retrofit into an Android ViewModel so that the network call will survive configuration changes (device rotation). This is a very simple example, but gets the point across and encourage you to check out the official android documentation on the subject.
class MakeUpViewModel( : ViewModel() {
init {
viewModelScope.launch {
val response = Retrofit.instance.getMakeUpList()
// update state with response
}
}
}

PagingDataAdapter class not executed after called adapter.submitData method

i want to try Paging 3 in my android apps. It successed until i get my PagingData<*> list from class PagingDataSource. but every time i called adapter.submitData(it), my adapter class not execute. Please help, where is my mistake? Thank you so much.
this is my code homeFragment.kt to call
private lateinit var webinarListAdapter: myAdapter
fun initAdapter() {
webinarAdapter = myAdapter()
with(recyclerView) {
layoutManager = LinearLayoutManager(requireContext())
adapter = webinarAdapter
adapter = webinarAdapter.withLoadStateHeaderAndFooter(
header = WebinarLoadStateAdapter { webinarAdapter.retry() },
footer = WebinarLoadStateAdapter { webinarAdapter.retry() }
)
}
lifecycleScope.launch {
viewModel.user.collectLatest {
webinarAdapter.submitData(it)
}
}
}```
MyAdapter.kt class
```class myAdapter: PagingDataAdapter<User, UserHolder>(DIFF_CALLBACK) {
override fun onBindViewHolder(holder: UserHolder, position: Int) {
Log.e(TAG, "onBindViewHolder")
holder.onBind(getItem(position)!!)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserHolder {
return UserHolder.create(parent)
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Taklim, newItem: Taklim) = oldItem == newItem
}
}
}```
Add this to you adapter
fun submitList(list: List<T>) {
yourlist.submitList(list)
notifyDataSetChanged()
}
after debug more and more i know where is my mistake. my mistake is on my paging data source executed catch exception. so my adapter not executed and my viewmodel not receive any data. Thanks!

RecyclerView only displaying data after restarting Activity

So i have this strange behaviour where my RecyclerView only Displays data when i start the App and then restart the activity via Android Studio. Or make a change in XML, then undo it and restart.
The then displayed data is correct and also updates when i add things, but it is not visible after starting the app.
Can anyone help me out? I am really clueless why that would be.
Fragment:
#AndroidEntryPoint
class HistoryFragment : Fragment(R.layout.fragment_history) {
private val viewModel: PurchaseViewmodel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentHistoryBinding.bind(view)
val exampleAdapter = ExampleAdapter()
binding.apply{
recyclerView.apply{
layoutManager = LinearLayoutManager(requireContext())
adapter = exampleAdapter
setHasFixedSize(true)
}
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false /// We dont need any interaction with Drag&Drop we only want swipe left&right
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val receipt = exampleAdapter.currentList[viewHolder.adapterPosition]
viewModel.onSwipe(receipt)
}
}).attachToRecyclerView(recyclerView)
}
setFragmentResultListener("add_receipt_request"){_,bundle ->
val result = bundle.getInt("add_receipt_request")
viewModel.onAddResult(result)
}
viewModel.receipts.observe(viewLifecycleOwner){ /// New Items get passed to the List
exampleAdapter.submitList(it)
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted { //as soon as we close our app the events will be suspended, but not deleted and will remain after restart
viewModel.addTaskEvent.collect { event->
when(event){
is PurchaseViewmodel.TasksEvent.ShowUndoDelete -> {
Snackbar.make(requireView(),"Tasks deleted", Snackbar.LENGTH_LONG)
.setAction("UNDO"){
viewModel.unDoDeleteClick(event.receipts)
}.show()
}
}
}
}
}
}
Adapter:
class ExampleAdapter : ListAdapter<Receipts,ExampleAdapter.ExampleViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExampleViewHolder {
val binding = ReceiptsBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return ExampleViewHolder(binding)
}
override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
override fun getItemCount(): Int {
return super.getItemCount()
}
class ExampleViewHolder(private val binding: ReceiptsBinding) : RecyclerView.ViewHolder(binding.root){ //Examples One Row in our list
fun bind (receipts: Receipts) {
binding.apply {
storeHistory.text = receipts.store
amountHistory.text = receipts.total
dateHistory.text = receipts.date
}
}
}
class DiffCallback : DiffUtil.ItemCallback<Receipts>() {
override fun areItemsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem == newItem
}
}
You should compare corresponding fields of old/new Receipts in areContentsTheSame() instead of comparing the entire old/new objects
override fun areContentsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem.store == newItem.store
&& oldItem.total == newItem.total
&& oldItem.date == newItem.date

Delete Item in RecyclerView and Dao with buttonclick

I have a RecyclerView displaying data from a Dao.
In the data that is shown in the recyclerView is a button with which the item should get deleted.
I know the function for that is :
fun deleteReceipt(receipts: Receipts) = viewModelScope.launch {
receiptDao.delete(receipts)
}
But I am not quite sure where to put it and call it. Because if I want to call it in the Adapter where i set the other displayed Item values I have no access to the Dao
Here is the fragment:
#AndroidEntryPoint
class HistoryFragment : Fragment(R.layout.fragment_history) {
private val viewModel: PurchaseViewmodel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentHistoryBinding.bind(view)
val exampleAdapter = ExampleAdapter()
binding.apply{
recyclerView.apply{
layoutManager = LinearLayoutManager(requireContext())
adapter = exampleAdapter
setHasFixedSize(true)
}
}
setFragmentResultListener("add_receipt_request"){_,bundle ->
val result = bundle.getInt("add_receipt_request")
viewModel.onAddResult(result)
}
viewModel.receipts.observe(viewLifecycleOwner){ /// New Items get passed to the List
exampleAdapter.submitList(it)
}
}
}
The ViewModel:
#HiltViewModel
class PurchaseViewmodel #Inject constructor(
private val receiptDao: ReceiptDao
): ViewModel() {
private val tasksEventChannel = Channel<TasksEvent>()
val addTaskEvent = tasksEventChannel.receiveAsFlow()
val receipts = receiptDao.getAllReceipts().asLiveData()
fun onAddResult(result: Int){
when (result){
ADD_RECEIPT_RESULT_OK ->showReceiptSavedConfirmation("Receipt is saved")
}
}
private fun showReceiptSavedConfirmation (text: String) = viewModelScope.launch {
tasksEventChannel.send(TasksEvent.ShowReceiptSavedConfirmation(text))
}
fun deleteReceipt(receipts: Receipts) = viewModelScope.launch {
receiptDao.delete(receipts)
}
sealed class TasksEvent {
data class ShowReceiptSavedConfirmation(val msg: String) : TasksEvent()
}
}
And the Adapter:
class ExampleAdapter : ListAdapter<Receipts,ExampleAdapter.ExampleViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExampleViewHolder { // Basically how to get a new Item from the List and display it
val binding = ReceiptsBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return ExampleViewHolder(binding)
}
override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
override fun getItemCount(): Int {
return super.getItemCount()
}
class ExampleViewHolder(private val binding: ReceiptsBinding) : RecyclerView.ViewHolder(binding.root){ //Examples One Row in our list
fun bind (receipts: Receipts) {
binding.apply {
storeHistory.text = receipts.store
amountHistory.text = receipts.total
dateHistory.text = receipts.date
}
}
}
class DiffCallback : DiffUtil.ItemCallback<Receipts>() {
override fun areItemsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem == newItem
}
}
Create a listener using Interface
Implement the listener in Fragment
Create listener variable in Adapter
pass the listener from fragment to adapter
pass the listener to viewHolder
on Delete button click of that item call the listener method, which will trigger the implementation in Fragment where you have access to your ViewModel
delete the receipt using viewModel.deleteReceipt

Categories

Resources