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
Related
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.
I am trying to write tests for my Repository which provides access to my Room database.
For this, I wrote a Mock Database and a mock DAO:
My Database:
abstract class JoozdlogDatabase protected constructor(): RoomDatabase() {
abstract fun aircraftTypeDao(): AircraftTypeDao
// (...)
}
class MockDatabase: JoozdlogDatabase() {
override fun aircraftTypeDao(): AircraftTypeDao = MockAircraftTypeDao()
}
My DAO:
interface AircraftTypeDao {
#Query("SELECT * FROM AircraftTypeData")
suspend fun requestAllAircraftTypes(): List<AircraftTypeData>
#Query("SELECT * FROM AircraftTypeData")
fun aircraftTypesFlow(): Flow<List<AircraftTypeData>>
// etc etc //
}
class MockAircraftTypeDao: AircraftTypeDao {
private val simulatedDatabase = ArrayList<AircraftTypeData>()
private val simulatedFlow = MutableStateFlow<List<AircraftTypeData>>(listOf(AircraftTypeData("aap", "noot", false, multiEngine = false)))
override suspend fun requestAllAircraftTypes(): List<AircraftTypeData> = simulatedDatabase
override fun aircraftTypesFlow(): Flow<List<AircraftTypeData>> = simulatedFlow
override suspend fun save(vararg aircraftTypeData: AircraftTypeData) {
//println("${this::class.simpleName} Saving ${aircraftTypeData.size} type data")
simulatedDatabase.addAll(aircraftTypeData)
emit()
}
override suspend fun clearDb() {
println("${this::class.simpleName} Clear DB")
simulatedDatabase.clear()
emit()
}
private fun emit(){
println("emit() should emit ${simulatedDatabase.size} items")
simulatedFlow.update { simulatedDatabase.toList() } // also tried: simulatedFlow.value = simulatedDatabase.toList()
println("simulatedFlow.value is now ${simulatedFlow.value.size}")
}
My Test data:
object TestData {
val aircraftTypes = listOf(
AircraftType("Test Aircraft 1 (MP/ME)", "TAC1", multiPilot = true, multiEngine = true),
AircraftType("Test Aircraft 2 (SP/SE)", "TAC2", multiPilot = false, multiEngine = false)
)
}
and my test:
#Test
fun test() {
runTest {
var currentTypesList: List<AircraftType> = emptyList()
val aircraftRepository = AircraftRepository.mock(MockDatabase())
// DispatcherProvider.default() provides UnconfinedTestDispatcher(TestCoroutineScheduler()) for my test.
launch(DispatcherProvider.default()) {
aircraftRepository.aircraftTypesFlow.collect {
println("emitted ${it.size} flights: $it")
currentTypesList = it
}
}
aircraftRepository.replaceAllTypesWith(TestData.aircraftTypes)
delay(500)
println("Done waiting")
assertEquals (2, currentTypesList.size)
}
}
Expected result: Test passed.
received result: java.lang.AssertionError: expected:<2> but was:<1> for the single assert
received output:
emitted 1 flights: [AircraftType(name=aap, shortName=noot, multiPilot=false, multiEngine=false)]
MockAircraftTypeDao Clear DB
emit() should emit 0 items
simulatedFlow.value is now 0
emit() should emit 2 items
simulatedFlow.value is now 2
Done waiting
Now, I have been at this all morning and I just don't get why it won't collect anything but the first value.
Things I tried:
Making a flow object to test my collector -> collector is OK
Accessing the flow item in DAO directly -> Does not work
Setting value of MutableStateFlow with update and with value = -> neither works.
Making a different flow object the is exactly the same but not called from a suspend function: Works.
So, I guess something about the calling suspend function is doing something wrong, but the Flow object is being updated before the delay is over, and it just won't collect.
If anybody is much smarter than me and can explain what I am doing wrong, I would very much appreciate it.
I fixed this by using the suggestion posted here and switching to turbine for all my flow testing needs.
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()
}
}
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
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.