Renaming item with DiffUtil not updating RecyclerView - android

For an app I am making I have a list in which I display pixel art creations, I do this with a RecyclerView and DiffUtil, here is the code:
package com.therealbluepandabear.pixapencil.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.therealbluepandabear.pixapencil.R
import com.therealbluepandabear.pixapencil.databinding.RecentCreationsLayoutBinding
import com.therealbluepandabear.pixapencil.enums.SnackbarDuration
import com.therealbluepandabear.pixapencil.extensions.setOnLongPressListener
import com.therealbluepandabear.pixapencil.extensions.showSnackbar
import com.therealbluepandabear.pixapencil.listeners.RecentCreationsListener
import com.therealbluepandabear.pixapencil.models.PixelArt
import com.therealbluepandabear.pixapencil.viewholders.PixelArtViewHolder
class PixelArtAdapter(
private val snackbarView: View,
private val listener: RecentCreationsListener,
private val context: Context
) : ListAdapter<PixelArt, RecyclerView.ViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding = RecentCreationsLayoutBinding.inflate(LayoutInflater.from(parent.context))
return PixelArtViewHolder(binding, context)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val pixelArt = getItem(position)
if (holder is PixelArtViewHolder) {
holder.bind(pixelArt)
holder.binding.recentCreationsLayoutMaterialCardView.setOnClickListener {
listener.onCreationTapped(pixelArt)
}
holder.binding.recentCreationsLayoutMaterialCardView.setOnLongPressListener {
listener.onCreationLongTapped(pixelArt)
}
holder.binding.recentCreationsLayoutFavoriteButton.setOnClickListener {
if (pixelArt.starred) {
pixelArt.starred = false
listener.onUnstarredTapped(pixelArt)
unFavouriteRecentCreation(snackbarView, pixelArt)
holder.bind(pixelArt)
} else {
pixelArt.starred = true
listener.onStarredTapped(pixelArt)
favouriteRecentCreation(snackbarView, pixelArt)
holder.bind(pixelArt)
}
}
}
}
private fun favouriteRecentCreation(contextView: View, pixelArt: PixelArt) { // move to listener
contextView.showSnackbar(contextView.context.getString(R.string.snackbar_pixel_art_project_saved_to_starred_items_in_code_str, pixelArt.title), SnackbarDuration.Default)
pixelArt.starred = true
}
private fun unFavouriteRecentCreation(contextView: View, pixelArt: PixelArt) {
contextView.showSnackbar(contextView.context.getString(R.string.snackbar_pixel_art_project_removed_from_starred_items_in_code_str, pixelArt.title), SnackbarDuration.Default)
pixelArt.starred = false
}
companion object {
val diffCallback: DiffUtil.ItemCallback<PixelArt> = object : DiffUtil.ItemCallback<PixelArt>() {
override fun areItemsTheSame(oldItem: PixelArt, newItem: PixelArt): Boolean {
return oldItem.objId == newItem.objId
}
override fun areContentsTheSame(oldItem: PixelArt, newItem: PixelArt): Boolean {
return oldItem == newItem
}
}
}
}
ViewHolder:
class PixelArtViewHolder(val binding: RecentCreationsLayoutBinding, private val context: Context) : RecyclerView.ViewHolder(binding.root) {
private fun loadPixelArtCoverImage(pixelArt: PixelArt) {
val widthHeight = if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
350
} else {
750
}
val requestOptions: RequestOptions = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.priority(Priority.IMMEDIATE)
.encodeFormat(Bitmap.CompressFormat.PNG)
.override(widthHeight, widthHeight)
.centerInside()
.format(DecodeFormat.DEFAULT)
Glide.with(itemView.context)
.setDefaultRequestOptions(requestOptions)
.load(File(itemView.context.getFileStreamPath(pixelArt.coverBitmapFilePath).absolutePath))
.transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.drawable.transparent_placeholder)
.into(binding.recentCreationsLayoutImageView)
}
private fun loadPixelArtTitle(pixelArt: PixelArt) {
if (pixelArt.title.length > 6) {
binding.recentCreationsLayoutTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.recentCreationsLayoutTitle.isSelected = true
binding.recentCreationsLayoutTitle.isSingleLine = true
(pixelArt.title + " ".repeat(10)).repeat(200).also { binding.recentCreationsLayoutTitle.text = it }
} else {
binding.recentCreationsLayoutTitle.text = pixelArt.title
}
}
private fun loadPixelArtStarred(pixelArt: PixelArt) {
binding.recentCreationsLayoutFavoriteButton.setImageResource(
if (pixelArt.starred) {
R.drawable.ic_baseline_star_24
} else {
R.drawable.ic_baseline_star_border_24
}
)
}
fun bind(pixelArt: PixelArt){
loadPixelArtCoverImage(pixelArt)
binding.recentCreationsLayoutSubtitle.text = context.getString(R.string.recentCreationsLayoutSubtitle_str, pixelArt.width, pixelArt.height)
loadPixelArtStarred(pixelArt)
loadPixelArtTitle(pixelArt)
}
}
Here is the result:
When the user long taps on a project, they get the following dialog:
When they press 'Rename', they get the following dialog where they can rename the project:
My issue is, that when the user types in a new name, and then presses OK, the data is not updating. Sometimes it takes twice to update, sometimes I need to restart the app for it to update, and sometimes it doesn't update at all.
Here is the code responsible for renaming:
fun MainActivity.extendedOnRenameTapped(pixelArt: PixelArt, bottomSheetDialog: BottomSheetDialog) {
val inflatedActivity = activity()?.layoutInflater?.inflate(R.layout.save_file_under_new_name_alert, activity()?.findViewById(android.R.id.content),false)
val textInput: TextInputLayout = inflatedActivity as TextInputLayout
showDialog(
getString(R.string.dialog_rename_title_in_code_str),
null,
getString(R.string.generic_ok_in_code_str), { _, _ ->
val input: String = textInput.editText?.text.toString()
if (input.isNotBlank()) {
pixelArt.title = input
pixelArtViewModel.update(pixelArt)
adapter.submitList(pixelArtData)
bottomSheetDialog.dismiss()
}
}, getString(R.string.generic_cancel_in_code_str), null, view = textInput, dimBackground = false
)
}
I am following everything by the book, so I am confused why this is not working.
Edit
I tried to make it all 'val' and then add this:
pixelArtViewModel.update(pixelArt.copy(title = input))
pixelArtViewModel.getAll().observe(this) {
adapter.submitList(it)
}
bottomSheetDialog.dismiss()
Still not working.

I see that you are setting pixelArt.title, which means your PixelArt class is mutable (has var properties or val properties that reference mutable classes). DiffUtil is 100% incompatible with mutable classes, because they make it impossible to compare items in the old and new lists. It will see the old list as having the new value already so it will treat it as unchanged.
Example with my imagined version of your PixelArt class.
data class PixelArt(
val objId: Long,
val name: String,
val starred: Boolean,
val imageFilePath: String
)
// In ViewModel:
// You probably have the list backed up to disk somehow. I'm just using
// placeholder functions to represent working with the repo or files or
// whatever you use.
val pixelArtLiveData = MutableLiveData<List<PixelArt>>().also {
viewModelScope.launch { it.value = readThePersistedData() }
}
private fun modifyItem(oldItem: PixelArt, newItem: PixelArt) {
pixelArtLiveData.value = pixelArtLiveData.value.orEmpty()
.map { if (it == oldItem) newItem else it }
// also update your persisted data here
}
fun renameItem(originalItem: PixelArt, newName: String) {
modifyItem(originalItem, originalItem.copy(name = newName))
}
fun toggleItemStarred(originalItem: PixelArt) {
modifyItem(originalItem, originalItem.copy(starred = !originalItem.starred))
}
// etc. or you could just make modifyItem public instead of making
// all these helper functions
Then in your adapter, you must call through to these ViewModel functions instead of directly modifying the items or the list or calling submitList. Since the adapter doesn't have direct access to the ViewModel, you probably use your RecentCreationsListener for this by adding
appropriate actions to it that your various click listeners can call.
Your Activity or Fragment would observe this LiveData and simply call submitList() with the observed value.

Related

Android RecyclerView items list doesn't change appropriately after chang settings and navigate again to fragment

I have single activity android application, a service app with task management. For navigation i use navigation component with bottom navigation. I also use data binding and Dagger2 DI, if it could be important for problem investigation.
After user successfully logged in, home screen with a list of queues (horizontal recyclerview) appears.
Home Fragment : -
Each queue (recyclerview item) has appropriate list of tasks available to perform by the user.
You can select any queue item which is active (contains at least one task in it). Tasks of the selected queue displayed below the recyclerview like another vertical recylcerview. There is also the summary queue item (very left item on the picture) which calculated and shows all tasks from all available queues.
Appearance of this summary queue item depends on the switch which is on the profile screen which is represented as Profile Fragment
Profle fragment : -
Scenario:
Summary queue item shown on Home screen by default;
I navigate to Profile screen and set switcher off. Here in ProfileFragment i call the method updateGeneralQueueState in view model which save at room db parameter isShouldBeShown (false in this case);
I navigate back to the Home screen. Here i retrieve isShouldBeShown parameter in my Home Fragment with calling apropriate method in view model which returns a earlier saved parameter from room db.
Problem:
I expect to see that summary queue item is not in the list of queues and most often it is, but sometimes when i repeat this scenario it is not. If not i go to profile fragment or any other screen, then go to home screeen again and then the summary queue item is not in the list as expected.
There are probably some architectural mistackes, thats why I'm asking for real help and explaining the reason for problem occurrence, as I would like not just only solve it, but also to understand this strange behavior.
I will attach below all related code! Many thanks in advance!
HomeFragment.kt
class HomeFragment : BaseFragment<HomeFragmentBinding>(), MenuItem.OnActionExpandListener {
#Inject lateinit var factory: HomeViewModelFactory
#Inject lateinit var viewModel: HomeViewModel
private lateinit var ticketsListAdapter: TicketsListAdapter
private lateinit var queuesListAdapter: QueuesListAdapter
private var searchView: SearchView? = null
private var pageLimit: Long = 10
private var offset: Long = 0L
private var selectedQueueId: Long = 0L
private var selectedQueueIndex: Int = 0
private var prevTicketsThreshold: Int = 0 // new
private var ticketsThreshold: Int = 0
private var lockId: Int = 1
private var allQueueIds: List<Long> = listOf()
private var isGeneralShoudlBeShown: Boolean = false
private var favoriteMode: Boolean = false
private lateinit var prefs: Prefs
private var selectedQueue: Queue? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
ComponentsHolder.getComponent().inject(this)
super.onViewCreated(view, savedInstanceState)
prefs = Prefs(requireContext())
(activity as MainActivity).showBottomNavigation()
(activity as MainActivity).getUnreadNotificationsCount()
val toolbar = view.findViewById(R.id.tickets_search_toolbar) as Toolbar
(activity as MainActivity).setSupportActionBar(toolbar)
toolbar.title = "Главная"
setHasOptionsMenu(true)
viewModel = ViewModelProvider(this, factory)[HomeViewModel::class.java]
binding.model = viewModel
binding.lifecycleOwner = this
with(viewModel) {
(activity as MainActivity).getPushToken { t ->
registerPushToken(t)
getUserSettings()
getUnreadNotificationsCount()
}
notificationscount.observe(viewLifecycleOwner) {
it?.let {
if (it.unreadCount > 0) {
(activity as MainActivity).setUnreadNotificationsCount(it.unreadCount)
.also { (activity as MainActivity).getUnreadNotificationsCount() }
}
}
}
checkUserSettings.observe(viewLifecycleOwner) {
isGeneralShoudlBeShown = it.isGeneralChecked
favoriteMode = it.isFavoritesChecked!!
getQueues(isGeneralShoudlBeShown, favoriteMode, selectedQueueIndex)
}
queueIds.observe(viewLifecycleOwner) {
it?.let {
allQueueIds = it
}
}
queues.observe(viewLifecycleOwner) {
it?.let {
when (it.responseCode) {
200 -> {
queuesListAdapter.submitList(it.queues)
queuesListAdapter.notifyDataSetChanged()
retrieveSelectedQueue(it.queues)
getTickets(
if (selectedQueueId == 0L) 0 else selectedQueueId,
if (selectedQueueId == 0L) allQueueIds else emptyList(),
lockId,
pageLimit,
offset
)
}
}
}
}
tickets.observe(viewLifecycleOwner) {
it?.let {
binding.refreshDate.text = getLastRefreshDateTime()
Log.i("hmfrgmnt", it.toString())
when (it.responseCode) {
401 -> {
binding.bottomProgress.visibility = View.GONE
if (mayNavigate()) {
findNavController().navigate(
HomeFragmentDirections
.actionHomeFragmentToSplashFragment()
)
}
}
200 -> {
binding.bottomProgress.visibility = View.GONE
ticketsListAdapter.submitList(null)
ticketsListAdapter.notifyDataSetChanged()
}
else -> (activity as MainActivity).showErrorDialog(
it.responseMessage!!,
null
)
}
}
}
navigateToTicketDetails.observe(viewLifecycleOwner) { ticketId ->
ticketId?.let {
if (mayNavigate()) {
findNavController().navigate(
HomeFragmentDirections
.actionHomeFragmentToTicketDetailsFragment(ticketId)
)
}
viewModel.onTicketDetailsNavigated()
}
}
}
with(binding) {
tabs.selectTab(tabs.getTabAt((lockId - 1)), true)
(queueList.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(selectedQueueIndex, queueList.top)
ticketsListAdapter = TicketsListAdapter(TicketsListListener { ticketId ->
viewModel.onTicketDetailsClicked(ticketId)
})
queuesListAdapter = QueuesListAdapter(
QueuesListListener { queue ->
setActiveQueueData(queue)
tabs.selectTab(tabs.getTabAt((lockId - 1)), true)
viewModel.onQueueClicked(if (queue.queueId == 0L) 0 else selectedQueueId, if (queue.queueId == 0L) allQueueIds else emptyList(), lockId, pageLimit, offset)
// ticketsListAdapter.notifyDataSetChanged()
}
)
ticketsList.adapter = ticketsListAdapter
queueList.adapter = queuesListAdapter
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
when (tab?.position) {
1 -> {
offset = 0
lockId = 2
viewModel.onQueueClicked(if (selectedQueueId == 0L) 0 else selectedQueueId, if (selectedQueueId == 0L) allQueueIds else emptyList(), lockId, pageLimit, offset)
}
else -> {
offset = 0
lockId = 1
viewModel.onQueueClicked(if (selectedQueueId == 0L) 0 else selectedQueueId, if (selectedQueueId == 0L) allQueueIds else emptyList(), lockId, pageLimit, offset)
}
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabReselected(tab: TabLayout.Tab?) {}
})
nestedScroll.setOnScrollChangeListener { v, _, scrollY, _, _ ->
if ((scrollY > (v as NestedScrollView).getChildAt(0).measuredHeight - v.measuredHeight - homeMainLayout.paddingBottom) && viewModel.status.value != ApiStatus.LOADING) {
if (ticketsThreshold > prevTicketsThreshold) {
if (ticketsThreshold < pageLimit || ticketsThreshold == 0) {
moreButton.visibility = View.GONE
endOfListView.visibility = View.VISIBLE
} else {
moreButton.visibility = View.VISIBLE
endOfListView.visibility = View.GONE
}
} else if (ticketsThreshold == prevTicketsThreshold) {
moreButton.visibility = View.GONE
endOfListView.visibility = View.VISIBLE
} else {
moreButton.visibility = View.VISIBLE
endOfListView.visibility = View.GONE
}
}
}
refreshButton.setOnClickListener {
offset = 0
viewModel.refresh(isGeneralShoudlBeShown, favoriteMode, selectedQueueIndex, selectedQueueId, allQueueIds, lockId, pageLimit, offset)
(queueList.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(selectedQueueIndex, queueList.top)
tabs.selectTab(tabs.getTabAt((lockId - 1)), true)
queuesListAdapter.notifyDataSetChanged()
}
moreButton.setOnClickListener {
prevTicketsThreshold = ticketsThreshold
offset += pageLimit
viewModel.getTickets(
if (selectedQueueId == 0L) 0 else selectedQueueId,
if (selectedQueueId == 0L) allQueueIds else emptyList(),
lockId,
pageLimit,
offset
)
}
}
}
override fun getFragmentBinding(
inflater: LayoutInflater,
container: ViewGroup?
) = HomeFragmentBinding.inflate(inflater, container, false)
private fun setActiveQueueData(queue: Queue) {
offset = 0
selectedQueue = queue
prefs.queueObject = queue
binding.selectedQueueTitle.text = queue.title
selectedQueueIndex = queuesListAdapter.currentList.getQueuePosition(selectedQueue as Queue) ?: 0
queuesListAdapter.currentList.forEach { i -> i.isSelected = false }
queuesListAdapter.notifyDataSetChanged()
queuesListAdapter.selectItem(selectedQueueIndex)
(binding.queueList.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(selectedQueueIndex, binding.queueList.top)
}
private fun saveSelectedQueueBeforeNavigating(selectedQueue: Queue) {
prefs.queueObject = selectedQueue
}
override fun onDestroyView() {
super.onDestroyView()
Log.i("profileSaveQueue", "i will save queue: $selectedQueue")
saveSelectedQueueBeforeNavigating(selectedQueue!!)
}
}
HomeViewModel.kt
class HomeViewModel #Inject constructor(
private val userRepository: UserRepository,
private val ticketsRepository: TicketsRepository,
private val queuesRepository: QueuesRepository,
private val notificationsRepository: NotificationsRepository,
private val pushRepository: PushRepository
) : BaseViewModel() {
private var ticketsList: MutableList<Ticket> = mutableListOf()
private var summaryTicketsCount: Int? = 0
private val _status = MutableLiveData<ApiStatus>()
val status: LiveData<ApiStatus>
get() = _status
private val _notificationsCount = MutableLiveData<NoticeCountResponse?>()
val notificationscount: LiveData<NoticeCountResponse?>
get() = _notificationsCount
private val _tickets = MutableLiveData<TicketsResponse?>()
val tickets: LiveData<TicketsResponse?>
get() = _tickets
private val _navigateToTicketDetails = MutableLiveData<Long?>()
val navigateToTicketDetails
get() = _navigateToTicketDetails
private val _queues = MutableLiveData<QueuesResponse?>()
val queues: LiveData<QueuesResponse?>
get() = _queues
private val _queueIds = MutableLiveData<List<Long>?>()
val queueIds: LiveData<List<Long>?>
get() = _queueIds
private val _checkUserSettings = MutableLiveData<User>()
val checkUserSettings: LiveData<User>
get() = _checkUserSettings
fun refresh(showGeneral: Boolean, favoriteOnly: Boolean, selectedQueueIndex: Int, queueId: Long, queueIds: List<Long>?, lockId: Int?, limit: Long?, offset: Long?) {
ticketsList = mutableListOf()
getQueues(showGeneral, favoriteOnly, selectedQueueIndex)
}
fun getUserSettings() {
viewModelScope.launch {
_checkUserSettings.value = retrieveUserSettings()
}
}
private suspend fun retrieveUserSettings(): User? {
return withContext(Dispatchers.IO) {
userRepository.getUserInfo()
}
}
fun getUnreadNotificationsCount() {
_status.value = ApiStatus.LOADING
viewModelScope.launch {
kotlin.runCatching { notificationsRepository.getUnreadNotificationsCount("Bearer ${getToken()}") }
.onSuccess {
_notificationsCount.value = it
_status.value = ApiStatus.DONE
}
.onFailure {
_status.value = ApiStatus.DONE
}
}
}
fun registerPushToken(token: String) {
viewModelScope.launch {
pushRepository.registerToken("Bearer ${getToken()}", TokenRegisterBody(token, 1))
}
}
fun getQueues(showGeneral: Boolean, favoriteOnly: Boolean, selectedQueueIndex: Int) {
_status.value = ApiStatus.LOADING
viewModelScope.launch {
kotlin.runCatching { queuesRepository.getQueuesListWithTicketsCount("Bearer ${getToken()}", favoriteOnly) }
.onSuccess { value ->
summaryTicketsCount = value.queues?.mapNotNull { q -> q.ticketsCount }?.sum()
val queuesList: List<Queue> = sortQueues(value.queues, selectedQueueIndex, showGeneral)
_queueIds.value = value.queues?.map { item -> item.queueId }
_queues.value = QueuesResponse(queuesList, value.responseCode, value.responseMessage)
_status.value = ApiStatus.DONE
}
.onFailure {
if (it is HttpException) {
_queues.value = QueuesResponse(null, it.code(), getResponseMessage(it))
_status.value = ApiStatus.DONE
}
else {
_queues.value = QueuesResponse(null, -1, "Что-то пошло не так")
_status.value = ApiStatus.DONE
}
}
}
}
fun getTickets(queueId: Long?, queueIds: List<Long>?, lockId: Int?, limit: Long?, offset: Long?) {
_status.value = ApiStatus.LOADING
val body = TicketsListBody(queueId = queueId, queueIds = queueIds, lockId = lockId, limit = limit, offset = offset)
viewModelScope.launch {
kotlin.runCatching { ticketsRepository.getTickets("Bearer ${getToken()}", body) }
.onSuccess {
it.tickets?.forEach { ticket -> if (ticket !in ticketsList) { ticketsList.add(ticket) } }
_tickets.value = TicketsResponse(ticketsList, it.responseCode, it.responseMessage)
_status.value = ApiStatus.DONE
}
.onFailure {
if (it is HttpException) {
_tickets.value = TicketsResponse(null, it.code(), getResponseMessage(it))
_status.value = ApiStatus.DONE
}
else {
_tickets.value = TicketsResponse(null, -1, "Что-то пошло не так")
_status.value = ApiStatus.DONE
}
}
}
}
private fun sortQueues(queues: List<Queue>?, selectedQueueIndex: Int, showGeneral: Boolean): List<Queue> {
val favoriteQueuesList: List<Queue>? = queues?.toMutableList()
?.filter { a -> a.isInFavoritesList }
?.sortedByDescending { b -> b.ticketsCount }
val restQueuesList: List<Queue>? = queues?.toMutableList()
?.filter { a -> !a.isInFavoritesList }
?.sortedByDescending { b -> b.ticketsCount }
val queuesList: List<Queue> = mutableListOf<Queue>()
.also { items ->
if (showGeneral) {
items.add(0, Queue(0, null, summaryTicketsCount, true,false))
}
favoriteQueuesList?.forEach { a -> items.add(a) }
restQueuesList?.forEach { a -> items.add(a) }
items[selectedQueueIndex].isSelected = true
}
return queuesList
}
fun onTicketDetailsClicked(id: Long) { _navigateToTicketDetails.value = id }
fun onTicketDetailsNavigated() { _navigateToTicketDetails.value = null }
fun onQueueClicked(id: Long, ids: List<Long>?, lockId: Int?, limit: Long?, offset: Long) {
ticketsList = mutableListOf()
getTickets(id, ids, lockId, limit, offset)
}
private suspend fun getToken(): String? {
return withContext(Dispatchers.IO) {
userRepository.getUserInfo()?.sessionValue
}
}
fun logout() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
userRepository.clean()
}
}
}
override fun onCleared() {
super.onCleared()
ticketsList = mutableListOf()
}
}
QueuesListAdapter.kt
class QueuesListAdapter (val clickListener : QueuesListListener):
ListAdapter<Queue, QueuesListAdapter.ViewHolder>(DIFF_CALLBACK) {
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Queue>() {
override fun areItemsTheSame(oldItem: Queue, newItem: Queue): Boolean {
return oldItem.queueId == newItem.queueId
}
override fun areContentsTheSame(oldItem: Queue, newItem: Queue): Boolean {
return oldItem == newItem
}
}
private var statesMap = HashMap<Int,Boolean>()
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
setItemView(item, holder.binding)
holder.bind(item, clickListener)
item.isSelected = statesMap[position] != null
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
fun selectItem(position: Int) {
val item = getItem(position)
item.isSelected = true
statesMap.clear()
statesMap[position] = item.isSelected
notifyItemChanged(position)
}
private fun setItemView(item: Queue, binding: ItemQueueBinding) {
when (item.isSelected) {
true -> {
item.isSelected = false
binding.queueContent.setBackgroundResource(R.drawable.item_selected_queue_background)
binding.queueContent.alpha = 1F
}
false -> {
binding.queueContent.setBackgroundResource(R.drawable.item_queue_background)
if (item.ticketsCount == 0) {
binding.queueContent.isEnabled = false
binding.queueContent.isFavoriteIcon.isEnabled = false
binding.queueContent.alpha = 0.3F
} else {
binding.queueContent.isEnabled = true
binding.queueContent.isFavoriteIcon.isEnabled = true
binding.queueContent.alpha = 1F
}
}
}
}
class ViewHolder private constructor(val binding: ItemQueueBinding): RecyclerView.ViewHolder(
binding.root
) {
fun bind(item: Queue, clickListener: QueuesListListener) {
binding.queues = item
binding.clickListener = clickListener
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemQueueBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
class QueuesListListener(val clickListener: (queue: Queue) -> Unit) {
fun onClick(queue: Queue) {
clickListener(queue)
}
}
ProfileFragment.kt
class ProfileFragment : BaseFragment<ProfileFragmentBinding>() {
#Inject lateinit var factory: ProfileViewModelFactory
#Inject lateinit var viewModel: ProfileViewModel
private lateinit var profileQueuesListAdapter: ProfileQueuesListAdapter
private var initialQueuesList = mutableListOf<Queue>()
private var favorites = mutableMapOf<Long,Boolean>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
ComponentsHolder.getComponent().inject(this)
super.onViewCreated(view, savedInstanceState)
(activity as MainActivity).showBottomNavigation()
(activity as MainActivity).getUnreadNotificationsCount()
viewModel = ViewModelProvider(this, factory)[ProfileViewModel::class.java]
binding.model = viewModel
binding.lifecycleOwner = this
with(viewModel) {
getUserSettings()
checkUserSettings.observe(viewLifecycleOwner) {
it?.let {
favoritesSwitchItem.isChecked = it.isFavoritesChecked!!
generalQueueSwitchItem.isChecked = it.isGeneralChecked
}
}
loggedOut.observe(viewLifecycleOwner) {
it?.let {
if (mayNavigate()) {
findNavController().navigate(
ProfileFragmentDirections
.actionProfileFragmentToLoginFragment()
)
}
}
}
}
with(binding) {
profileAppBar.toolbar.title = "Профиль"
logoutButton.setOnClickListener { viewModel.logout() }
appVersionDescription.text = requireContext().packageManager.getPackageInfo(requireContext().packageName, 0).versionName
generalQueueSwitchItem.setOnCheckedChangeListener { _, _ ->
if (generalQueueSwitchItem.isChecked) {
viewModel.updateGeneralQueueState(true)
} else {
viewModel.updateGeneralQueueState(false)
}
}
favoritesSwitchItem.setOnCheckedChangeListener { _, _ ->
if (favoritesSwitchItem.isChecked) {
viewModel.updateFavoritesState(true)
} else {
viewModel.updateFavoritesState(false)
}
}
}
}
override fun getFragmentBinding(
inflater: LayoutInflater,
container: ViewGroup?
) = ProfileFragmentBinding.inflate(inflater, container, false)
}
ProfileViewModel.kt
class ProfileViewModel #Inject constructor(
private val userRepository: UserRepository,
private val queuesRepository: QueuesRepository
) : BaseViewModel() {
private var summaryTicketsCount: Int? = 0
private var addToFavoritesList = mutableListOf<Long>()
private var removeFromFavoritesList = mutableListOf<Long>()
private val _status = MutableLiveData<ApiStatus>()
val status: LiveData<ApiStatus>
get() = _status
private val _queues = MutableLiveData<QueuesResponse?>()
val queues: LiveData<QueuesResponse?>
get() = _queues
private val _loggedOut = MutableLiveData<Boolean>()
val loggedOut : LiveData<Boolean>
get() = _loggedOut
private val _checkUserSettings = MutableLiveData<User>()
val checkUserSettings: LiveData<User>
get() = _checkUserSettings
init { }
fun logout() {
coroutineScope.launch {
clean().also { _loggedOut.value = true }
}
}
fun getUserSettings() {
coroutineScope.launch {
_checkUserSettings.postValue(retrieveUserSettings())
}
}
private suspend fun retrieveUserSettings(): User? {
return withContext(Dispatchers.IO) {
userRepository.getUserInfo()
}
}
fun updateGeneralQueueState(isShouldBeShown: Boolean) {
_status.value = ApiStatus.LOADING
coroutineScope.launch {
updateGeneralQueue(isShouldBeShown)
_status.value = ApiStatus.DONE
}
}
fun updateFavoritesState(isFavoritesActive: Boolean) {
_status.value = ApiStatus.LOADING
coroutineScope.launch {
updateFavorites(isFavoritesActive)
_status.value = ApiStatus.DONE
}
}
private suspend fun updateGeneralQueue(isShouldBeShown: Boolean) {
withContext(Dispatchers.IO) {
userRepository.updateGeneralQueueState(isShouldBeShown)
}
}
private suspend fun updateFavorites(isFavoritesActive: Boolean) {
withContext(Dispatchers.IO) {
userRepository.updateFavoritesState(isFavoritesActive)
}
}
private suspend fun getToken(): String? {
return withContext(Dispatchers.IO) {
userRepository.getUserInfo()?.sessionValue
}
}
private suspend fun clean() {
withContext(Dispatchers.IO) {
userRepository.clean()
}
}
}
I do not know exactly what's happening since this is a lot of code and quite hard to follow line by line; it's very easy to miss something when reading code on SO, but I do see a few things where you could improve your architecture.
Where I'd start is by looking at parts of your architecture that have "code smells" (A word of caution: Most of the code I'll write will be pseudo-code, and I don't have experience with DataBinding (only ViewBinding), as I'm not a fan of what it does and I've always chosen not to use it, so if Databinding is causing an issue, I wouldn't know for sure.)
Architecture
When leveraging the power of coroutines, you'd want to benefit from the ability to use suspend functions, and the reactive nature of LiveData (or Flow) to observe and react in your UI. I won't go too much detail into every topic, but I'll mention potential testability issues when I see them, since you'll want to Unit Test your business logic and to do that, you ought to keep some things in consideration.
In general, you'd want to follow the Jetpack architecture ideas (unless you work for Square, in which case everything must be different because Google is wrong); with that in mind, I'll just adhere to Google recommended practices where applicable because if you don't like it, you can find your own alternatives ;)
Fragments
I see a lot of state in the Fragments. Lots of booleans, integers, lists, etc. This is normally a red flag. You have a ViewModel, that's where your state should be coming from, the Fragment rarely has reasons to "store" this state locally.
ViewModels
I feel like you're using a lot of LiveData, which is fine, but I believe you'd benefit from a step further by replacing most of that by a combined flow. Each of your internal states is instead a Flow, and you expose to the fragment one (combined) or a couple if you want to split parts of your reactive code. By using the combine(flow1, flow2, etc...) function in your VM, you can then produce a single more cohesive state, and even expose it as a StateFlow for even more efficiency, as you'd then observe the flow from your fragment using something like:
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.yourFlow.collect {...} //or collectLatest, depending on your usecase
}
This is optional, but it would be an improvement over having so many liveDatas floating around.
Fragment - Adapters
I see you have two or three ListView adapters (good), but they don't really need to be lateinit. You're not really adding much, have them created at init:
private val adapter1 = SomeAdapter()
private val adapter2 = AnotherAdater()
Since they are ListAdapters, once you receive data via (livedata/flow) all you should do is adapter1.submitList(...), since they cannot be null ever. By using lateinit (in something you know you're gonna need anyway) you're not really gaining anything, and are introducing complexity. There are better optimizations you can do than lateinit there.
In the end, your fragment should be as dummy as possible. You "load it" when you display it, abide by its crazy lifecycle, and then wire the things up so it can observe a livedata/flow and update its UI with the incoming state, that's all it should do. And navigation of course, but mainly because it's part of the required plumbing you ought to do in the android framework.
If you add more logic/stuff/state, you're putting yourself in a testing corner and a more complex scenario to manage, as fragments are destroyed, detached, re-added, etc.
ListAdapters
Good job using List Adapters, but your adapters have a few issues.
Don't call notifyDataSetChanged, it defeats the purpose. Submit list should do the trick, that's why you have a DiffUtil. If this is not working, well, there are other nuisances with ListAdapter you may need to be aware of, but once you get past those, you should be good to go.
Take for instance this snippet of your code:
fun selectItem(position: Int) {
val item = getItem(position)
item.isSelected = true
statesMap.clear()
statesMap[position] = item.isSelected
notifyItemChanged(position)
}
Why do you have a static hashMap to indicate selection?
The Adapter already has a lot of work to do behind the scenes, it shouldn't have this responsibility.
When something is selected, you do something about it (set some boolean to true like yourItem.isSelected = true for e.g.) and then produce a new list that will be submitted to the adapter and the diffutil will pick the change.
(this is just an example of an operation that mutates your list, it could be something else, but the principle is, don't keep state where it doesn't belong, instead, react to changes received via the expected channels).
ViewHolders/Adapter
This doesn't look bad, but I feel you're not delegating your responsibilities correctly. Your adapter should not have a lot of if statements there. If an item is selected, the ViewHolder should receive this information and act accordingly, not the Adapter. So I'd pass a boolean to your fun bind alongside all the info the ViewHolder needs to set its correct appearance. This is all read-only info anyway.
Coroutine Scope/Dispatchers
Careful with hardcoding Dispatchers.IO all over the place, this makes it impossible to correctly test, as you cannot override the dispatcher that easily. Instead, inject it, since you're using Dagger already. This way your tests will be able to override them.
In the viewModel always do
viewModelScope.launch(injected_dispatcher) {
//call your suspend functions and such
val result = someRepo.getSomething()
someFlow.emit(result)
}
(just an example).
When you test your VM, you'll supply a test dispatcher.
Conclusion
Overall, good job on the architecture, it's better than a huge activity doing all the work ;)
I feel like you could simplify your code a bit, which, in turn, will greatly help you in finding what part is not behaving as expected.
Remember. Activity/Fragment Observes ViewModel, and deals with Android Framework things (as Google calls them "policy delegates") like navigation, intents, etc. They react to data received and pass it along (to an Adapter for e.g.).
A viewModel sits between your source of truth (repos, data layers) and your business logic (split in usecases/interactors or whatever you call them). The ViewModel is there to give your fragile Fragments/Activities, a more stable and longer-living component that will glue your data with your UI.
The Repos/Etc. are all suspend functions that return the data you need from the source, and update it when needed. (e.g. talk to Room DB or an API, or both!) They merely return the data for the VM and higher levels to consume.
UseCase/Interactors are just abstractions to "reuse" the communication between viewmodels and repositories and such (or to some specific logic). They can apply transformations to your data as they see fit, liberating the VM from this resposibility.
E.g. if you have a GetSomethingUseCase, that may, behind the scenes, talk to a repo, wait (suspend), then transform the API/DB Response into something that is needed by the UI, all done without the VM (or the UI) knowing what's going on.
And lastly, make your adapters as small as possible. Remember they already have a lot of responsibilities.
The way I see this working is.
Fragment 1 Starts, VM is init. Fragment observes its state via some livedata/flow when started. Fragment mutates its views to match the state received.
The user goes to Fragment 2 and changes something, this 'something' updates a DB or in-memory data structure.
The user returns to Fragment 1, all is init again or restored, but this time, the liveData/Flow is observed again, and the data comes back (now modified by fragment2). The Fragment updates its UI without thinking much about it, as it's not its responsibility.
I hope this lengthy answer points you in the right direction.
Short of that, I suggest you break down your problem into a smaller one to try to isolate what is not doing what it should. The less "state" you have in random places (Adapters, fragments, etc.) the less the chances of weird problems you're going to have.
Don't fight the framework ;)
Lastly, if you made it this far, if you submit a list to your adapter and it doesn't update, take a look at this SO question/answer as ListAdapter has a "it's not a bug according to google but the documentation doesn't make this clear enough" situation.

Why can I not inherit from ListAdapter with my custom defined class in Kotlin?

I am working on an Todo-list as an android app to get started with Kotlin, but I am running into the problem, that my TodoAdapter class (which is supposed to define what to do with said Todos in a recyclerview as far as I understood?) can't inherit from the ListAdapter class for some reason.
I believe I didn't have the problem before I tried to add persistence to my app by saving to a simple .txt-file as a start. Please have a look at my code below and help me fix my code.
My TodoAdapter class:
class TodoAdapter (
private val todos: MutableList<Todo>
) : ListAdapter<Todo,TodoAdapter.TodoViewHolder>() {
class TodoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
return TodoViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_todo,
parent,
false
)
)
}
fun addTodo(todo: Todo) {
todos.add(todo)
notifyItemInserted(todos.size - 1)
}
fun deleteDoneTodos() {
todos.removeAll { todo ->
todo.isChecked
}
notifyDataSetChanged()
}
private fun toggleStrikeThrough(tvTodoTitle: TextView, isChecked: Boolean) {
if (isChecked) {
tvTodoTitle.paintFlags = tvTodoTitle.paintFlags or STRIKE_THRU_TEXT_FLAG
} else{
tvTodoTitle.paintFlags = tvTodoTitle.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
}
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val curTodo = todos[position]
holder.itemView.apply {
tvTodoTitle.text = curTodo.title //Hier stimmt etwas nicht: tvTodoTitle Import fehlt???
cbDone.isChecked = curTodo.isChecked
toggleStrikeThrough(tvTodoTitle, curTodo.isChecked)
cbDone.setOnCheckedChangeListener{ _, isChecked ->
toggleStrikeThrough(tvTodoTitle, isChecked)
curTodo.isChecked = !curTodo.isChecked
}
}
}
override fun getItemCount(): Int {
return todos.size
}
My data class Todo:
data class Todo(
val title: String,
var isChecked: Boolean = false
)
And this is the code in my MainActivity.kt I tried to add persistence with:
private fun setupInternalStorageRecyclerView() = binding.rvTodoItems.apply {
adapter = todoAdapter
layoutManager = rvTodoItems.layoutManager
}
private fun loadTodoItemsFromInternalStorageIntoRecyclerView() {
lifecycleScope.launch {
val todoItems = loadTodoItemsFromInternalStorage()
todoAdapter.submitList(todoItems)
}
}
private suspend fun loadTodoItemsFromInternalStorage(): List<Todo> {
return withContext(Dispatchers.IO) {
val todoItemList: MutableList<Todo> = mutableListOf<Todo>()
val files = filesDir.listFiles()
files?.filter { it.canRead() && it.isFile && it.name.endsWith(".txt") }?.map {
val lines = it.bufferedReader().readLines()
for (i in lines.indices step 2) {
todoItemList.add(Todo(lines[i], lines[i+1].toBoolean()))
}
todoItemList
} ?: mutableListOf<Todo>()
} as MutableList<Todo>
}
private fun saveTodoItemsToInternalStorage(filename: String, todoItems: List<Todo>): Boolean {
return try{
openFileOutput("$filename.txt", MODE_PRIVATE).use { stream ->
File(filename).printWriter().use { out ->
for (item in todoItems) {
out.println(item.title)
out.println(item.isChecked)
}
}
}
true
} catch(e: IOException) {
e.printStackTrace()
false
}
}
I hope this is enough information to help me with, feel free to ask for more information, I will gladly provide it.
First thing's first, one major problem you have, regardless if you're using ListAdapter, is that you are using your adapter to manage your actual data. You must not use an adapter to be the "master copy" of your data, or else your data will be lost the moment the UI is rebuilt for a screen rotation or someone returning to the app after it has been in the background. Your data should be managed by a ViewModel, and the list instance should be passed along to the adapter by the Activity or Fragment. Your functions that modify the list (such as addTodo()) should be in your ViewModel, not your adapter.
Regarding your specific question, the quick and dirty solution is to inherit from RecyclerView.Adapter instead of ListAdapter:
class TodoAdapter (
private val todos: MutableList<Todo>
) : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
ListAdapter is more work to set up, but the advantage with it is that it does automatic comparisons on a background thread when you update your list content so it can automatically find exactly what has changed and animate changes to your list for you. If you want to use ListAdapter, you must define a DiffUtil.ItemCallback for it and pass that to its constructor. Typically, your Todo class would be defined as an immutable (no vars) data class and then you could define your callback like:
// Inside your Todo class define:
object DiffCallback: DiffUtil.ItemCallback<Todo>() {
override fun areItemsTheSame(oldItem: Todo, newItem: Todo) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Todo, newItem: Todo) =
oldItem == newItem
}
And with ListAdapter, you do not use your own list property. You must pass read-only Lists to it via submitList. So the class definition would look like:
class TodoAdapter: ListAdapter<TodoAdapter.TodoViewHolder>(Todo.DiffCallback) {
There is a lot more you need to understand to use ListAdapter correctly, so you should work through a tutorial if you want to use it. For example: You must not submit mutable Lists to it. It needs a fresh list instance each time you call submitList() so it can compare the new version to the previous version. Your Todo class must not have any mutable properties either. Your line val curTodo = todos[position] would need to be changed to val curTodo = item.

How to hide or NOT SHOW items in a Room Database based on a Boolean

I have a Todo Application and I want to hide (which basically means not showing)the tasks based on its completed status(strikeThrough over the text). However, the hideCompleted tasks implementation I followed isn't working but the sort and search is working and I said this because I put all the Implementations in a single query and made them work together with stateFlow but the hide isn't working. Here is my code.
Okay What I mean by isn't working is that it unchecks the checkBoxes besides the Tasks instead of hiding them.
First My Model class
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.*
/** Our Model class. This class will represent our database table **/
#Entity(tableName = "todo_table")
data class Todo(
#PrimaryKey (autoGenerate = true) // here "Room" will autoGenerate the id for us
instead of assigning a randomUUID value
val id : Int = 0,
var title : String = "",
var date : Date = Date(),
var time : Date = Date(),
var todoCheckBox : Boolean = false
)
Then my Dao. Only the two sort(By date and by Name) functions are directly accessed from the Dao. The others are through the repository.
import androidx.room.*
import com.bignerdranch.android.to_dolist.model.Todo
import kotlinx.coroutines.flow.Flow
/**
* This will be our DAO file where we will be update, delete and add Todos to our
database so it contains the methods used for accessing the database
*/
#Dao
interface TodoDao {
// function to hold all out queries and will be executed based on our sortOrder
fun getAllTasks(query : String, sortOrder: SortOrder, hideCompleted: Boolean) : Flow<List<Todo>> =
when(sortOrder) {
SortOrder.BY_DATE -> getTasksSortedByDateCreated(query, hideCompleted)
SortOrder.BY_NAME -> getTasksSortedByName(query, hideCompleted)
}
#Query("SELECT * FROM todo_table WHERE (todoCheckBox != :hideCompleted OR todoCheckBox = 0) AND title LIKE '%' || :searchQueryText || '%' ORDER BY title COLLATE NOCASE")
fun getTasksSortedByName(searchQueryText : String, hideCompleted : Boolean): Flow<List<Todo>>
#Query("SELECT * FROM todo_table WHERE (todoCheckBox != :hideCompleted OR todoCheckBox = 0) AND title LIKE '%' || :searchQueryText || '%' ORDER BY time ASC")
fun getTasksSortedByDateCreated(searchQueryText : String, hideCompleted : Boolean): Flow<List<Todo>>
// onConflict will ignore any known conflicts, in this case will remove duplicate "Todos" with the same name
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addTodo(todo: Todo)
#Query("DELETE FROM todo_table WHERE id IN (:idList)")
suspend fun deleteSelectedTasks(idList : Long)
#Query("DELETE FROM todo_table")
suspend fun deleteAllTasks()
}
My ViewModel(Where I call the sort functions directly from the Dao)
import android.app.Application
import androidx.lifecycle.*
import com.bignerdranch.android.to_dolist.model.Todo
import com.bignerdranch.android.to_dolist.repository.TodoRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
/** Our AndroidViewModel. This AndroidViewModel holds reference to our Application context. **/
class TodoViewModel(application: Application) : AndroidViewModel(application) {
/**
* NOTE! : "Context" are needed to instantiate a database that is why we are using
an AndroidViewModel in this case because it holds reference to an
* Application context. And if I remember correctly, it will start as the "Application" starts.
**/
private val repository : TodoRepository
private val userDao = TodoDatabase.getDatabase(application).todoDao()
init {
// having access to our TodoDao from our database
val userDao = TodoDatabase.getDatabase(application).todoDao()
repository = TodoRepository(userDao)
}
val searchQuery = MutableStateFlow("")
val sortOrder = MutableStateFlow(SortOrder.BY_DATE) // adding BY_DATE to make the
lists sorted by date as default
val hideCompleted = MutableStateFlow(false)
/**
* The combine function here is a an object in the flow library that is used too
combine the most recent values of a flow, so if one value changes it will
* automatically return the latest values of the other flows. This is done so that the three flows will work in harmony.
*/
private val tasksFlow = combine(
searchQuery,
sortOrder,
hideCompleted
) { query, sortOrder, hideCompleted -> // LAMBDA
Triple(query, sortOrder, hideCompleted)
// flatMapLatest gets triggered when any of this flows changes and then passes it to the query to be executed.
}.flatMapLatest { (query, sortOrder, hideCompleted) ->
userDao.getAllTasks(query, sortOrder, hideCompleted)
}
val tasks = tasksFlow.asLiveData()
// All functions using coroutines objects indicates that whatever is in it should run in a background thread
fun addTodo(todo : Todo) {
viewModelScope.launch(Dispatchers.IO) {
repository.addTodo(todo)
}
}
fun deleteSelectedTasks(idList: Long) {
viewModelScope.launch(Dispatchers.IO) {
repository.delSelectedTasks(idList)
}
}
fun deleteAllTasks() {
viewModelScope.launch(Dispatchers.IO) {
repository.delAllTasks()
}
}
}
enum class SortOrder { BY_DATE, BY_NAME }
Then my Fragment
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bignerdranch.android.to_dolist.databinding.FragmentListBinding
import com.bignerdranch.android.to_dolist.R
import com.bignerdranch.android.to_dolist.data.SortOrder
import com.bignerdranch.android.to_dolist.data.TodoViewModel
import com.bignerdranch.android.to_dolist.model.Todo
import com.bignerdranch.android.to_dolist.utils.onQueryTextChanged
private const val TAG = "ListFragment"
class ListFragment : Fragment() {
private var _binding : FragmentListBinding? = null
private val binding get() = _binding!!
lateinit var mTodoViewModel: TodoViewModel
private lateinit var recyclerView: RecyclerView
private val adapter = ListAdapter() // getting reference to our ListAdapter
private var todosList = emptyList<Todo>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment with ViewBinding style
_binding = FragmentListBinding.inflate(inflater, container, false)
// this tells our activity/fragment that we have a menu_item it should respond to it.
setHasOptionsMenu(true)
recyclerView = binding.recyclerViewTodo
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
/**
* updates our recyclerView with the new "observed" changes in our database through our adapter
*/
// TodoViewModel
mTodoViewModel = ViewModelProvider(this)[TodoViewModel::class.java]
mTodoViewModel.tasks.observe(viewLifecycleOwner) { todos ->
adapter.setData(todos)
todosList = todos
}
// Add Task Button
binding.fbAdd.setOnClickListener {
findNavController().navigate(R.id.action_listFragment_to_addFragment)
}
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_list, menu)
val search = menu.findItem(R.id.todo_search)
val searchView = search.actionView as SearchView
searchView.onQueryTextChanged { querySearch ->
mTodoViewModel.searchQuery.value = querySearch
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when(item.itemId) {
R.id.sort_by_name -> {
mTodoViewModel.sortOrder.value = SortOrder.BY_NAME
true
}
R.id.sort_by_date -> {
mTodoViewModel.sortOrder.value = SortOrder.BY_DATE
true
}
R.id.todo_hide_completed -> {
item.isChecked = !item.isChecked
mTodoViewModel.hideCompleted.value = item.isChecked
true
}
R.id.del_selected_tasks -> {
deleteSelectedUsers()
true
}
R.id.del_all_tasks -> {
deleteAllTasks()
true
}
else -> super.onOptionsItemSelected(item)
}
}
// function to delete all of our Tasks
private fun deleteAllTasks() {
val builder = AlertDialog.Builder(requireContext())
builder.setPositiveButton("Yes") {_,_->
mTodoViewModel.deleteAllTasks()
Toast.makeText(requireContext(), "All tasks have been successfully deleted!", Toast.LENGTH_LONG).show()
}
builder.setNegativeButton("No") {_,_-> }
builder.setTitle("Confirm Deletion")
builder.setMessage("Are you sure you want to delete all Tasks?")
builder.create().show()
}
// function to delete only selected Tasks
#SuppressLint("NotifyDataSetChanged")
private fun deleteSelectedUsers() {
val builder = AlertDialog.Builder(requireContext())
// Our todos that have been marked completed by the checkBox
val finishedTodos = todosList.filter { it.todoCheckBox }
builder.setPositiveButton("Yes") {_,_->
finishedTodos.forEach { todos ->
mTodoViewModel.deleteSelectedTasks(todos.id.toLong())
}
Toast.makeText(requireContext(), "Selected tasks successfully deleted!", Toast.LENGTH_LONG).show()
}
builder.setNegativeButton("No") {_,_-> }
builder.setTitle("Confirm Deletion")
builder.setMessage("Are you sure you want to delete only selected Tasks?")
builder.create().show()
Log.i(TAG , "Our todos list size is ${finishedTodos.size}")
}
// We want to leave no trace of our Binding class Reference to avoid memory leaks
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
I was able to find a solution. It turns out there was no logic to actually change the boolean value of the todoCheckBox(was changed to completed), it was just adding a strikeThrough. So I followed a better method to Implement the strikeThrough and refactored some of the code. So here's my code.
My Adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.ListAdapter
import com.bignerdranch.android.to_dolist.databinding.CustomRowBinding
import com.bignerdranch.android.to_dolist.fragments.add.SIMPLE_DATE_FORMAT
import com.bignerdranch.android.to_dolist.fragments.add.SIMPLE_TIME_FORMAT
import com.bignerdranch.android.to_dolist.model.Todo
import java.text.SimpleDateFormat
import java.util.*
class TodoAdapter(private val listener : OnItemClickListener):
ListAdapter<Todo, TodoAdapter.TodoViewHolder>(DiffCallBack) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
// this can be done in an inline variable and I may experiment on it later.
val binding = CustomRowBinding.inflate(LayoutInflater.from(parent.context),
parent,
false
)
return TodoViewHolder(binding)
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
inner class TodoViewHolder(private val binding : CustomRowBinding) : RecyclerView.ViewHolder(binding.root) {
/** Calling onClickListeners for each _Todo and the associated checkBox. **/
init {
binding.apply {
root.setOnClickListener {
val position = adapterPosition // this represents the position of any item in the root layout
// NO_POSITION means that an item is invalid and out of this list, so this is a safe check because-
// we don't want to call a listener on an invalid item
if (position != RecyclerView.NO_POSITION) {
val todo = getItem(position)
listener.onItemClick(todo)
}
}
cbTask.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val todo = getItem(position)
listener.onCheckBoxClick(todo, cbTask.isChecked)
}
}
}
}
fun bind(todo : Todo) {
val dateLocales = SimpleDateFormat(SIMPLE_DATE_FORMAT, Locale.getDefault())
val timeLocales = SimpleDateFormat(SIMPLE_TIME_FORMAT, Locale.getDefault())
binding.apply {
tvTaskTitle.text = todo.title
tvTaskDate.text = dateLocales.format(todo.date)
tvTaskTime.text = timeLocales.format(todo.time)
cbTask.isChecked = todo.completed
tvTaskTitle.paint.isStrikeThruText = todo.completed
}
}
}
interface OnItemClickListener {
fun onItemClick(todo : Todo)
fun onCheckBoxClick(todo: Todo, isChecked: Boolean)
}
// This piece of code checks between our old and changed and lists and updates the recyclerView with the latest list.
// This also stops the recyclerView from redrawing itself after the position of an item has been changed. It even provides a nice animation.
object DiffCallBack : DiffUtil.ItemCallback<Todo>() {
override fun areItemsTheSame(oldItem: Todo, newItem: Todo) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Todo, newItem: Todo) =
oldItem == newItem
}
}
Fragment
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bignerdranch.android.to_dolist.databinding.FragmentListBinding
import com.bignerdranch.android.to_dolist.R
import com.bignerdranch.android.to_dolist.viewmodel.SortOrder
import com.bignerdranch.android.to_dolist.viewmodel.TodoViewModel
import com.bignerdranch.android.to_dolist.model.Todo
import com.bignerdranch.android.to_dolist.utils.onQueryTextChanged
private const val TAG = "ListFragment"
class ListFragment : Fragment(), TodoAdapter.OnItemClickListener {
private var _binding : FragmentListBinding? = null
private val binding get() = _binding!!
private lateinit var mTodoViewModel: TodoViewModel
private lateinit var recyclerView: RecyclerView
private val adapter = TodoAdapter(this)
private var todosList = emptyList<Todo>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment with ViewBinding style
_binding = FragmentListBinding.inflate(inflater, container, false)
// this tells our activity/fragment that we have a menu_item it should respond to it.
setHasOptionsMenu(true)
recyclerView = binding.recyclerViewTodo
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.setHasFixedSize(true)
/**
* updates our recyclerView with the new "observed" changes in our database through our adapter
*/
// TodoViewModel
mTodoViewModel = ViewModelProvider(this)[TodoViewModel::class.java]
mTodoViewModel.tasks.observe(viewLifecycleOwner) { todos ->
adapter.submitList(todos)
todosList = todos
}
// Add Task Button
binding.fbAdd.setOnClickListener {
findNavController().navigate(R.id.action_listFragment_to_addFragment)
}
return binding.root
}
override fun onItemClick(todo: Todo) {
mTodoViewModel.onTaskSelected(todo)
}
override fun onCheckBoxClick(todo: Todo, isChecked: Boolean) {
mTodoViewModel.onTaskCheckedChanged(todo, isChecked)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_list, menu)
val search = menu.findItem(R.id.todo_search)
val searchView = search.actionView as SearchView
searchView.onQueryTextChanged { querySearch ->
mTodoViewModel.searchQuery.value = querySearch
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when(item.itemId) {
R.id.sort_by_name -> {
mTodoViewModel.sortOrder.value = SortOrder.BY_NAME
true
}
R.id.sort_by_date -> {
mTodoViewModel.sortOrder.value = SortOrder.BY_DATE
true
}
R.id.action_hide_completed_tasks -> {
item.isChecked = !item.isChecked
mTodoViewModel.hideCompleted.value = item.isChecked
true
}
R.id.del_selected_tasks -> {
deleteSelectedUsers()
true
}
R.id.del_all_tasks -> {
deleteAllTasks()
true
}
else -> super.onOptionsItemSelected(item)
}
}
// function to delete all of our Tasks
private fun deleteAllTasks() {
val builder = AlertDialog.Builder(requireContext())
builder.setPositiveButton("Yes") {_,_->
mTodoViewModel.deleteAllTasks()
Toast.makeText(requireContext(), "All tasks have been successfully deleted!", Toast.LENGTH_LONG).show()
}
builder.setNegativeButton("No") {_,_-> }
builder.setTitle("Confirm Deletion")
builder.setMessage("Are you sure you want to delete all Tasks?")
builder.create().show()
}
// function to delete only selected Tasks
#SuppressLint("NotifyDataSetChanged")
private fun deleteSelectedUsers() {
val builder = AlertDialog.Builder(requireContext())
// Our todos that have been marked completed by the checkBox
val finishedTodos = todosList.filter { it.completed }
builder.setPositiveButton("Yes") {_,_->
finishedTodos.forEach { todos ->
mTodoViewModel.deleteSelectedTasks(todos.id.toLong())
}
Toast.makeText(requireContext(), "Selected tasks successfully deleted!", Toast.LENGTH_LONG).show()
}
builder.setNegativeButton("No") {_,_-> }
builder.setTitle("Confirm Deletion")
builder.setMessage("Are you sure you want to delete only selected Tasks?")
builder.create().show()
Log.i(TAG , "Our todos list size is ${finishedTodos.size}")
}
// We want to leave no trace of our Binding class Reference to avoid memory leaks
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
And then just add both functions in the ViewModel
fun onTaskSelected(task : Todo) {
TODO()
}
fun onTaskCheckedChanged(todo : Todo, isChecked : Boolean) {
viewModelScope.launch {
repository.updateTask(todo.copy(completed = isChecked))
}
}

How to use service to set countDown timer in recycler view using Paging library and avoid timer flickering?

I hope you're doing great. Please, I need help for this.I'm currently working on an app that has two main functionalities : schedule tasks and take note. For task sheduling part, I want to add countDown timer in the recycler view so that each scheduled task of the recycler view can has its own timer. The timer takes in account the the date the task is created and the due date and counts till the end. The code I used triggers the timer for each item as expected but not only the timer flickers but it also restarts when I close the app and open it. What I want is to keep the timer running in background even if the user closes the app. So i'm searching solutions for two things : Timer flickering and lauching the countDown timer in background using service. I tried to create an object for the CountDown timer separately, not in the Adapter class like I did in the code below but I didn't manage to set it in the Adapter and display the Timer in the Text view that should contains the timer.
As I'm using Paging library to display the tasks list in a recycler view, is there any way to avoid Timer flickering in one hand and use service to launch the countDown timer in background in other hand? Please, I really need help for that. If you need any other information, let me so that I provide them.
Here is my code in adapter class
package eg.esperantgada.dailytodo.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.os.CountDownTimer
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import eg.esperantgada.dailytodo.R
import eg.esperantgada.dailytodo.databinding.TodoItemBinding
import eg.esperantgada.dailytodo.model.Todo
import java.text.SimpleDateFormat
import java.util.concurrent.TimeUnit
const val TAG = "TodoAdapter"
class TodoAdapter(
private val context: Context,
private val listener: OnItemClickedListener,
) : PagingDataAdapter<Todo, TodoAdapter.TodoViewHolder>(DiffCallback) {
inner class TodoViewHolder(private val binding: TodoItemBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.apply {
root.setOnClickListener {
//Gets the position of the View holder
val position = absoluteAdapterPosition
if (position != RecyclerView.NO_POSITION) {
val todo = getItem(position)
if (todo != null) {
listener.onItemClicked(todo)
}
}
}
checkbox.setOnClickListener {
val position = absoluteAdapterPosition
if (position != RecyclerView.NO_POSITION) {
val todo = getItem(position)
if (todo != null) {
listener.onCheckBoxClicked(todo, checkbox.isChecked)
}
}
}
}
}
fun bind(todo: Todo) {
#SuppressLint("SimpleDateFormat")
val dateTimeFormat = SimpleDateFormat("dd/MM/yyyy hh:mm a")
var todoCreatedDateAndTime = dateTimeFormat.parse(todo.createdAt)
var todoDueDateAndTime = dateTimeFormat.parse("${todo.date} ${todo.time}")
val createdDateAndTime = todoCreatedDateAndTime?.time
val dueDateAndTime = todoDueDateAndTime?.time
val countTimeLength = dueDateAndTime?.minus(createdDateAndTime!!)
val countDownTimer = object : CountDownTimer(countTimeLength!!, 1000) {
override fun onTick(p0: Long) {
val millisecond: Long = p0
val dayHourMinuteSecond = String.format(
"%02d Days :%02d Hours :%02d Min :%02d Sec",
TimeUnit.MILLISECONDS.toDays(millisecond),
(TimeUnit.MILLISECONDS.toHours(millisecond) - TimeUnit.DAYS.toHours(
TimeUnit.MILLISECONDS.toDays(millisecond))),
(TimeUnit.MILLISECONDS.toMinutes(millisecond) - TimeUnit.HOURS.toMinutes(
TimeUnit.MILLISECONDS.toHours(millisecond))),
(TimeUnit.MILLISECONDS.toSeconds(millisecond) - TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(millisecond)))
)
binding.todoTimer.text = dayHourMinuteSecond
Log.d(TAG, "COUNTDOWNTIMER IN ADAPTER: $dayHourMinuteSecond")
}
override fun onFinish() {
binding.todoTimer.text = "00:00:00:00"
}
}
countDownTimer.start()
binding.apply {
checkbox.isChecked = todo.completed
todoTextView.text = todo.name
todoTextView.paint.isStrikeThruText = todo.completed
priorityImageView.isVisible = todo.important
durationTextView.text = todo.duration
}
binding.createdDateAndTime.setText(todo.createdAt)
binding.dueDateAndTime.text =
context.getString(R.string.date_and_time, todo.date, todo.time)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
val binding = TodoItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return TodoViewHolder(binding)
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val currentTodo = getItem(position)
if (currentTodo != null) {
holder.bind(currentTodo)
}
}
interface OnItemClickedListener {
fun onItemClicked(todo: Todo)
fun onCheckBoxClicked(todo: Todo, isChecked: Boolean)
fun startTodoCountDownTimer(): Boolean
}
companion object {
val DiffCallback = object : DiffUtil.ItemCallback<Todo>() {
override fun areItemsTheSame(oldItem: Todo, newItem: Todo): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Todo, newItem: Todo): Boolean {
return oldItem == newItem
}
}
}
}
Thanks in advance!

Move an item in a RecyclerView to a specific position

I am using a RecyclerView to populate data that are fetched in my Room database. When data is fetched or removed from my Room database, the RecyclerView is updated automatically. I have implemented a swipe to delete and undo in my RecyclerView. When i swipe, i delete the item in my Room Database and the RecyclerView gets updated automatically. However, when i click the "Undo" button, i insert the item again in the Room database and the item is shown in the Recyclerview. The problem here is when i restore the item, it gets restored in the last position. I want it to be restore in the position it was deleted, not last.
My code is as below:
fragment.xml
<androidx.recyclerview.widget.RecyclerView
android:id="#id/RecyclerView_fromFragmentToDo_Main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/Margin_Views_Small"
android:layout_marginBottom="?attr/actionBarSize"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
adapter.kt
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.th3pl4gu3.lifestyle.core.lifestyle.ToDo
import com.th3pl4gu3.lifestyle.databinding.CustomRecyclerviewListTodoBinding
class ToDoAdapter : ListAdapter<ToDo, ToDoAdapter.ViewHolder>(ToDoDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val toDo = getItem(position)
holder.bind(toDo)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
class ViewHolder private constructor(val binding: CustomRecyclerviewListTodoBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(toDo: ToDo) {
binding.myToDo = toDo
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CustomRecyclerviewListTodoBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
class ToDoDiffCallback: DiffUtil.ItemCallback<ToDo>() {
override fun areItemsTheSame(oldItem: ToDo, newItem: ToDo): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ToDo, newItem: ToDo): Boolean {
return oldItem == newItem
}
}
viewmodel.kt
import android.app.Application
import androidx.lifecycle.*
import com.th3pl4gu3.lifestyle.core.lifestyle.ToDo
import com.th3pl4gu3.lifestyle.core.operations.Filter
import com.th3pl4gu3.lifestyle.core.operations.ToDoOperations
import com.th3pl4gu3.lifestyle.database.LifestyleDatabase
import com.th3pl4gu3.lifestyle.ui.enums.ToggleButtonStates
import kotlinx.coroutines.*
class ToDoViewModel(
val database: LifestyleDatabase,
application: Application) : AndroidViewModel(application) {
private var _viewModelJob = Job()
private val _uiScope = CoroutineScope(Dispatchers.Main + _viewModelJob)
//Current state of the toggle button (Current button checked)
var currentToggleButtonState = ToggleButtonStates.BUTTON_ACTIVE
//Fetch all to dos from database
private var _toDos = ToDoOperations.getAllOffline(database)
val toDosMediatorLiveData = MediatorLiveData<List<ToDo>>()
init {
//Update the list of the recyclerview on INIT
updateList(currentToggleButtonState)
}
/**
* Public functions that are accessible from the outside
**/
fun updateList(toggleButton: ToggleButtonStates) {
toDosMediatorLiveData.removeSource(_toDos)
when(toggleButton){
ToggleButtonStates.BUTTON_ALL ->{
currentToggleButtonState = ToggleButtonStates.BUTTON_ALL
toDosMediatorLiveData.addSource(_toDos){
toDosMediatorLiveData.value = it
}
}
ToggleButtonStates.BUTTON_ACTIVE ->{
currentToggleButtonState = ToggleButtonStates.BUTTON_ACTIVE
toDosMediatorLiveData.addSource(_toDos){
toDosMediatorLiveData.value = Filter<ToDo>(it).getActive()
}
}
ToggleButtonStates.BUTTON_COMPLETE ->{
currentToggleButtonState = ToggleButtonStates.BUTTON_COMPLETE
toDosMediatorLiveData.addSource(_toDos){
toDosMediatorLiveData.value = Filter<ToDo>(it).getCompleted()
}
}
}
}
fun insertItem(toDo: ToDo) {
_uiScope.launch {
insert(toDo)
}
}
fun markAsDeleted(toDo: ToDo) {
_uiScope.launch {
remove(toDo)
}
}
fun markItem(toDo: ToDo){
if(toDo.dateCompleted == null){
markAsCompleted(toDo)
}else{
markAsIncomplete(toDo)
}
}
/**
* Private functions for internal use ONLY
**/
private fun markAsCompleted(newToDo: ToDo) {
_uiScope.launch {
newToDo.markAsComplete()
update(newToDo)
}
}
private fun markAsIncomplete(newToDo: ToDo) {
_uiScope.launch {
newToDo.markAsIncomplete()
update(newToDo)
}
}
private suspend fun insert(toDo: ToDo) {
withContext(Dispatchers.IO) {
toDo.add(database)
}
}
private suspend fun remove(toDo: ToDo) {
withContext(Dispatchers.IO) {
toDo.delete(database)
}
}
private suspend fun update(newToDo: ToDo) {
withContext(Dispatchers.IO) {
newToDo.update(database)
}
}
/**
* Overridden functions
**/
override fun onCleared() {
super.onCleared()
//Clear the view model job when user leave
_viewModelJob.cancel()
}
}
fragment.kt
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.th3pl4gu3.lifestyle.R
import com.th3pl4gu3.lifestyle.ui.Utils.toast
import com.th3pl4gu3.lifestyle.database.LifestyleDatabase
import com.th3pl4gu3.lifestyle.databinding.FragmentToDoBinding
import com.th3pl4gu3.lifestyle.ui.Utils.action
import com.th3pl4gu3.lifestyle.ui.Utils.snackBar
import com.th3pl4gu3.lifestyle.ui.Utils.snackBarWithAction
import com.th3pl4gu3.lifestyle.ui.enums.ToggleButtonStates
import java.util.*
class FragmentToDo : Fragment() {
private lateinit var mBinding: FragmentToDoBinding
private lateinit var mToDoViewModel: ToDoViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
//Inflate the layout for this fragment
mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_to_do, container, false)
//Configures the screen views (Eg. Title, appearance of top bar etc...)
configureScreenAppearance()
//Get the activity's application
val application = requireNotNull(this.activity).application
//Fetch the database
val dataSource = LifestyleDatabase.getInstance(application)
//Instantiate the view model factory
val viewModelFactory = ToDoViewModelFactory(dataSource, application)
//Instantiate the view model of this fragment
mToDoViewModel = ViewModelProviders.of(this, viewModelFactory).get(ToDoViewModel::class.java)
//Bind view model
mBinding.toDoViewModel = mToDoViewModel
//Instantiate the lifecycle owner
mBinding.lifecycleOwner = this
//RecyclerView's configuration
val adapter = ToDoAdapter()
mBinding.RecyclerViewFromFragmentToDoMain.adapter = adapter
mToDoViewModel.toDosMediatorLiveData.observe(viewLifecycleOwner, Observer {
it.let { x ->
//Update the UI and determine whether recyclerview should be visible or not
updateUI(x.isNotEmpty())
adapter.submitList(x)
}
})
//Swipe configurations
val swipeHandler = object : ToDoSwipeToCallback(requireContext()) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val swipedToDo = (mBinding.RecyclerViewFromFragmentToDoMain.adapter as ToDoAdapter).currentList[viewHolder.adapterPosition]
val fab = requireActivity().findViewById<FloatingActionButton>(R.id.FAB_fromHomeActivity_BottomAppBarAttached)
val position = viewHolder.adapterPosition
when(direction){
ItemTouchHelper.LEFT -> {
mToDoViewModel.markItem(swipedToDo)
}
ItemTouchHelper.RIGHT -> {
mToDoViewModel.markAsDeleted(swipedToDo)
//Show Snackbar with 'Undo' action
requireActivity().findViewById<View>(android.R.id.content).snackBarWithAction(getString(R.string.Message_Exception_fromFragmentLifeStyleItems_ErrorWhileSwiping, swipedToDo.title), anchorView = fab){
action("Undo"){
mToDoViewModel.insertItem(swipedToDo)
//Restore Item
}
}
}
else ->{
requireContext().toast(getString(R.string.Message_Exception_fromFragmentLifeStyleItems_ErrorWhileSwiping))
}
}
}
}
val itemTouchHelper = ItemTouchHelper(swipeHandler)
itemTouchHelper.attachToRecyclerView(mBinding.RecyclerViewFromFragmentToDoMain)
return mBinding.root
}
/**
* Private functions for internal use ONLY
**/
private fun updateUI(recyclerViewVisible: Boolean){
if(recyclerViewVisible){
mBinding.RecyclerViewFromFragmentToDoMain.visibility = View.VISIBLE
mBinding.EmptyViewForRecyclerView.visibility = View.GONE
}else{
if(mToDoViewModel.currentToggleButtonState == ToggleButtonStates.BUTTON_COMPLETE){
mBinding.TextViewFromFragmentToDoEmptyView.text = getString(R.string.TextView_fromToDoFragment_Message_EmptyList_Completed)
}else if(mToDoViewModel.currentToggleButtonState == ToggleButtonStates.BUTTON_ACTIVE){
mBinding.TextViewFromFragmentToDoEmptyView.text = getString(R.string.TextView_fromToDoFragment_Message_EmptyList_Active)
}
mBinding.RecyclerViewFromFragmentToDoMain.visibility = View.GONE
mBinding.EmptyViewForRecyclerView.visibility = View.VISIBLE
}
}
private fun configureScreenAppearance(){
//Set title of fragment
val screenTitle = requireActivity().findViewById<TextView>(R.id.TextView_fromHomeActivity_Screen_Title)
screenTitle.text = getString(R.string.TextView_fromFragmentInHomeActivity_ScreenTitle_ToDo)
//Show Top Bar
val topBar = requireActivity().findViewById<RelativeLayout>(R.id.RelativeLayout_fromHomeActivity_TopBar)
topBar.visibility = View.VISIBLE
//Show Fab
val fab = requireActivity().findViewById<FloatingActionButton>(R.id.FAB_fromHomeActivity_BottomAppBarAttached)
fab.show()
}
}
I need to restore the item here:
ItemTouchHelper.RIGHT -> {
mToDoViewModel.markAsDeleted(swipedToDo)
//Show Snackbar with 'Undo' action
requireActivity().findViewById<View>(android.R.id.content).snackBarWithAction(getString(R.string.Message_Exception_fromFragmentLifeStyleItems_ErrorWhileSwiping, swipedToDo.title), anchorView = fab){
action("Undo"){
mToDoViewModel.insertItem(swipedToDo)
//Restore Item
}
}
}
I've tried fetching the list and then do list.add(position, item) but it doesn't work. Can someone please help ? Thanks.
If list.add didn't work then I would suggest a different approach when you swipe to delete don't delete the item. Add another column isDeleted in your model and change room query to fetch all rows in which isDeleted is false.
select * from table where isDeleted = 0
Then when swipe to delete update the row to set isDeleted true and when you undo just set isDeleted to false. You can delete the row at later point when you're sure that there'll be no undo required.
Update your insert function in viewmodel.kt file as follows
private suspend fun insert(toDo: ToDo) {
withContext(Dispatchers.IO) {
toDo.add(index, database)
}
}
Here index is an integer which indicates the position that you want to insert the new item. And notify recyclerview adapter if necessary as follows
//Show Snackbar with 'Undo' action
requireActivity().findViewById<View>(android.R.id.content).snackBarWithAction(getString(R.string.Message_Exception_fromFragmentLifeStyleItems_ErrorWhileSwiping, swipedToDo.title), anchorView = fab){
action("Undo"){
mToDoViewModel.insertItem(swipedToDo)
adapter.notifyItemInserted(index)
//Restore Item
}
}

Categories

Resources