Trying to get RecyclerView to use a diffferent dataset - android

I've been stuck trying to figure out how to update the list that my RecyclerView is showing.
What I'm trying to do is show a subset of a shown list when a spinner is changed. I have a collection of animals in my database and some have their pet attribute set as true and others have it set as false.
Using Room Database with repositories and viewModels, and what I've been trying to piece together is that it's good to have three different lists that I can tune into, so in m
Repository:
class AnimalRepository(private val animalDao: AnimalDao) {
val allAnimals: Flow<List<Animal>> = animalDao.getAnimalsByCategory()
val pets: Flow<List<Animal>> = animalDao.getAnimalsByPetStatus(true)
val nonPets: Flow<List<Animal>> = animalDao.getAnimalsByPetStatus(false)
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun insert(animal: Animal) {
animalDao.insert(animal)
}
#WorkerThread
suspend fun get(id: Int): Animal {
return animalDao.get(id)
}
#WorkerThread
suspend fun delete(id: Int) {
animalDao.delete(id)
}
}
ViewModel
class AnimalViewModel(private val repository: AnimalRepository) : ViewModel() {
var allAnimals: LiveData<List<Animal>> = repository.allAnimals.asLiveData()
val pets: LiveData<List<Animal>> = repository.pets.asLiveData()
val nonPets: LiveData<List<Animal>> = repository.nonPets.asLiveData()
var result: MutableLiveData<Animal> = MutableLiveData<Animal>()
var mode: VIEW_MODES = VIEW_MODES.BOTH
/*
* Launching a new coroutine to insert the data in a non-blocking way
* */
fun insert(animal: Animal) = viewModelScope.launch {
repository.insert(animal)
}
/*
* Launching a new coroutine to get the data in a non-blocking way
* */
fun get(id: Int) = viewModelScope.launch {
result.value = repository.get(id)
}
fun delete(id: Int) = viewModelScope.launch {
repository.delete(id)
}
}
class AnimalViewModelFactory(private val repository: AnimalRepository) : ViewModelProvider.Factory {
override fun <T: ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AnimalViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return AnimalViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
In my MainActivity I have it set up where I have an observer on these three lists and depending on which view mode is active (the spinner sets the view mode), that list is fed into the my RecyclerView's ListAdapter's submitList
animalViewModel.allAnimals.observe(this) { animals ->
if (viewMode == VIEW_MODES.BOTH) {
animals.let {
adapter.submitList(it)
// recyclerView.adapter = adapter
}
}
}
animalViewModel.pets.observe(this) { animals ->
if (viewMode == VIEW_MODES.PETS) {
animals.let {
adapter.submitList(it)
// recyclerView.adapter = adapter
}
}
}
animalViewModel.nonPets.observe(this) { animals ->
if (viewMode == VIEW_MODES.NON_PETS) {
animals.let {
adapter.submitList(it)
}
}
}
I am changing the mode with my spinner doing
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when (position) {
0 -> {
viewMode = VIEW_MODES.BOTH
}
1 -> {
viewMode = VIEW_MODES.PETS
}
2 -> {
viewMode = VIEW_MODES.NON_PETS
}
}
adapter.notifyDataSetChanged()
}
This works fine if add or remove an animal after changing the view mode since the observers fire and the correct one is allowed to populate the adapter, but the notifyDataSetChanged() isn't doing anything and I've been stuck on getting the adapter to update without having to add or remove from the lists
I also tried resetting the adapter in the observer but that didn't do anything either
I am extremely new to kotlin and android programming, and I'm sure that I'm going about this the wrong way, but is there a way force a list refresh?
Update:
I think I may have found a found a solution but I worry that it's hacky. In my ViewModel I am replacing the contents of my allAnimals with the filtered lists
fun showBoth() {
allAnimals = repository.allAnimals.asLiveData()
}
fun showPets() {
allAnimals = repository.pets.asLiveData()
}
fun showNonPets() {
allAnimals = repository.nonPets.asLiveData()
}
and then in my main activity I changed my logic on when handling the spinner change to tell the view model to do its thing and then to remove the observer and slap it back on
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when (position) {
0 -> {
animalViewModel.showBoth()
}
1 -> {
animalViewModel.showPets()
}
2 -> {
animalViewModel.showNonPets()
}
}
refreshObserver()
}
private fun refreshObserver() {
animalViewModel.allAnimals.removeObservers(this)
animalViewModel.allAnimals.observe(this) { animals ->
animals.let {
adapter.submitList(it)
}
}
}
this seems to work to get the recycler view to update, but is it hacky?

As far as I can see it makes perfect sense that notifyDataSetChanged isn't doing anything, you don't submit any new data before that call. However I think what you're trying to do is to get the adapter to react to a change in viewMode.
If this is the case, I would recommend also having your viewMode as a LiveData object and then expose a single list for your adapter to observe, which changes depending on the viewMode selected.
The Transformations.switchMap(LiveData<X>, Function<X, LiveData<Y>>) method (or its equivalent Kotlin extension function) would probably do most of the work for you here. In summary it maps the values of one LiveData to another. So in your example, you could map your viewMode to one of the allAnimals, pets and nonPets.
Here is a simple pseudocode overview for some clarity:
AnimalViewModel {
val allAnimals: LiveData<List<Animal>>
val pets: LiveData<List<Animal>>
val nonPets: LiveData<List<Animal>>
val modes: MutableLiveData<VIEW_MODES>
val listAnimals = modes.switchMap {
when (it) {
VIEW_MODES.BOTH -> allAnimals
...
}
}
}
fun onItemSelected {
viewModel.onModeChanged(position)
}
viewModel.listAnimals.observe {
adapter.submitList(it)
}

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.

LiveData with condition

I want to use livedata in an recyclerview. But I only want to observe Livedata with a certain ID. The data gets loaded, but it doesn't update.
So here is the function in m Dao:
#Query("SELECT * FROM zutaten_table NATURAL JOIN table_ref WHERE table_ref.rezeptid = :id")
fun getZutatenforRezept(id:Int): LiveData<List<ZutatenData>>
I use a Viewmodel and a repository like this:
class LiveDataZutatenRepository(private val rezeptDao: AllDao, rezeptID: Int){
val Dao = rezeptDao
val allZutaten = Dao.getZutatenforRezept(rezeptID)
}
class SpecialZutatViewmodel(application: Application): AndroidViewModel(application){
private val repository: JustGetSpecialTypesRepository
private lateinit var repositoryLiveData: LiveDataZutatenRepository
lateinit var ZutatenforRezept : LiveData<List<ZutatenData>>
val Dao : AllDao
init {
Dao = EssenRoomDatabase.getDatabase(application, viewModelScope).allDao()
repository = JustGetSpecialTypesRepository(Dao)
}
suspend fun getRezeptWithZutat(id: Int):RezeptWithZutat{
return repository.getRezeptWithZutatFromID(id)
}
suspend fun getMengen(rezid: Int): List<RefZutatRezept>{
return repository.getMengen(rezid)
}
fun setLiveData(rezeptid: Int){
repositoryLiveData = LiveDataZutatenRepository(Dao, rezeptid )
ZutatenforRezept = repositoryLiveData.allZutaten
}
}
an in my view I use an observer to get the livedata:
val zutatViewmodel = ViewModelProvider(this).get(SpecialZutatViewmodel::class.java)
lifecycleScope.launch {
zutatViewmodel.setLiveData(rezeptid)
}
zutatViewmodel.ZutatenforRezept.observe(this, Observer { zutaten ->
zutaten?.let { adapterzut.setZutaten(it) }
})
Viewholder function:
override fun onBindViewHolder(holder: ZutatenViewHolder, position: Int) {
val current = zutaten[position]
holder.rezepteItemView.text = current.zutname
if(current.bild>=0) {
holder.rezeptePicView.setImageResource(current.bild)
holder.rezeptePicView.drawable.isFilterBitmap = false
}
}
unfortenatly the list doesn't update when the database is changed, but is loaded correctly the first time. What am I doing wrong?
it seems you are using room. So, instead of passing the entire list to the adapter, you can pass a list of all the ids in the database. Then, in the onBindViewholder, you can call the rest of the elements by using the id of the element. the code sample below might give you a better idea -
override fun onBindViewHolder(holder: PassViewHolder, position: Int) {
viewModel.getById(getItem(position)).asLiveData().observe(lifecycleOwner) {
try {
holder.bind(it)
}catch (e:Exception){
Log.e(TAG,"PassData passed = null")
e.printStackTrace()
}
}
}
I had the same problem where the views weren't getting updated but the changes where still being recorded. this method fixed it all.
the below piece of code returns the elements linked to the id as a flow.
viewModel.getById(getItem(position))
And then you covert it to live data and add an observer.
if you want, you can have a look at the project where I implemented this

MutableLiveData won't trigger loadAfter to fetch from Android ROM using PagedList

I have 70 itens stored on my ROM and I would like to fetch a paged amount of 15. I read many posts so far with related issues, however none of them were useful for me.
Some possible causes for loadAfter not being triggered:
Solution 1 : call getItem inside onBindViewHolder
Solution 2 : call submitList to PagedListAdapter
Solution 3 : replace ListAdapter with PagedListAdapter
I assume DataBinding is fine since everything works without trying to paging.
I'm mocking my data source to understand what's happening. Some functions are suspended 'cause they should have data coming from ROM which requires it. My code state be like:
ADAPTER
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
getItem(position).let { wkda ->
with(holder) {
wkda?.apply { bind(createOnClickListener(this)) }
}
}
}
FRAGMENT
vm.manufacturers.observe(viewLifecycleOwner) { manufacturers ->
adapter.submitList(manufacturers)
}
VIEWMODEL
var manufacturers: MutableLiveData<PagedList<WKDA>> = MutableLiveData()
init {
viewModelScope.launch {
repository.getManufacturers(manufacturers)
}
}
REPOSITORY
suspend fun getManufacturers(manufacturers: MutableLiveData<PagedList<WKDA>>) {
withContext(Dispatchers.IO) {
manufacturers.postValue(ManufacturerPagedList.
getInstance().
fetchPage())
}
}
MANUFACTURER PAGED LIST
private val executor = ManufacturerExecutor()
private val paginationConfig: PagedList.Config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setPrefetchDistance(FETCH_DISTANCE)
.setEnablePlaceholders(false)
.build()
companion object {
#Volatile
private var instance: ManufacturerPagedList? = null
fun getInstance() = instance ?: synchronized(this) {
ManufacturerPagedList().also {
instance = it
}
}
}
fun fetchPage(): PagedList<WKDA> = PagedList.Builder<Int, WKDA>(
MockDataSource(),
paginationConfig)
.setInitialKey(INITIAL_KEY)
.setFetchExecutor(executor)
.setNotifyExecutor(executor)
.build()
}
DATASOURCE
class MockDataSource : PageKeyedDataSource<Int, WKDA>() {
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, WKDA>) {
callback.onResult(List(20) { generatePost(params.requestedLoadSize) }.toList(), -1, 1)
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, WKDA>) {
callback.onResult(List(20) { generatePost(params.key) }.toList(), params.key + 1)
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, WKDA>) {
callback.onResult(List(20) { generatePost(params.key) }.toList(), params.key - 1)
}
private fun generatePost(key: Int): WKDA {
return WKDA("name", "author $key")
}
}
CONSTANTS
const val INITIAL_KEY: Int = 0
const val PAGE_SIZE: Int = 15
const val FETCH_DISTANCE: Int = 1
What am I missing here?
After check: loadAfter was called properly. The problem was model itself:
wkda.id had always the same "name" value
DiffCallback compared old list of objects with the new one and didn't see differences, so the item "duplicates" weren't added to the adapter

What is the proper way to manipulate mutual content among RecyclerViews with Android Architecture Components

I've got an Activity with 2 Fragments sharing mutual items. When I bookmark an item from one Fragment, I would like to see it bookmarked on the other Fragment.
When I #Delete an item from one of the fragments, it is also deleted in the other fragment, however, when I #Update the item, it does not even update in current fragment.
Both fragments are using same ViewModel through their common parent Activity.
What is the proper way to achieve this would be?
Dao
#Dao
interface RssDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertArticles(rssItem: List<Article>)
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun updateArticle(article: Article)
#Delete
fun deleteArticle(article: Article)
Repository
private fun getRssFeed(url: String): LiveData<Resource<List<Article>>> {
return object : NetworkBoundResource<List<Article>, ArrayList<Article>>(appContext) {
override fun loadFromDb(): LiveData<List<Article>> = rssDao.getAllRss()
override fun createCall(): LiveData<ApiResponse<ArrayList<Article>>> =
rssFeedProvider.fetch(url)
override fun saveCallResult(item: ArrayList<Article>) {
rssDao.insertArticles(item)
}
override fun shouldFetch(data: List<Article>?): Boolean {
...
}
override fun onFetchFailed() {
DebugUtils.log("onFetchFailed")
}
override fun processResponse(response: ApiResponse<ArrayList<Article>>):
ArrayList<Article>? = response.body
}.asLiveData()
}
fun updateArticle(article: Article) {
bg {
rssDao.updateArticle(article)
}
}
fun deleteArticle(article: Article) {
bg {
rssDao.deleteArticle(article)
}
}
ViewModel
private var rssResponses = rssRepo.getAllRss()
var rssUiModelData: LiveData<List<ArticleUiModel>> =
Transformations.switchMap(rssResponses, { response ->
handleResponseData(response)
})
private fun handleResponseData(response: Resource<List<Article>>): MutableLiveData<List<ArticleUiModel>> {
response.data?.let {
val sortedList = it.sortedWith(compareByDescending(Article::pubDateTimestamp))
result.postValue(sortedList.map { article ->
ArticleUiModel(article, newsClickCallback)
})
}
return result
}
/* when this function is called bookmark button onClick. The item is
updated in the database but the LiveData from rssRepo.getAllRss()
is not updating the changes. On the other hand, If
rssRepo.deleteArticle(article)
is called, it reacts to it and observer onChanged is called,
hence adapter removes the item. */
private fun toggleBookmark(article: Article) {
if (article.bookmarked == 1) {
article.bookmarked = 0
} else {
article.bookmarked = 1
}
rssRepo.updateArticle(article)
}
Fragment
...
viewModel = activity?.let { it ->
ViewModelProviders.of(it, factory)
.get(FeedViewModel::class.java)
viewModel?.rssUiModelData?.observe(this, Observer {
adapter.addUiModels(it as Collection<BaseUiModelAlias>)})
...
You should have an adapter for your Recyclerview and running the class function notifyDataSetChanged will refresh your recyclerview whether it's deleted or updated.
mRecyclerView.setAdapter(your_adapter);
your_adapter.notifyDataSetChanged();
Look into this site if you do not have set an adapter for each viewholder of recyclerview: https://www.androidhive.info/2016/01/android-working-with-recycler-view/

Categories

Resources