Home > Mobile >  Observing live match scores from an API is not updating UI
Observing live match scores from an API is not updating UI

Time:10-12

I am trying to develop a football app demo. Data comes from an API from the link https://www.football-data.org/

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, how can i make it work?

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 while only looking at screen.

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 MatchesViewModelFactory(
    val app: Application,
    private val repository: MatchesRepository
): ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        @Suppress("UNCHECKED_CAST")
        if (modelClass.isAssignableFrom(MatchesViewModel::class.java)) {
            return MatchesViewModel(app, repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel Class")
    }
}
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)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentMatchesTodayBinding.inflate(inflater, container, false)
        return binding.root
    }

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

    private fun setupRecyclerView() {
        mMatchesAdapter = MatchesAdapter(this)
        binding.rvMatchesToday.adapter = mMatchesAdapter
        binding.rvMatchesToday.layoutManager = LinearLayoutManager(activity)
    }

  
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

}
class MatchesAdapter(val fragment: Fragment): RecyclerView.Adapter<MatchesAdapter.ViewHolder>() {

    class ViewHolder(view: ItemMatchBinding): RecyclerView.ViewHolder(view.root) {
        val tvMatchTime = view.tvMatchDate
        val ivHomeTeamImage = view.ivHomeTeamEmblem
        val tvHomeTeamScore = view.tvHomeTeamScore
        val tvHomeTeamName = view.tvHomeTeamName
        val ivAwayTeamImage = view.ivAwayTeamEmblem
        val tvAwayTeamScore = view.tvAwayTeamScore
        val tvAwayTeamName = view.tvAwayTeamName
    }

    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)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding: ItemMatchBinding = ItemMatchBinding.inflate(
            LayoutInflater.from(fragment.context), parent, false)
        return ViewHolder(binding)
    }

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

}

CodePudding user response:

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

}
  • Related