Gabs
Gabs

Reputation: 51

Kotlin: Live data does not change Fragment UI when data changes

I am struggling to use Live data on an MVVM pattern. The app is supposed to:

  1. Fetch data from an API (which it does correctly)
  2. Store that data in the Live data object from the ViewModel
  3. Then the fragment calls the Observer method to fill the recyclerView.

The problem comes in point 3, it does nothing, and I cannot find the solution.

Here is the relevant code. (If I'm missing something, I will try to answer as quickly as possible)

Main Activity:


class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val viewModel: SharedViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Custom button to fetch data from api and log the Live Data value.
        binding.refreshFab.setOnClickListener {
            viewModel.fetchPlayerData()
            Log.d("gabs", "${viewModel.livePlayerlist.value}")
        }
    }
}

ViewModel:

class SharedViewModel(app: Application): AndroidViewModel(app) {

//    val playerDao = LaRojaDB.getDatabase(app).playerDao()

    lateinit var playerList: Players
    val livePlayerlist: MutableLiveData<MutableList<Players.PlayersItem>> by lazy {
        MutableLiveData<MutableList<Players.PlayersItem>>()
    }

    fun fetchPlayerData() {
        CoroutineScope(Dispatchers.IO).launch {
            val response = MyService.getLaRojaService().getAllPlayers()
            withContext(Dispatchers.Main) {
                if (response.isSuccessful) {
                    val body = response.body()

                    if(!body.isNullOrEmpty()){
                        playerList = body
                        val playerArrayList = mutableListOf<Players.PlayersItem>()
                        playerList.forEach {
                            playerArrayList.add(it)
                        }
                        livePlayerlist.value = playerList
                    }
                }
            }
        }
    }
}

The fragment that displays the recycler view: (Fragment is already showing, I set up a textView as a title to make sure since I'm new using fragments as well.)

class PlayerListFragment : Fragment() {

    private var _binding: FragmentPlayerListBinding? = null
    private val binding get() = _binding!!

    private val model: SharedViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentPlayerListBinding.inflate(inflater, container, false)
        binding.rvPlayerList.layoutManager = LinearLayoutManager(activity)

        ----> // This is the observer that does not update the UI** <----
        model.livePlayerlist.observe( viewLifecycleOwner, {
            binding.rvPlayerList.adapter = PlayerAdapter(it)
        })

        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_player_list, container, false)
    }
}

Thank you all in advance, hope I can finally learn what is causing the issue!

Upvotes: 1

Views: 1267

Answers (1)

Martin Marconcini
Martin Marconcini

Reputation: 27226

I think you don't need to switch Coroutine contexts. A few changes I'd expect if I were reviewing this code:

This should all be in the same IO context. You then postValue to your liveData.

fun fetchPlayerData() {
   viewModelScope.launch(Dispatchers.IO) {
      val xx = api.fetch()
      ...
      _playerState.postValue(xx) //see below
   }
}

Additionally, it's preferred not to expose mutable state, so your ViewModel should not expose the MutableLiveData (which shouldn't really be lazy). But it's also better to encapsulate the state in a sealed class:

    //delete this
    val livePlayerlist: MutableLiveData<MutableList<Players.PlayersItem>> by lazy {
        MutableLiveData<MutableList<Players.PlayersItem>>()
    }

Should be: (names are just pseudo code, I have no idea what this code is about)

sealed class PlayerDataState {
  data class ListAvailable(data: List<Players.PlayersItem>>): PlayerDataState
  object Loading(): PlayerDataState
}

And your new LiveData:

private val _playerState = MutableLiveData<PlayerDataState>()
val playerState: LiveData<PlayerDataState>() get() = _playerState

Finally when observing from the UI, you just...

model.playerState.observe(viewLifecycleOwner, {
   when (it) {
     is Loading -> ... 
     is ListAvailable -> binding.rvPlayerList.adapter = PlayerAdapter(it.data)
   }
}

Upvotes: 1

Related Questions