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()
}
}
}