mirror of
https://github.com/wulkanowy/wulkanowy.git
synced 2025-01-31 15:18:20 +01:00
Fix race condition of showing empty view in timetable (#2486)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
This commit is contained in:
parent
dbc7587741
commit
ad5381ce34
@ -7,20 +7,20 @@ import android.view.ViewGroup
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.github.wulkanowy.R
|
import io.github.wulkanowy.R
|
||||||
import io.github.wulkanowy.data.db.entities.Timetable
|
import io.github.wulkanowy.data.db.entities.Timetable
|
||||||
import io.github.wulkanowy.databinding.ItemTimetableBinding
|
import io.github.wulkanowy.databinding.ItemTimetableBinding
|
||||||
import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding
|
import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding
|
||||||
import io.github.wulkanowy.databinding.ItemTimetableSmallBinding
|
import io.github.wulkanowy.databinding.ItemTimetableSmallBinding
|
||||||
|
import io.github.wulkanowy.utils.SyncListAdapter
|
||||||
import io.github.wulkanowy.utils.getPlural
|
import io.github.wulkanowy.utils.getPlural
|
||||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||||
import io.github.wulkanowy.utils.toFormattedString
|
import io.github.wulkanowy.utils.toFormattedString
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TimetableAdapter @Inject constructor() :
|
class TimetableAdapter @Inject constructor() :
|
||||||
ListAdapter<TimetableItem, RecyclerView.ViewHolder>(differ) {
|
SyncListAdapter<TimetableItem, RecyclerView.ViewHolder>(Differ) {
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
|
override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
|
||||||
|
|
||||||
@ -61,12 +61,10 @@ class TimetableAdapter @Inject constructor() :
|
|||||||
binding = holder.binding,
|
binding = holder.binding,
|
||||||
item = getItem(position) as TimetableItem.Small,
|
item = getItem(position) as TimetableItem.Small,
|
||||||
)
|
)
|
||||||
|
|
||||||
is NormalViewHolder -> bindNormalView(
|
is NormalViewHolder -> bindNormalView(
|
||||||
binding = holder.binding,
|
binding = holder.binding,
|
||||||
item = getItem(position) as TimetableItem.Normal,
|
item = getItem(position) as TimetableItem.Normal,
|
||||||
)
|
)
|
||||||
|
|
||||||
is EmptyViewHolder -> bindEmptyView(
|
is EmptyViewHolder -> bindEmptyView(
|
||||||
binding = holder.binding,
|
binding = holder.binding,
|
||||||
item = getItem(position) as TimetableItem.Empty,
|
item = getItem(position) as TimetableItem.Empty,
|
||||||
@ -307,8 +305,7 @@ class TimetableAdapter @Inject constructor() :
|
|||||||
private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) :
|
private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root)
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
companion object {
|
private object Differ : DiffUtil.ItemCallback<TimetableItem>() {
|
||||||
private val differ = object : DiffUtil.ItemCallback<TimetableItem>() {
|
|
||||||
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean =
|
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean =
|
||||||
when {
|
when {
|
||||||
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
|
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
|
||||||
@ -334,4 +331,3 @@ class TimetableAdapter @Inject constructor() :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -21,7 +21,11 @@ import io.github.wulkanowy.ui.modules.main.MainView
|
|||||||
import io.github.wulkanowy.ui.modules.timetable.additional.AdditionalLessonsFragment
|
import io.github.wulkanowy.ui.modules.timetable.additional.AdditionalLessonsFragment
|
||||||
import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment
|
import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment
|
||||||
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
||||||
import io.github.wulkanowy.utils.*
|
import io.github.wulkanowy.utils.dpToPx
|
||||||
|
import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear
|
||||||
|
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||||
|
import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear
|
||||||
|
import io.github.wulkanowy.utils.openMaterialDatePicker
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -104,8 +108,11 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateData(data: List<TimetableItem>) {
|
override fun updateData(data: List<TimetableItem>, isDayChanged: Boolean) {
|
||||||
timetableAdapter.submitList(data)
|
when {
|
||||||
|
isDayChanged -> timetableAdapter.recreate(data)
|
||||||
|
else -> timetableAdapter.submitList(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearData() {
|
override fun clearData() {
|
||||||
|
@ -81,7 +81,7 @@ class TimetablePresenter @Inject constructor(
|
|||||||
} else currentDate?.previousSchoolDay
|
} else currentDate?.previousSchoolDay
|
||||||
|
|
||||||
reloadView(date ?: return)
|
reloadView(date ?: return)
|
||||||
loadData()
|
loadData(isDayChanged = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNextDay() {
|
fun onNextDay() {
|
||||||
@ -90,7 +90,7 @@ class TimetablePresenter @Inject constructor(
|
|||||||
} else currentDate?.nextSchoolDay
|
} else currentDate?.nextSchoolDay
|
||||||
|
|
||||||
reloadView(date ?: return)
|
reloadView(date ?: return)
|
||||||
loadData()
|
loadData(isDayChanged = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPickDate() {
|
fun onPickDate() {
|
||||||
@ -104,7 +104,7 @@ class TimetablePresenter @Inject constructor(
|
|||||||
|
|
||||||
fun onSwipeRefresh() {
|
fun onSwipeRefresh() {
|
||||||
Timber.i("Force refreshing the timetable")
|
Timber.i("Force refreshing the timetable")
|
||||||
loadData(true)
|
loadData(forceRefresh = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRetry() {
|
fun onRetry() {
|
||||||
@ -112,7 +112,7 @@ class TimetablePresenter @Inject constructor(
|
|||||||
showErrorView(false)
|
showErrorView(false)
|
||||||
showProgress(true)
|
showProgress(true)
|
||||||
}
|
}
|
||||||
loadData(true)
|
loadData(forceRefresh = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDetailsClick() {
|
fun onDetailsClick() {
|
||||||
@ -145,7 +145,7 @@ class TimetablePresenter @Inject constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadData(forceRefresh: Boolean = false) {
|
private fun loadData(forceRefresh: Boolean = false, isDayChanged: Boolean = false) {
|
||||||
flatResourceFlow {
|
flatResourceFlow {
|
||||||
val student = studentRepository.getCurrentStudent()
|
val student = studentRepository.getCurrentStudent()
|
||||||
val semester = semesterRepository.getCurrentSemester(student)
|
val semester = semesterRepository.getCurrentSemester(student)
|
||||||
@ -169,9 +169,9 @@ class TimetablePresenter @Inject constructor(
|
|||||||
enableSwipe(true)
|
enableSwipe(true)
|
||||||
showProgress(false)
|
showProgress(false)
|
||||||
showErrorView(false)
|
showErrorView(false)
|
||||||
|
updateData(it.lessons, isDayChanged)
|
||||||
showContent(it.lessons.isNotEmpty())
|
showContent(it.lessons.isNotEmpty())
|
||||||
showEmpty(it.lessons.isEmpty())
|
showEmpty(it.lessons.isEmpty())
|
||||||
updateData(it.lessons)
|
|
||||||
setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content)
|
setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content)
|
||||||
reloadNavigation()
|
reloadNavigation()
|
||||||
}
|
}
|
||||||
@ -216,15 +216,14 @@ class TimetablePresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateData(lessons: List<Timetable>) {
|
private fun updateData(lessons: List<Timetable>, isDayChanged: Boolean) {
|
||||||
tickTimer?.cancel()
|
tickTimer?.cancel()
|
||||||
|
|
||||||
if (currentDate != now()) {
|
view?.updateData(createItems(lessons), isDayChanged)
|
||||||
view?.updateData(createItems(lessons))
|
if (currentDate == now()) {
|
||||||
} else {
|
tickTimer = timer(period = 2_000, initialDelay = 2_000) {
|
||||||
tickTimer = timer(period = 2_000) {
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
view?.updateData(createItems(lessons))
|
view?.updateData(createItems(lessons), isDayChanged)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ interface TimetableView : BaseView {
|
|||||||
|
|
||||||
fun initView()
|
fun initView()
|
||||||
|
|
||||||
fun updateData(data: List<TimetableItem>)
|
fun updateData(data: List<TimetableItem>, isDayChanged: Boolean)
|
||||||
|
|
||||||
fun updateNavigationDay(date: String)
|
fun updateNavigationDay(date: String)
|
||||||
|
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
package io.github.wulkanowy.utils
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom alternative to androidx.recyclerview.widget.ListAdapter. ListAdapter is asynchronous which
|
||||||
|
* caused data race problems in views when a Resource.Error arrived shortly after
|
||||||
|
* Resource.Intermediate/Success - occasionally in that case the user could see both the Resource's
|
||||||
|
* data and an error message one on top of the other. This is synchronized by design to avoid that
|
||||||
|
* problem, however it retains the quality of life improvements of the original.
|
||||||
|
*/
|
||||||
|
abstract class SyncListAdapter<T : Any, VH : RecyclerView.ViewHolder> private constructor(
|
||||||
|
private val updateStrategy: SyncListAdapter<T, VH>.(List<T>) -> Unit
|
||||||
|
) : RecyclerView.Adapter<VH>() {
|
||||||
|
|
||||||
|
constructor(differ: DiffUtil.ItemCallback<T>) : this({ newItems ->
|
||||||
|
val diffResult = DiffUtil.calculateDiff(toCallback(differ, items, newItems))
|
||||||
|
items = newItems
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
|
})
|
||||||
|
|
||||||
|
var items = emptyList<T>()
|
||||||
|
private set
|
||||||
|
|
||||||
|
final override fun getItemCount() = items.size
|
||||||
|
|
||||||
|
fun getItem(position: Int): T {
|
||||||
|
return items[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates all items, same as submitList, however also disables animations temporarily.
|
||||||
|
* This prevents a flashing effect on some views. Should be used in favor of submitList when
|
||||||
|
* all data is changed (e.g. the selected day changes in timetable causing all lessons to change).
|
||||||
|
*/
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun recreate(data: List<T>) {
|
||||||
|
items = data
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitList(data: List<T>) {
|
||||||
|
updateStrategy(data.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T : Any> toCallback(
|
||||||
|
itemCallback: DiffUtil.ItemCallback<T>,
|
||||||
|
old: List<T>,
|
||||||
|
new: List<T>,
|
||||||
|
) = object : DiffUtil.Callback() {
|
||||||
|
override fun getOldListSize() = old.size
|
||||||
|
|
||||||
|
override fun getNewListSize() = new.size
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
itemCallback.areItemsTheSame(old[oldItemPosition], new[newItemPosition])
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
itemCallback.areContentsTheSame(old[oldItemPosition], new[newItemPosition])
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
itemCallback.getChangePayload(old[oldItemPosition], new[newItemPosition])
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user