kotlinandroid-recyclerviewandroid-viewmodelmutablelivedataandroid-diffutils

Observing live data from an API is not updating ui when data changes


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
    }

}

Solution

  • 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
            }
        }
    
    }