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.
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:
areItemsTheSame
: when comparing items in both lists, first this function is called so it knows if two items are the same (to then check for changes). so in here you need to check their unique identifier like id
which you are doing correct.areContentsTheSame
: This is called after the previous one returns true, and it wants you to compare the contents of these classes to see if they are changed.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