Kotlin Flow collect takes a long time - android

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.

Related

Koltin Flow is repeating execution using flatMapMerge

I was trying to implement an approach to fetch products from two Data sources (Room & FirebaseFirestore) using Flows.
It was working fine until I noticed that the debugger was returning to the same break point infinitely. When the execution of "ViewmMdel.insertProducts(products)" ends, the debugger returns to Repository.getProducts(//) & repeats.
I changed the approach using only suspending functions & coroutines & works fine but I am curious about how I must to use Flows to implement this approach.
Maybe is only that flatMapMerge is in preview version.
Thanks in advance :D
This one is the implementation:
ViewModel:
fun getProductNames(companyName: String) {
viewModelScope.launch {
repository.getProducts(companyName).catch {
_event.value = AddSaleEvents.ShouldShowLoading(false)
_event.value = AddSaleEvents.ProductsFailureResponse(it.message.toString())
}.collect { products ->
productsList = products
if (products != emptyList<Product>()) {
_event.value = AddSaleEvents.ShouldShowLoading(false)
_event.value = AddSaleEvents.ProductsSuccessfulResponse(products)
insertProducts(products)
} else {
_event.value = AddSaleEvents.ShouldShowLoading(false)
_event.value = AddSaleEvents.ProductsSuccessfulResponse(products)
}
}
}
}
Repository:
#OptIn(FlowPreview::class)
override suspend fun getProducts(compnayName: String): Flow<List<Product>> {
return localDataSource.getProducts().flatMapMerge { list -> // LINE RUNNING INFINITELY
getProductsFromFirebase(list, compnayName)
}.flowOn(Dispatchers.IO).catch {
Log.d("Error", it.message.toString())
}
}
private fun getProductsFromFirebase(products: List<Product>, compnayName: String) = flow {
if (products.isEmpty()) {
remoteDataSource.getProducts(compnayName).collect {
emit(it)
}
} else {
emit(products)
}
}
LocalDataSource with Room:
override suspend fun getProducts(): Flow<List<Product>> = saleDao.getProducts()
Firebase Data Source:
override suspend fun getProducts(company: String): Flow<List<Product>> = flow {
val response = fireStore.collection("products").whereEqualTo("company", company).get()
response.await()
if (response.isSuccessful && !response.result.isEmpty) {
emit(response.result.toObjects(FirebaseProduct::class.java).toEntity())
}
}.catch {
Log.d("Error", it.message.toString())
}
How can I chain the response of a flow to trigger another one inside the MVVM Architecture + Clean Architecture?
6 if it is possible, I want to understand the reason the code is repeating infinitely.
Looks like insertProducts(products) triggers room's DAO.
So localDataSource.getProducts() is a observable read query
Observable queries are read operations that emit new values whenever there are changes to any of the tables that are referenced by the query.
Try to change LocalDataSource
interface SaleDao {
// fun getProducts(): Flow<List<Product>>
suspend fun getProducts(): List<Product>
}

My MutableStateFlow doesnt emit when called from suspend function in test

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.

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

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

Room with Flowable: initialize database if it's empty

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.

Categories

Resources