Reputation: 59
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
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