kotlinandroid-fragmentsandroid-recyclerviewandroid-adapterlistadapter

List Adapter doesnt get updated after updating an item in dataBase


I'm writing a note app with RoomDB and when I try to update an existing note it updates prefectly in the database but i have to either restart the app or open another fragment and then com back for the Recycler view to get updated

here is my adapter :

class RVNotesAdapter : ListAdapter<Note, RVNotesAdapter.NoteViewHolder>(DiffUtilCallback()) {


    inner class NoteViewHolder(item: View) : RecyclerView.ViewHolder(item) {
        val binding = NoteItemLayoutBinding.bind(item)
        val title = binding.noteItemTitle
        val content = binding.noteContentItem
        val date = binding.noteDate
        val parent = binding.noteItemLayoutParent
        val markWon = Markwon.builder(item.context)
            .usePlugin(StrikethroughPlugin.create())
            .usePlugin(TaskListPlugin.create(item.context))
            .usePlugin(object : AbstractMarkwonPlugin() {
                override fun configureVisitor(builder: MarkwonVisitor.Builder) {
                    super.configureVisitor(builder)
                    builder.on(
                        SoftLineBreak::class.java
                    ) { visitor, _ ->
                        visitor.forceNewLine()
                    }
                }
            }).build()
    }


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
        return NoteViewHolder(LayoutInflater.from(parent.context)
            .inflate(R.layout.note_item_layout,parent,false))
    }

    override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
        getItem(position).let {note->
            holder.apply {
                parent.transitionName = "recyclerView_${note.id}"
                title.text = note.title
                markWon.setMarkdown(content,note.content)
                date.text = note.date
                parent.setCardBackgroundColor(note.color)

                itemView.setOnClickListener {
                    val action = NoteFragmentDirections.actionNoteFragmentToSaveOrUpdateFragment()
                        .setNote(note)

                    val extras = FragmentNavigatorExtras(parent to "recyclerView_${note.id}")
                    it.hideKeyboard()
                    Navigation.findNavController(it).navigate(action,extras)
                }

                content.setOnClickListener {
                    val action = NoteFragmentDirections.actionNoteFragmentToSaveOrUpdateFragment()
                        .setNote(note)

                    val extras = FragmentNavigatorExtras(parent to "recyclerView_${note.id}")
                    it.hideKeyboard()
                    Navigation.findNavController(it).navigate(action,extras)
                }
            }
        }
    }
}

here is the DiffUtil class :

class DiffUtilCallback : DiffUtil.ItemCallback<Note>() {
    override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean {
        return oldItem.id == newItem.id
    }
    override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean {
        return oldItem.id == newItem.id
    }
}

here is the NoteFragment :

@AndroidEntryPoint
class NoteFragment : Fragment(R.layout.fragment_note) {

    private lateinit var binding: FragmentNoteBinding
    private val noteActivityViewModel by viewModels<NoteActivityViewModel>()
    private lateinit var rvNotesAdapter: RVNotesAdapter

   
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding = FragmentNoteBinding.bind(view)
        val activity = activity as MainActivity
        val navController = Navigation.findNavController(view)
        requireView().hideKeyboard()
        CoroutineScope(Dispatchers.Main).launch {
            delay(10)
//            activity.window.statusBarColor = Color.WHITE
            activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
            activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
            activity.window.statusBarColor = Color.parseColor("#9e9d9d")
        }





        binding.addNoteFab.setOnClickListener {
            binding.appBar.visibility = INVISIBLE
            navController.navigate(NoteFragmentDirections.actionNoteFragmentToSaveOrUpdateFragment())
        }

        binding.innerFab.setOnClickListener {
            binding.appBar.visibility = INVISIBLE
            navController.navigate(NoteFragmentDirections.actionNoteFragmentToSaveOrUpdateFragment())
        }

        recyclerViewDisplay()
        swipeToDelete(binding.rvNote)


        // implement search
        binding.search.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                binding.noData.isVisible = false
            }

            override fun onTextChanged(s: CharSequence?, p1: Int, p2: Int, p3: Int) {
                if (s.toString().isNotEmpty()) {
                    val text = s.toString()
                    val query = "%$text%"
                    if (query.isNotEmpty()) {
                        noteActivityViewModel.searchNote(query).observe(viewLifecycleOwner) {
                            rvNotesAdapter.submitList(it)
                        }
                    } else {
                        observerDataChanges()
                    }
                } else {
                    observerDataChanges()
                }
            }


            override fun afterTextChanged(p0: Editable?) {

            }

        })

        binding.search.setOnEditorActionListener { v, actionId, _ ->
            if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                v.clearFocus()
                requireView().hideKeyboard()
            }

            return@setOnEditorActionListener true
        }

        binding.rvNote.setOnScrollChangeListener { _, scrollX, scrollY, _, oldscrollY ->

            when {
                scrollY > oldscrollY -> {
                    binding.fabText.isVisible = false
                }

                scrollX == scrollY -> {
                    binding.fabText.isVisible = true
                }

                else -> {
                    binding.fabText.isVisible = true
                }
            }
        }


    }

    private fun swipeToDelete(rvNote: RecyclerView) {
        val swipeToDeleteCallBack = object : SwipeToDelete() {
            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                val position = viewHolder.absoluteAdapterPosition
                val note = rvNotesAdapter.currentList[position]
                var actionBTNTab = false
                noteActivityViewModel.deleteNote(note)
                binding.search.apply {
                    hideKeyboard()
                    clearFocus()
                }

                if (binding.search.toString().isEmpty()) {
                    observerDataChanges()
                }

                val snackBar = Snackbar.make(
                    requireView(),
                    "یادداشت حذف شد",
                    Snackbar.LENGTH_SHORT
                )
                    .addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
                        override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
                            super.onDismissed(transientBottomBar, event)

                        }

                        override fun onShown(transientBottomBar: Snackbar?) {
                            transientBottomBar?.setAction("برگرد") {
                                noteActivityViewModel.addNote(note)
                                actionBTNTab = true
                                binding.noData.isVisible = false
                            }
                            super.onShown(transientBottomBar)
                        }
                    })
                    .apply {
                        animationMode = Snackbar.ANIMATION_MODE_SLIDE
                        setAnchorView(R.id.add_note_fab)
                    }
                snackBar.setActionTextColor(
                    ContextCompat.getColor(
                        requireContext(),
                        R.color.yellowOrange
                    )
                )
                    .show()
            }

        }

        val itemTouchHelper = ItemTouchHelper(swipeToDeleteCallBack)
        itemTouchHelper.attachToRecyclerView(rvNote)
    }

    private fun recyclerViewDisplay() {
        when (resources.configuration.orientation) {
            Configuration.ORIENTATION_PORTRAIT -> {
                setUpRecyclerView(2)
            }

            Configuration.ORIENTATION_LANDSCAPE -> {
                setUpRecyclerView(3)
            }
        }
    }

    private fun setUpRecyclerView(spanCount: Int) {
        binding.rvNote.apply {
            layoutManager =
                StaggeredGridLayoutManager(spanCount, StaggeredGridLayoutManager.VERTICAL)
            setHasFixedSize(true)
            rvNotesAdapter = RVNotesAdapter()
            rvNotesAdapter.stateRestorationPolicy =
                RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
            adapter = rvNotesAdapter
            postponeEnterTransition(300L, TimeUnit.MILLISECONDS)
            viewTreeObserver.addOnPreDrawListener {
                startPostponedEnterTransition()
                true
            }
        }
        observerDataChanges()
    }

    private fun observerDataChanges() {
        noteActivityViewModel.allNotes.observe(viewLifecycleOwner) { list ->
            binding.noData.isVisible = list.isEmpty()
            rvNotesAdapter.submitList(list)
        }
    }

}

and here is all the code for saveOrUpdate Fragment :


@AndroidEntryPoint
class SaveOrUpdateFragment : Fragment(R.layout.fragment_save_or_update) {

 private lateinit var navController: NavController
    private lateinit var binding: FragmentSaveOrUpdateBinding
    private var note: Note? = null
    private var color = -1
    private lateinit var result: String
    private val noteActivityViewModel by viewModels<NoteActivityViewModel>()
    private val currentDate = SimpleDateFormat.getInstance().format(Date())
    private val job = CoroutineScope(Dispatchers.Main)
    private val args: SaveOrUpdateFragmentArgs by navArgs()
    private lateinit var rvNotesAdapter: RVNotesAdapter

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val animation = MaterialContainerTransform().apply {
            drawingViewId = R.id.fragment
            scrimColor = Color.TRANSPARENT
            duration = 300
        }

        sharedElementEnterTransition = animation
        sharedElementReturnTransition = animation
    }


    @SuppressLint("SetTextI18n")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding = FragmentSaveOrUpdateBinding.bind(view)



        navController = Navigation.findNavController(view)
        val activity = activity as MainActivity


        ViewCompat.setTransitionName(
            binding.noteContentFragParent,
            "recyclerView_${args.note?.id}"
        )

        binding.backButton.setOnClickListener {
            requireView().hideKeyboard()
            navController.popBackStack()
        }




        binding.saveNote.setOnClickListener {
            saveNote()
        }

        try {
            binding.etNoteContent.setOnFocusChangeListener { view, hasFocus ->
                if (hasFocus) {
                    binding.bottomBar.visibility = View.VISIBLE
                    binding.etNoteContent.setStylesBar(binding.styleBar)
                } else {
                    binding.bottomBar.visibility = View.GONE
                }
            }
        } catch (e: Exception) {
            Log.e("TAG", "$e")
        }

        binding.fabColorPicker.setOnClickListener {
            val bottomSheetDialog = BottomSheetDialog(
                requireContext(),
                R.style.BottomSheetDialogTheme
            )


            val bottomSheetView: View = layoutInflater.inflate(R.layout.bottom_sheet_layout, null)

            with(bottomSheetDialog) {
                setContentView(bottomSheetView)
                show()
            }

            val bottomSheetBinding = BottomSheetLayoutBinding.bind(bottomSheetView)
            bottomSheetBinding.apply {
                colorPicker.apply {
                    setSelectedColor(color)
                    setOnColorSelectedListener { value ->
                        color = value
                        binding.apply {
                            noteContentFragParent.setBackgroundColor(color)
                            toolbarFragNoteContent.setBackgroundColor(color)
                            bottomBar.setBackgroundColor(color)
                            activity.window.statusBarColor = color
                        }
                        bottomSheetBinding.bottomSheetParent.setCardBackgroundColor(color)
                    }
                }
                bottomSheetParent.setCardBackgroundColor(color)
            }
            bottomSheetView.post {
                bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
            }
        }

        // opens existing item
        setUpNote()

    }

    @SuppressLint("SetTextI18n")
    private fun setUpNote() {
        val note = args.note
        val title = binding.etTitle
        val content = binding.etNoteContent
        val lastEdited = binding.lastEdited

        if (note == null){
            binding.lastEdited.text =
                "${getString(R.string.edited_on)}: ${SimpleDateFormat.getDateInstance().format(Date())}"
        }
        if (note!=null){
            title.setText(note.title)
            content.renderMD(note.content)
            lastEdited.text = "${getString(R.string.edited_on)}: ${note.date}"
            color = note.color
            binding.apply {
                job.launch {
                    delay(10)
                    noteContentFragParent.setBackgroundColor(color)
                }

                toolbarFragNoteContent.setBackgroundColor(color)
                bottomBar.setBackgroundColor(color)

            }
            activity?.window?.statusBarColor = note.color
        }
    }

    private fun saveNote() {
        if (binding.etNoteContent.text.toString().isEmpty() || binding.etTitle.text.toString()
                .isEmpty()
        ) {
            Toast.makeText(activity, "یادداشت نباید خالی باشد", Toast.LENGTH_SHORT).show()
        } else {
            note = args.note

            when (note) {
                null -> {
                    noteActivityViewModel.addNote(
                        Note(
                            0,
                            title = binding.etTitle.text.toString(),
                            content = binding.etNoteContent.getMD(),
                            color = color,
                            date = currentDate
                        )
                    )

                    result = "ذخیره شد"
                    setFragmentResult(
                        "key",
                        bundleOf("bundleKey" to result)
                    )

                    navController.navigate(SaveOrUpdateFragmentDirections.actionSaveOrUpdateFragmentToNoteFragment())
                }

                else -> {
                    updateNote()
                    navController.popBackStack()
                }
            }
        }
    }

    private fun updateNote() {
        if (note != null) {
            noteActivityViewModel.updateNote(
                Note(
                    id = note!!.id,
                    content = binding.etNoteContent.getMD(),
                    title = binding.etTitle.text.toString(),
                    date = currentDate,
                    color = color
                )
            )
        }
    }


}

I would appreciate any help.


Solution

  • Your Problem lies in your diffUtil class.

    Short answer:

    Change areContentsTheSame to this:

    override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean {
            return oldItem == newItem
        }
    

    Long answer:

    DiffUtil has two methods:

    The second one is the one you are doing right, as i mentioned it is only called when the previous function returns true, as you are checking ids then definitely the ids are gonna be the same (which is the condition you are checking here too) so item does not change.

    In here you need to check if the Content of the class has changed, which two most used way are hashCode comparison oldItem.hashCode() == newItem.hashCode() or equality comparison oldItem == newItem. although if you have some data in you class which changes but is not shown in the ui (and you have alot of fields) i recommend doing a more detail check:

    return oldItem.content == newItem.content &&
        oldItem.title == newItem.title &&
        oldItem.color == newItem.color
    

    Be aware that using a custom Equality check, means if you add something UI related to your data class, you need to add it here as well, so ui is updated