Room with Flowable: initialize database if it's empty - android

I have following #Dao, that provides Flowable<User> stream:
#Dao
interface UsersDao {
#Query("SELECT * FROM users")
fun loadUsers(): Flowable<List<User>>
}
I want the subscriber of the stream to receive updates of the database as soon as some change happens there. Subscribing to Room's Flowable I will get that feature out of the box.
What I want is following: if database is empty I want to perform a web request and save users into database. The subscriber will automatically receive new updates that had just happened.
Now I want the client of the repository not to be aware all of the initialization logics: all he does - he performs usersRepository.loadUsers(). And all of these magic should take place inside the repository class:
class UsersRepository #Inject constructor(
private val api: Api,
private val db: UsersDao
) {
fun loadUsers(): Flowable<List<User>> {
...
}
}
Of course I can use following approach:
fun loadUsers(): Flowable<List<User>> {
return db.loadTables()
.doOnSubscribe {
if (db.getCount() == 0) {
val list = api.getTables().blockingGet()
db.insert(list)
}
}
}
But I would like to construct the stream without using side-effects (doOn... operators). I've tried composing() but that didn't help much. Been stuck on this for a while.

You could apply some conditional flatMaps:
#Dao
interface UsersDao {
#Query("SELECT * FROM users")
fun loadUsers(): Flowable<List<User>>
#Query("SELECT COUNT(1) FROM users")
fun userCount() : Flowable<List<Integer>>
#Insert // I don't know Room btw.
fun insertUsers(List<User> users) : Flowable<Object>
}
interface RemoteUsers {
fun getUsers() : Flowable<List<User>>
}
fun getUsers() : Flowable<List<User>> {
return
db.userCount()
.take(1)
.flatMap({ counts ->
if (counts.isEmpty() || counts.get(0) == 0) {
return remote.getUsers()
.flatMap({ users -> db.insertUsers(users) })
.ignoreElements()
.andThen(db.loadUsers())
}
return db.loadUsers()
})
}
Disclaimer: I don't know Room so please adapt the example above as the features of it allow.

Assuming your insert() call is async and also handles updates, you could do something like this:
fun loadUsers(): Flowable<List<User>> = userDao.getAllUsers().switchIfEmpty { api.getAllUsers().doOnNext { userDao.insert(it) } }
You could also use some:
fun loadUsers(): Flowable<List<User>> = userDao.getAllUsers().flatMap { it-> if (it.isEmpty()) api.getAllUsers().doOnNext { userDao.insert(it) } else Flowable.just(it)}
Advice:
You should consider the case when the data is stale, therefore you need to go another way around, do a network request and database call at the same time. Whichever observable finish first, take the result and display it. Updating database should be right after network call is done.

Related

Kotlin Flow collect takes a long time

I have a local database in my Android app. There is a function that takes some data from server and updates local database.
When this function is running, if I collect a list from local database by returning Flow, it takes unusual time to finish.
I don't have any problem with LiveData, it works well but Flow doesn't.
this is my dao :
#Transaction
#Query("SELECT * FROM tbl WHERE id=:id")
fun getData(id: String): Flow<Entity?>
repo :
fun getData(id: String): Flow<Entity?> {
return dao.getData(id).map { it?.toModel() }
}
fragment :
lifecycleScope.launch() {
repo.getData(args.id)
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.distinctUntilChanged()
.collect { data ->
data?.let {
setData(data)
}
}
}
Try to add .flowOn(Dispatchers.IO) in you repository function after map like this:
Repository
fun getData(id: String): Flow<Entity?> {
return dao.getData(id)
.map { it?.toModel() }
.flowOn(Dispatchers.IO)
}
I think the problem is that you delay the main thread with your heavy function operation (map) on the flow.

Why my flow doesn't triggers, when data changes in RoomDao

I'm using RoomDao with kotlin coroutines and Flow. What I'm trying to do is collect one Training with all its Exercises with all Repetitions per Exercise. Exercises and Repetitions are Flows, cuz this values can be changed and I want to observe them.
The problem is that when I updating exercises, getTrainingExerciseLinksBy doesn't triggers, and I don't know, why. Here is my code in UseCase:
suspend fun getTrainingWithExercisesAndRepetitionsBy(trainingId: Long): Flow<UiTrainingWithExercisesAndRepetitions> {
/// This method returns Flow<List<TrainingExerciseLink>>
return trainingExerciseLinksRepository.getTrainingExerciseLinksBy(trainingId).flatMapConcat { trainingExerciseLinks ->
trainingExerciseLinks.map { trainingExerciseLink ->
/// This method returns Flow<List<ExerciseRepetition>>
repetitionsRepository.getExerciseRepetitionsBy(trainingExerciseLink.id).map { repetitions ->
/// do some other selects for collecting data about exercise in one training
}.flowOn(Dispatchers.IO)
}.zipFlows()
}.flowOn(Dispatchers.IO)
}
In my ViewModel I'm observing this method like this:
viewModelScope.launch {
useCase.getTrainingWithExercisesAndRepetitionsBy(trainingId)
.distinctUntilChanged()
.collect {
_exercisesListLiveData.value = it.exercises
_trainingListLiveData.value = it.trainingData
}
}
What is wrong with this code?
UPD:
In my DAO I'm using Flows for subscribing on database's updates, like this:
#Dao
abstract class TrainingExerciseLinkDao {
#Query("select * from TrainingExerciseLink where trainingId = :trainingId")
abstract fun getTrainingExerciseLinksBy(trainingId: Long): Flow<List<TrainingExerciseLink>>
}
and ExerciseRepetitionsDao:
#Dao
abstract class ExerciseRepetitionDao {
#Query("select * from ExerciseRepetitionEntity where trainingExerciseId = :trainingExerciseId")
abstract fun getExerciseRepetitionsBy(trainingExerciseId: Long): Flow<List<ExerciseRepetitionEntity>>
}
Actually I found the answer, so maybe somebody will jump in the same gap and this thread will be helpful.
The problem in my code was that I used flatMapConcat. This operator waits emits from original Flow and from flatMapped Flow at one time, so in this case it will trigger callback. To fix this, flatMapLatest should be used. You can read more about difference between this operators here.
So my code now looks like this:
suspend fun getTrainingWithExercisesAndRepetitionsBy(trainingId: Long): Flow<UiTrainingWithExercisesAndRepetitions> {
/// This method returns Flow<List<TrainingExerciseLink>>
/// Here is main change: flatMapConcat -> flatMapLatest
return trainingExerciseLinksRepository.getTrainingExerciseLinksBy(trainingId).flatMapLatest { trainingExerciseLinks ->
trainingExerciseLinks.map { trainingExerciseLink ->
/// This method returns Flow<List<ExerciseRepetition>>
repetitionsRepository.getExerciseRepetitionsBy(trainingExerciseLink.id).map { repetitions ->
/// do some other selects for collecting data about exercise in one training
}.flowOn(Dispatchers.IO)
}.zipFlows()
}.flowOn(Dispatchers.IO)
}
You are using it wrong , as when database updates your getTrainingWithExercisesAndRepetitionsBy does not know,
to get over this issue use flows in your dao like this example as Room supports Flow then
viewModelScope.launch {
viewModel.yourFunctionThatGetsDataFromRepository(trainingId)
.distinctUntilChanged()
.collect {
_exercisesListLiveData.value = it.exercises
_trainingListLiveData.value = it.trainingData
}
}
and if more you can refer this example

How to get data from Room database

I have a function written which listens to changes happening in my room database. if I just want to get the data at any other point without listening to changes what should I be doing.
Dao
#Dao
interface OfflineDataDao {
#Query("SELECT * FROM OfflineData")
fun getOfflineData(): Flow<List<OfflineData>>
class OfflineDatabaseManager private constructor(
private val dp: LibraryDatabase
) {
//LISTENS TO CHANGES IN THE DATABASE
fun getOfflineData(): Flow<List<OfflineData>> {
return dp.getOfflineDataDao().getOfflineData()
}
}
fun getOfflineData() {
launch {
OfflineDatabaseManager.getInstance(app.applicationContext).getOfflineData().collect {
Timber.d("OfflineDataLib - observing offline data" + it.toString())
}
}
}
Using the above functions I can listen to changes in the database but what if I just want to get the data from the database any other point.
How can I do that please
To be more precise, I have a function in which I listen to network changes, if have network or not and in there I want get the data in the offline data, How can I do this.
This is my function which listens to network changes
fun listenToConnectionChanges() {
launch {
OfflineDatabaseManager.getInstance(app.applicationContext).networkConnectionActivated
.collect { isNetworkConnectionActive ->
Timber.d("OfflineDataLib - getOfflineData() - isNetworkConnectionActive - " + isNetworkConnectionActive)
if (isNetworkConnectionActive) {
//I WANT TO GET THE DATA FROM THE DATABASE
}
}
}
}
Thanks
R
You can use a suspend function and directly return List<OfflineData> in your DAO.
OfflineDataDao:
#Query("SELECT * FROM OfflineData")
suspend fun getOfflineData(): <List<OfflineData>
OfflineDatabaseManager:
suspend fun getOfflineData(): <List<OfflineData> {
return dp.getOfflineDataDao().getOfflineData()
}
And then directly get the data in a coroutine scope:
fun getOfflineData() {
launch {
val items = OfflineDatabaseManager.getInstance(app.applicationContext).getOfflineData()
}
}

Wait for Room #Insert query completion

I can't figure out how to do a "simple" operation with Room and MVVM pattern.
I’m fetching some data with Retrofit. A “proper” response triggers an observer in the activity and a small part of the response itself is inserted in the database using Room library, wiping all previous values stored and inserting the fresh ones. Otherwise old values are retained on DB.
Just after that, I would like to check for a field in the database, but I’m not able to force this operation to wait until the previous one is completed.
Models
#Entity(tableName = "licence")
data class Licence(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "licence_id")
var licenceId: Int = 0,
#Ignore
var config: List<LicenceConfig>? = null,
.......
//all the others attributes )
#Entity(foreignKeys = [
ForeignKey(
entity = Licence::class,
parentColumns = ["licence_id"],
childColumns = ["licence_reference"],
onDelete = ForeignKey.CASCADE
)],tableName = "licence_configurations")
data class LicenceConfig(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "licence_config_id")
var licenceConfigId: Int,
#ColumnInfo(name="licence_reference")
var licenceReference: Int,
Observer in the activity
loginViewModel.apiResponse.observe(this, Observer { response ->
response?.let {
loginViewModel.insertLicences(response.licence)
}
//here I need to wait for the insertion to end
loginViewModel.methodToCheckForTheFieldOnDatabase()
})
ViewModel
fun insertLicences(licences: List<Licence>) = viewModelScope.launch {
roomRepository.deleteAllLicences()
licences.forEach { licence ->
roomRepository.insertLicence(licence).also { insertedLicenceId ->
licence.config?.forEach { licenceConfiguration ->
roomRepository.insertLicenceConfiguration(
licenceConfiguration.apply { licenceReference = insertedLicenceId.toInt() }
)
}
}
}
}
Room Repository
class RoomRepository(private val roomDao: RoomDao) {
val allLicences: LiveData<List<Licence>> = roomDao.getAllLicences()
suspend fun insertLicence(licence: Licence): Long {
return roomDao.insertLicence(licence)
}
suspend fun insertLicenceConfiguration(licenceConfiguration: LicenceConfig){
return roomDao.insertLicenceConfiguration(LicenceConfig)
}
}
RoomDao
#Dao
interface RoomDao {
#Query("select * from licence")
fun getAllLicences(): LiveData<List<Licence>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLicence(licence: Licence): Long
#Insert
suspend fun insertLicenceConfiguration(licence: LicenceConfig)
#Query("DELETE FROM licence")
suspend fun deleteAllLicences()
}
Set an observer to the "allLicences" LiveData or directly on that field on DB is not an option because the operations will be performed just after the activity creation and I have to wait until the API response to perform them.
In another project, without Room, I have used async{} and .await() to perform sequential operations while working with coroutines but I can't really make it works here. When I pause the debugger just after the insertion method the value of "allLicences" it's always null but after resuming and exporting the DB the data are properly inserted. I also tried adding .invokeOnCompletion{} after the ViewModel method but with the same result.
Basically I would like to wait for this method to end to do another operation.
Any suggestions?
EDIT
I totally forgot to report the models! Each licence have a list of configurations. When I perform a licence insert I take the autogenerated id, I apply it to the licenceConfig and then I perform the insert for each licenceConfig object (the code in the nested forEach loop of the ViewModel method). The problem seems to be that performing this nested loop breaks the "synchronicity" of the operation
To wait until insertion is completed, you need to move the coroutine creation from insertLicences() to your observer and also make the insertLicences() a suspend function.
loginViewModel.apiResponse.observe(this, Observer { response ->
lifecycleScope.launch {
response?.let {
loginViewModel.insertLicences(response.licence)
}
//here I need to wait for the insertion to end
loginViewModel.methodToCheckForTheFieldOnDatabase()
}
})
and
suspend fun insertLicences(licences: List<Licence>) {
roomRepository.deleteAllLicences()
licences.forEach { licence ->
roomRepository.insertLicence(licence).also { insertedLicenceId ->
licence.config?.forEach { licenceConfiguration ->
roomRepository.insertLicenceConfiguration(
licenceConfiguration.apply { licenceReference = insertedLicenceId.toInt() }
)
}
}
}
}
Alternative Solution
You can shift all of the code present in the observer into ViewModel.
loginViewModel.apiResponse.observe(this, Observer { response ->
loginViewModel.refreshLicenses(response)
})
and in ViewModel
fun refreshLicenses(response:Response?){
viewModelScope.launch{
response?.let {
insertLicences(response.licence)
}
methodToCheckForTheFieldOnDatabase()
}
}
and also make insertLicences as suspend function
suspend fun insertLicences(licences: List<Licence>) {
roomRepository.deleteAllLicences()
licences.forEach { licence ->
roomRepository.insertLicence(licence).also { insertedLicenceId ->
licence.config?.forEach { licenceConfiguration ->
roomRepository.insertLicenceConfiguration(
licenceConfiguration.apply { licenceReference = insertedLicenceId.toInt() }
)
}
}
}
}
Edit: Didn't read your conclusion before I reply but, I still think that your answer lies in coroutines
Using callbacks or promises, won't your function be executed when the insert query is finished?
Callbacks
With callbacks, the idea is to pass one function as a parameter to
another function, and have this one invoked once the process has
completed.
fun postItem(item: Item) {
preparePostAsync { token ->
submitPostAsync(token, item) { post ->
processPost(post)
}
}
}
fun preparePostAsync(callback: (Token) -> Unit) {
// make request and return immediately
// arrange callback to be invoked later
}
I would prefer promises to be honest
Promises
The idea behind futures or promises (there are also other terms these
can be referred to depending on language/platform), is that when we
make a call, we're promised that at some point it will return with an
object called a Promise, which can then be operated on.
fun postItem(item: Item) {
preparePostAsync()
.thenCompose { token ->
submitPostAsync(token, item)
}
.thenAccept { post ->
processPost(post)
}
}
fun preparePostAsync(): Promise<Token> {
// makes request an returns a promise that is completed later
return promise
}
Do your work and when the promise is fullfilled, proceed to data validation.
You can read more about coroutines here

Cannot make the method retryWhen work(Kotlin, Rx)

I invoke retryNextTweet.onNext() method but it doesn't make observable retry.
My goal is to get the next item from the local storage. If there are no records in sqlite, then i'll fill the local storage using apiService and retry.
private val retryNextTweet: PublishSubject<Any> = PublishSubject.create()
override fun getNextTweet(cacheId: Long, tweetSearchParams: TweetSearchParams): Observable<Tweet> {
return tweetDao.getNextTweet(cacheId)
.toObservable()
.retryWhen {
it.flatMap { loadTweetsFromApi(tweetSearchParams).subscribe({
if(it.isNotEmpty())
retryNextTweet.onNext(Any())
}, {})
retryNextTweet }
}
}
#Dao
interface TweetDao {
#Query("SELECT * FROM tweet WHERE cacheId > :cacheId LIMIT 1")
fun getNextTweet(cacheId: Long): Single<Tweet>
}
I just replaced PublishSubject with a BehaviorSubject and it worked. Thank you akarnokd https://stackoverflow.com/users/61158/akarnokd

Categories

Resources