Reputation: 2160
I'm rewriting an app that involves retrieving data from a server via REST, saving that to the database on each Android device, and then displaying that data to the user. The data being retrieved from the server has a "since" parameter, so it won't return all data, just data that has changed since the last retrieval.
I have the retrieval from the server working fine, but I'm not sure the best way to save that data to the database, then show it to the user. I'm using Kotlin, Retrofit, Room and LiveData.
The code below is a simplified version of what I'm actually doing, but it gets the point across.
MyData.kt (model)
@Entity(tableName = "MyTable")
data class MyData(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id Int? = null,
@SerializedName("message")
@ColumnInfo(name = "message")
var message: String? = null
) {
companion object {
fun fromContentValues(values: ContentValues): MyData {
val data = MyData()
// Do this for id and message
if (values.containsKey("id") {
data.id = values.getAsInteger("id")
}
}
}
}
DataViewModel.kt
class DataViewModel(application: Application) : AndroidViewModel(application) {
private val repository = DataRepository()
fun data(since: Long) =
liveData(Dispatchers.IO) {
val data = repository.getDataFromServer(since)
emit(data)
}
fun saveData(data: List<MyData>) =
liveData(Dispatchers.Default) {
val result = repository.saveDataToDatabase(data)
emit(result)
}
fun data() =
liveData(Dispatchers.IO) {
val data = repository.getDataFromDatabase()
emit(data)
}
}
DataRepository.kt
class DataRepository(application: Application) {
// I won't add how the Retrofit client is created, it's standard
private var client = "MyUrlToGetDataFrom"
private var myDao: MyDao
init {
val myDatabase = MyDatabase.getDatabase(application)
myDao = myDatabase!!.myDao()
}
suspend fun getDataFromServer(since: Long): List<MyData> {
try {
return client.getData(since)
} catch (e: Exception) {
}
}
fun getDataFromDatabase(): List<MyData> = myDao.getAll()
suspend fun insertData(data: List<MyData>) =
myDao.insertData(data)
}
MyDao.kt
@Dao
interface PostsDao {
@Query("SELECT * FROM " + Post.TABLE_NAME + " ORDER BY " + Post.COLUMN_ID + " desc")
suspend fun getAllData(): List<MyData>
@Insert
suspend fun insertData(data: List<MyData>)
}
ListActivity.kt
private lateinit var mDataViewModel: DataViewModel
override fun onCreate(savedInstanceBundle: Bundle?) {
super.onCreate(savedInstanceBundle)
mDataViewModel = ViewModelProvider(this, DataViewModelFactory(contentResolver)).get(DataViewModel::class.java)
getData()
}
private fun getData() {
mDataViewModel.data(getSince()).observe(this, Observer {
saveData(it)
})
}
private fun saveData(data: List<MyData>) {
mDataViewModel.saveData(data)
mDataViewModel.data().observe(this, Observer {
setupRecyclerView(it)
})
}
ListActivity.kt, and possibly the ViewModel and Repository classes where it uses coroutines, are where I'm stuck. getData() retrieves the data from the server without a problem, but when it comes to saving it in the database, then taking that saved data from the database and displaying it to the user I'm unsure of the approach. As I mentioned I'm using Room, but Room will not let you access the database on the main thread.
Remember, I have to save in the database first, then retrieve from the database, so I don't want to call mDataViewModel.data().observe
until after it saves to the database.
What is the proper approach to this? I've tried doing CoroutineScope on the mDataViewModel.saveData() then .invokeOnCompletion
to do mDataViewModel.data().observe
, but it doesn't save to the database. I'm guessing I'm doing my Coroutines incorrectly, but not sure where exactly.
It will also eventually need to delete and update records from the database.
Upvotes: 1
Views: 6496
Reputation: 2358
After reading comments and updated question I figured out that you want to fetch a small list of data and store it to database and show all the data stored in the database. If this is what you want, you can perform the following (omitted DataSouce for brevity) -
In PostDao
You can return a LiveData<List<MyData>>
instead of List<MyData>
and observe that LiveData
in the Activity to update the RecyclerView
. Just make sure you remove the suspend
keyword as room
will take care of threading when it returns LiveData
.
@Dao
interface PostsDao {
@Query("SELECT * FROM " + Post.TABLE_NAME + " ORDER BY " + Post.COLUMN_ID + " desc")
fun getAllData(): LiveData<List<MyData>>
@Insert
suspend fun insertData(data: List<MyData>)
}
In Repository make 2 functions one for fetching remote data and storing it to the database and the other just returns the LiveData
returned by the room
. You don't need to make a request to room
when you insert the remote data, room
will automatically update you as you are observing a LiveData
from room
.
class DataRepository(private val dao: PostsDao, private val dto: PostDto) {
fun getDataFromDatabase() = dao.getAllData()
suspend fun getDataFromServer(since: Long) = withContext(Dispatchers.IO) {
val data = dto.getRemoteData(since)
saveDataToDatabase(data)
}
private suspend fun saveDataToDatabase(data: List<MyData>) = dao.insertData(data)
}
Your ViewModel should look like,
class DataViewModel(private val repository : DataRepository) : ViewModel() {
val dataList = repository.getDataFromDatabase()
fun data(since: Long) = viewModelScope.launch {
repository.getDataFromServer(since)
}
}
In the Activity make sure you use ListAdapter
private lateinit var mDataViewModel: DataViewModel
private lateinit var mAdapter: ListAdapter
override fun onCreate(savedInstanceBundle: Bundle?) {
...
mDataViewModel.data(getSince())
mDataViewModel.dataList.observe(this, Observer(adapter::submitList))
}
First of all, I would recommend you to look into Android Architecture Blueprints v2. According to Android Architecture Blueprints v2 following improvements can be made,
DataRepository
should be injected rather than instantiating internally according to the Dependency Inversion principle.You should decouple the functions in the ViewModel
. Instead of returning the LiveData
, the data()
function can update an encapsulated LiveData
. For example,
class DataViewModel(private val repository = DataRepository) : ViewModel() {
private val _dataList = MutableLiveData<List<MyData>>()
val dataList : LiveData<List<MyData>> = _dataList
fun data(since: Long) = viewModelScope.launch {
val list = repository.getData(since)
_dataList.value = list
}
...
}
Repository
should be responsible for fetching data from remote data source and save it to local data source. You should have two data source i.e. RemoteDataSource
and LocalDataSource
that should be injected in the Repository. You can also have an abstract DataSource
. Let's see how can you improve your repository,
interface DataSource {
suspend fun getData(since: Long) : List<MyData>
suspend fun saveData(list List<MyData>)
suspend fun delete()
}
class RemoteDataSource(dto: PostsDto) : DataSource { ... }
class LocalDataSource(dao: PostsDao) : DataSource { ... }
class DataRepository(private val remoteSource: DataSource, private val localSource: DataSource) {
suspend fun getData(since: Long) : List<MyData> = withContext(Dispatchers.IO) {
val data = remoteSource.getData(since)
localSource.delete()
localSource.save(data)
return@withContext localSource.getData(since)
}
...
}
In your Activity
, you just need to observe the dataList: LiveData
and submit it's value to ListAdapter
.
private lateinit var mDataViewModel: DataViewModel
private lateinit var mAdapter: ListAdapter
override fun onCreate(savedInstanceBundle: Bundle?) {
...
mDataViewModel.data(since)
mDataViewModel.dataList.observe(this, Observer(adapter::submitList))
}
Upvotes: 2