I am trying to develop a football app demo. Data comes from an API from the api
It loads data as expected when app started, but when score of match changes, ui is not updating for scores by itself. I am using DiffUtil getChangePayload() to detect changes in score and status fields of Match objects which comes from the response. But it is not triggering when live match data changes. What am i missing?
P.S. I put layout in SwipeRefreshLayout and when i refresh, it gets scores and update the ui. But i want to see the match status and scores updating by itself.
Here is my code:
class MatchesViewModel(
app: Application,
private val repository: MatchesRepository
): AndroidViewModel(app) {
val matchesToday: MutableLiveData<List<Matche>> = MutableLiveData()
init {
getMatchesToday()
}
fun getMatchesToday() = viewModelScope.launch {
safeMatchesToday()
}
private suspend fun safeMatchesToday() {
if (Constants.checkConnection(this)) {
val response = repository.getMatchesToday()
if (response.isSuccessful) {
response.body()?.let {
matchesToday.postValue(it.matches)
}
}
}
}
}
class MatchesTodayFragment : Fragment() {
private var _binding: FragmentMatchesTodayBinding? =null
private val binding get() = _binding!!
private lateinit var mMatchesAdapter: MatchesAdapter
private val viewModel: MatchesViewModel by viewModels {
MatchesViewModelFactory(requireActivity().application, (requireActivity().application as MatchesApplication).repository)
}
@RequiresApi(Build.VERSION_CODES.N)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
viewModel.matchesToday.observe(viewLifecycleOwner) { matches ->
mMatchesAdapter.differ.submitList(matches)
}
binding.srlMatchesToday.setOnRefreshListener {
viewModel.getMatchesToday()
binding.srlMatchesToday.isRefreshing = false
}
}
}
class MatchesAdapter(val fragment: Fragment): RecyclerView.Adapter<MatchesAdapter.ViewHolder>() {
private val differCallback = object: DiffUtil.ItemCallback<Matche>() {
override fun areItemsTheSame(oldItem: Matche, newItem: Matche): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Matche, newItem: Matche): Boolean {
return oldItem.status == newItem.status &&
oldItem.score.fullTime.home == newItem.score.fullTime.home &&
oldItem.score.fullTime.away == newItem.score.fullTime.away &&
oldItem == newItem
}
override fun getChangePayload(oldItem: Matche, newItem: Matche): Any? {
val bundle: Bundle = bundleOf()
if (oldItem.status != newItem.status) {
bundle.apply {
putString(Constants.MATCH_STATUS, newItem.status)
}
}
if (oldItem.score.fullTime.home != newItem.score.fullTime.home) {
bundle.apply {
putInt(Constants.HOME_SCORE, newItem.score.fullTime.home)
}
}
if (oldItem.score.fullTime.away != newItem.score.fullTime.away) {
bundle.apply {
putInt(Constants.AWAY_SCORE, newItem.score.fullTime.away)
}
}
if (bundle.size() == 0) {
return null
}
return bundle
}
}
val differ = AsyncListDiffer(this, differCallback)
@SuppressLint("UseCompatLoadingForDrawables")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val match = differ.currentList[position]
holder.apply {
Glide.with(fragment)
.load(match.homeTeam.crest)
.placeholder(fragment.resources.getDrawable(R.drawable.ic_ball))
.into(ivHomeTeamImage)
Glide.with(fragment)
.load(match.awayTeam.crest)
.placeholder(fragment.resources.getDrawable(R.drawable.ic_ball))
.into(ivAwayTeamImage)
tvHomeTeamName.text = match.homeTeam.name
tvAwayTeamName.text = match.awayTeam.name
when (match.status) {
Constants.TIMED -> {
tvMatchTime.text = Constants.toTimeForTR(match.utcDate)
tvHomeTeamScore.text = "-"
tvAwayTeamScore.text = "-"
}
Constants.PAUSED -> {
tvMatchTime.text = Constants.FIRST_HALF
tvHomeTeamScore.text = match.score.fullTime.home.toString()
tvAwayTeamScore.text = match.score.fullTime.away.toString()
}
Constants.FINISHED -> {
tvMatchTime.text = Constants.FINISHED
tvHomeTeamScore.text = match.score.fullTime.home.toString()
tvAwayTeamScore.text = match.score.fullTime.away.toString()
}
else -> {
tvMatchTime.text = Constants.IN_PLAY
tvHomeTeamScore.text = match.score.fullTime.home.toString()
tvAwayTeamScore.text = match.score.fullTime.away.toString()
}
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isNotEmpty()) {
val item = payloads[0] as Bundle
val status = item.getString(Constants.MATCH_STATUS)
val homeScore = item.getInt(Constants.HOME_SCORE)
val awayScore = item.getInt(Constants.AWAY_SCORE)
holder.apply {
tvMatchTime.text = status
tvHomeTeamScore.text = homeScore.toString()
tvAwayTeamScore.text = awayScore.toString()
Log.e("fuck", status.toString())
}
}
super.onBindViewHolder(holder, position, payloads)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
LiveData only pushes new values if you command it to. Since you want to do it repeatedly, you need to create a loop. This is very easy to do using the liveData
coroutine builder.
class MatchesViewModel(
app: Application,
private val repository: MatchesRepository
): AndroidViewModel(app) {
val matchesToday = liveData {
while (true) {
if (Constants.checkConnection(this)) {
val response = repository.getMatchesToday()
if (response.isSuccessful) {
response.body()?.let {
emit(it.matches)
}
}
}
delay(5000) // however many ms you want between fetches
}
}
}
If this is a Retrofit response, I think checking isSuccessful
is redundant because body()
will be non-null if and only if isSuccessful
is true. So it could be simplified a bit from what you have:
class MatchesViewModel(
app: Application,
private val repository: MatchesRepository
): AndroidViewModel(app) {
val matchesToday = liveData {
while (true) {
if (Constants.checkConnection(this)) {
repository.getMatchesToday()?.body()?.matches?.let(::emit)
}
delay(5000) // however many ms you want between fetches
}
}
}