Reputation: 31
I am facing following problem: Having this entity to show a list of channels with each its current and next program in a recyclerview (ListAdapter with Diffutil):
data class TvChannelsWithEpgData(
val tvChannels: TvChannels,
var epgDataList: List<EpgData>
)
In my viewmodel there is: val tvChannelsWithEpgList: MutableLiveData<List<TvChannelsWithEpgData>> = MutableLiveData()
that I observe in my TvChannelsFragment.
Additional i have the function loadTvChannelsWithEpgData() that fetches the list of channels, based on some specific properties, from the database (Room).
After receiving the requested list of channels, for every channel in that list a api-call is made, to get the latest epg-data for each channel.
At the beginning, so when the user opens a specific category to show its channels with epg I want that immediately the channellist is shown, also if the channels didn't get their epg-data yet. So the user hasn't to wait until everything was fetched, what - based on the quantity of channels - can last some time. For that at start I add the channels to tvChannelsWithEpgList and an emptyList() for the epgData.
Then when the epg-data for a channel is fetched and is not null,I save it in the EpgData entity and pass it to tvChannelsWithEpgList related to the channel I used for the api request. Then, as I already wrote, i observe the tvChannelsWithEpgList in my Fragment and pass the list to the adapter, using submitlist()
In the adapter i use fun bind() in the Viewholder to pass the data to the views and to calculate the currentProgram and the nextProgram from the List and to calculate the progress of the program, showed in a progressbar.
So until here everything is working more or less as it should. But now my problems start and as I understand it has to do with the way Recyclerview works.
Example: the user opens the TvChannelsFragment and the list of channels is appearing, logically with "No information" in the beginning, as no epg-data was fetched yet. But while in the background the data for some channels was fetched (checked with the App inspection) it isn't showed immediately for the visible items. Instead I have to scroll away and back, so that the bind() function is called again and the "new" epg-data is used. As I understand the bind() function for an item is only called once normally and then only again when it's "recreated" (scroll away and back). But isn't there a way to handle that somehow, so that the "new" epg-data is showing in real-time?
Can I use DiffUtil to handle the updating of visibile items? Or is there some simple method to show always the latest epg-data of an item (visible item), without having to scroll away? I know that i can use notifyDataSetChanged() after every channels epg-response, but in that case the user isn't able to use the clicklisteners normally until the epg-data of all channels was fetched. And reading in the internet, it's not a good way to do this.
Following my viewmodel and adapter code, and the code i use in the fragment to submit the list. If you need any additional informations, please feel free to ask.
VIEWMODEL:
val tvChannelsWithEpgList: MutableLiveData<List<TvChannelsWithEpgData>> = MutableLiveData()
fun loadTvChannelsWithEpgData(
name: String,
tvGenreId: String,
url: String,
password: String
) {
viewModelScope.launch {
val token = sessionManager.fetchAuthToken()
val timeZone = timeZoneManager.fetchTimeZone()
val accountId = accountData.id
val tvChannels = withContext(Dispatchers.IO) {
repository.getChannelsPerGenreAndAccount(accountId, tvGenreId)
}
val sortedChannels = tvChannels.sortedBy { it.number.toString().toInt() }
val tvChannelsWithEpg =
sortedChannels.map { TvChannelsWithEpgData(it, emptyList()) }.toMutableList()
for (channel in sortedChannels) {
val epgResult = stalkerRepository.getShortEpgByChannel(
url,
channel.id.toString(),
password,
"Bearer $token"
)
when (epgResult) {
is Resource.Success -> {
val epg = epgResult.data?.js?.map {
EpgData(
it.id.toString(),
it.ch_id,
it.time,
it.time_to,
it.duration,
it.name,
it.descr,
it.category,
it.director,
it.actor,
it.start_timestamp.toLong(),
it.stop_timestamp.toLong(),
it.t_time,
it.t_time_to,
accountId,
null,
it.ch_id,
"${it.id}_$accountId"
)
}
val tvChannelsWithEpgItem =
tvChannelsWithEpg.find { it.tvChannels.id == channel.id }
if (tvChannelsWithEpgItem != null) {
if (!epg.isNullOrEmpty()) {
tvChannelsWithEpgItem.epgDataList = epg
} else {
tvChannelsWithEpgItem.epgDataList = emptyList()
}
tvChannelsWithEpgList.value = tvChannelsWithEpg
}
}
is Resource.Error -> {
Log.d(
"EPG ERROR",
"ERROR FETCHING EPGDATA FOR CHANNEL ${channel.name}: ${epgResult.message}"
)
}
}
}
channelsWithEpgRequestSuccessful()
}
}
ADAPTER_CODE:
class TvChannelsAdapter(
private val onClickListener: OnClickListener,
private val onLongClickListener: OnLongClickListener
) : ListAdapter<TvChannelsWithEpgData, TvChannelsAdapter.ViewHolder>(
TV_CHANNELS_COMPERATOR
) {
private val timer = Timer()
private var progressUpdater: TimerTask? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = RvItemTvChannelsBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding)
}
inner class ViewHolder(val binding: RvItemTvChannelsBinding) :
RecyclerView.ViewHolder(binding.root) {
val progressBar: ProgressBar = binding.progressBar
private val handler = Handler(Looper.getMainLooper())
private var progressUpdater: Runnable? = null
fun bind(channel: TvChannelsWithEpgData) {
binding.apply {
progressBar.progress = 0
rvItemNameTvchannels.text = channel.tvChannels.name
val image = channel.tvChannels.logo
if (!image.isNullOrEmpty()) {
Glide.with(itemView.context).load(channel.tvChannels.logo)
.into(binding.rvItemLogoTvchannels)
}
val currentTimeMillis = System.currentTimeMillis() / 1000
var currentProgram: EpgData? = null
var duration = 0L
val currentTime = SimpleDateFormat("HH:mm", Locale.getDefault())
.format(Date(currentTimeMillis * 1000))
for (epgData in channel.epgDataList) {
if (currentTimeMillis >= epgData.startTimestamp && currentTimeMillis <= epgData.stopTimestamp) {
currentProgram = epgData
duration = (epgData.stopTimestamp - epgData.startTimestamp).toLong()
break
}
}
val nextProgram = channel.epgDataList.firstOrNull { it.startTimestamp > currentTimeMillis }
if (currentProgram != null) {
rvItemCurrentepgProgram.text = currentProgram.name
rvItemCurrentepgStarttime.text = "${currentProgram.tTime} -"
rvItemCurrentepgEndtime.text = currentProgram.tTimeTo
progressBar.max = 100
val progress =
((System.currentTimeMillis() / 1000 - currentProgram.startTimestamp) * 100 / duration).toInt()
progressBar.progress = progress
progressUpdater?.let { handler.removeCallbacks(it) }
progressUpdater = object : Runnable {
override fun run() {
val currentTimeMillis = System.currentTimeMillis() / 1000
if (currentTimeMillis > currentProgram.stopTimestamp) {
bind(channel) // update EPG when current program has ended
} else {
val progress =
((currentTimeMillis - currentProgram.startTimestamp) * 100 / duration).toInt()
progressBar.progress = progress
}
handler.postDelayed(this, 1000)
}
}
handler.post(progressUpdater!!)
} else {
rvItemCurrentepgProgram.text = "No information"
rvItemCurrentepgStarttime.text = "$currentTime -"
rvItemCurrentepgEndtime.text = currentTime
progressBar.max = 0
progressBar.progress = 0
}
if (nextProgram != null) {
rvItemNextepgProgram.text = nextProgram.name
} else {
rvItemNextepgProgram.text = "No information"
}
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val channel = getItem(position)!!
holder.bind(channel)
holder.itemView.setOnClickListener {
val currentProgram = channel.epgDataList.filter { epgData ->
val currentTimeMillis = System.currentTimeMillis() / 1000
currentTimeMillis >= epgData.startTimestamp && currentTimeMillis <= epgData.stopTimestamp
}.map { it.name }.firstOrNull()
val currentStartTime = channel.epgDataList.filter { epgData ->
val currentTimeMillis = System.currentTimeMillis() / 1000
currentTimeMillis >= epgData.startTimestamp && currentTimeMillis <= epgData.stopTimestamp
}.map { it.tTime }.firstOrNull()
val currentEndTime = channel.epgDataList.filter { epgData ->
val currentTimeMillis = System.currentTimeMillis() / 1000
currentTimeMillis >= epgData.startTimestamp && currentTimeMillis <= epgData.stopTimestamp
}.map { it.tTimeTo }.firstOrNull()
onClickListener.onClick(channel, currentProgram, currentStartTime, currentEndTime, position)
}
holder.itemView.setOnLongClickListener {
onLongClickListener.onLongClick(channel, position)
true
}
}
class OnClickListener(val clickListener: (channel: TvChannelsWithEpgData, currentProgram: String?, currentStartTime: String?, currentEndTime: String?, position: Int) -> Unit) {
fun onClick(channel: TvChannelsWithEpgData, currentProgram: String?, currentStartTime: String?, currentEndTime: String?, position: Int) = clickListener(channel, currentProgram, currentStartTime, currentEndTime, position)
}
class OnLongClickListener(val longclickListener: (channel: TvChannelsWithEpgData, position: Int) -> Unit) {
fun onLongClick(channel: TvChannelsWithEpgData, position: Int) = longclickListener(channel, position)
}
companion object {
private val TV_CHANNELS_COMPERATOR =
object : DiffUtil.ItemCallback<TvChannelsWithEpgData>() {
override fun areItemsTheSame(
oldItem: TvChannelsWithEpgData,
newItem: TvChannelsWithEpgData
) =
oldItem.tvChannels.id == newItem.tvChannels.id
override fun areContentsTheSame(
oldItem: TvChannelsWithEpgData,
newItem: TvChannelsWithEpgData
): Boolean {
return oldItem.epgDataList == newItem.epgDataList &&
oldItem == newItem
}
}
}
}
In the TvChannelsFragment I simple use:
viewModel.tvChannelsWithEpgList.observe(viewLifecycleOwner) {
tvChannelsAdapter.submitList(it)
}
Upvotes: 0
Views: 92