DazedFury
DazedFury

Reputation: 59

Confusion with LiveData Observers, Adapters, Coroutines

Using MVVM Architecture.

I'm having trouble trying to figure out how I should be approaching this task. The idea is that I want to retrieve a list of songs from a REST API, pass that list into a SongListAdapter that uses the song list to create a custom recycler view, and then inflate that view.

Before it was as simple as

//Get Songs
        CoroutineScope(IO).launch {
            val songList = viewModel.getSongs()                  

            withContext(Main){
                val rvSongs = rvSongs as RecyclerView
                val adapter = SongListAdapter(songList)
                rvSongs.adapter = adapter
                rvSongs.layoutManager = LinearLayoutManager(this@SongListActivity)
            }
        }

But now I want to do this with liveData instead and val songList = viewModel.getSongs() doesn't cut it. Instead I need to use

viewModel.getSongs().observe(this@SongListActivity, Observer {
    //UI Stuff
}

The issue is that getSongs() hits an api endpoint to retrieve the list, which needs to be done on a background thread. But observers can't be invoked on background threads... Which makes me think the way I'm approaching this is all wrong.

I was thinking that I could have two different functions for getting songs... one that hits the endpoint and the other which converts the List<Song> data to a MutableLiveData<List<Song>> but that feels like I'm over-complicating it. How should I go about doing this with livedata?

SongListActivity

class SongListActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.song_list) //I changed this for testing, change back to activity_main

        //Set View Model
        val viewModel = ViewModelProvider(this).get(SongListActivityViewModel::class.java)

        //Get Songs
        CoroutineScope(IO).launch{
            //Need to hit getSongs() endpoint here

            withContext(Main){
                val rvSongs = rvSongs as RecyclerView
                val adapter = SongListAdapter(songList)
                rvSongs.adapter = adapter
                rvSongs.layoutManager = LinearLayoutManager(this@SongListActivity)
            }
        }
    }
}

SongListActivityViewModel

class SongListActivityViewModel : ViewModel() {
    private val songLiveData = MutableLiveData<List<Song>?>()

    //Returns list of songs on the first page.
    suspend fun getSongs(): MutableLiveData<List<Song>?> {
        //Hit Endpoint
        val songPage1 = FunkwhaleRepository.getSongs()

        //Store as LiveData
        songLiveData.postValue(songPage1.results)
        return songLiveData
    }
}

Upvotes: 0

Views: 612

Answers (1)

Henry Twist
Henry Twist

Reputation: 5980

Firstly I think you should take a look at using proper Android lifecycle scopes to launch your coroutines. If you create a scope in an Android component like an Activity then you should look into cancelling it yourself when the activity is destroyed. There is some good documentation here which I would recommend taking a look at.

In regards to your actual question, you're on the right track!

First of all, your ViewModel should expose some LiveData as it does in your code but should launch a coroutine scope in its init block which fetches the data from the endpoint. Then when it has fetched it, you should only then set the LiveData value to your loaded data.

In your activity, instead of trying to launch a coroutine to setup your RecyclerView, set it up beforehand with just the LayoutManager and then start observing the exposed LiveData. Then when the data being observed is changed, only then set the adapter to the RecyclerView.

A quick pseudocode overview might help:

ViewModel

ViewModel {

    val liveData = MutableLiveData<>()

    init {
        scope.launch {
            liveData.value = api.fetch()
        }
    }
}

Activity

Activity {
    onCreate {
        ...
        recyclerView.layoutManager = LinearLayoutManager
        ...
        viewModel.liveData.observe {
            recyclerView.adapter = Adapter(data)
        }
    }
}

Note: You can also use the liveData(LiveDataScope<T>.() -> Unit) builder function to simplify the ViewModel code but I would recommend understanding both approaches and how they differ.

Upvotes: 1

Related Questions