I am trying to migrate from LiveData to Kotlin Flow. Right now I am working on a project that has offline supports in Room.
I was looking through the documentation and I managed to write an observable query in coroutines with Flow. (see: here)
The problem that I am facing right now is that whenever I add the suspend keyword inside the DAO class and try to run the project, it fails with the following error:
error: Not sure how to convert a Cursor to this method's return type (kotlinx.coroutines.flow.Flow<MyModel>).
The code with the problem:
#Transaction
#Query("SELECT * FROM table WHERE status = :status LIMIT 1")
suspend fun getWithSpecificStatus(status: String): Flow<MyModel?>
I am calling the code like this:
val modelLiveData: LiveData<MyModel?> = liveData(Dispatchers.IO) {
val result = databaseService.getWithSpecificStatus(Enum.IN_PROGRESS.status).first()
result?.let {
emit(it)
}
}
I tried to keep things simple. why is my code failing?
You could directly initialise the value of modelLiveData as:
val modelLiveData=databaseService.
getWithSpecificStatus(Enum.IN_PROGRESS.status).first()
.asLiveData()
You have used Flow so asLiveData() is used to convert it to LiveData
Also suggestion , you do should not use the suspend keyword because when you are returning Flow, Room automatically does this asynchronously. You just need to consume the Flow.
Related
I'm working on implementing Room in an android app and I have a use case where I am receiving a Flow with the current user id for functionality similar to switching which logged in Google user to view a service as. I'd like to pass that flow into a #Query annotated method in my DAO to get user widgets so that if the currently selected user changes or the list of widgets stored changes, the output Flow<List> would change as well.
Something like
WidgetRepo.kt
val widgets: Flow<List<Widget>> = widgetDAO.getWidgetsByUser(currentUserID)
WidgetDao.kt
#Query("select * from widget where userID = :userID")
fun getWidgetsByUser(userID: Flow<Int>): Flow<List<Widget>>
I don't think that's a good practice. I would rather do the following logic in a specific use-case, for example:
class GetWidgetsByUserUseCase(
private val userRepo: UserRepository,
private val widgetLocalSource: WidgetRepository
){
suspend operator fun invoke() = userRepo.flatMapLatest { user ->
widgetLocalSource.getWidgetsByUser(user.id)
}
}
With this implementation every time a new user is emitted, the widgetLocalSource.getWidgetsByUser() will be triggered and the previous flows will be canceled.
See flatMapLatest for more information on the operator.
Room Database has supported Flow since before. Add this to gradle:
// Kotlin Extensions and Coroutines support for Room - latest Room version 2.3.0
implementation("androidx.room:room-ktx:$room_version")
See more Write observable queries with Room here.
I'm trying to rewrite my Kotlin database interface to use it with coroutines, so I made all the functions suspend. But when I run the application I get the error: Not sure how to convert a Cursor to this method's return type.
Failing code:
#Dao
interface PostDao {
#Query("SELECT * FROM post")
suspend fun getAll(): LiveData<List<Post>>
}
The function which failed the build returns the LiveData object and this seems to be the problem, because if I remove the "suspend" word or LiveData, the app works properly.
Working variant:
#Dao
interface PostDao {
#Query("SELECT * FROM post")
fun getAll(): LiveData<List<Post>>
}
Could anyone explain why does it work so? Is there a way to use suspend with function returning LiveData?
It doesn't really make sense to use a suspend function to return a LiveData. Generating a LiveData instance is a non-blocking action, so there's no reason for it to suspend. In the case of LiveData, the blocking data request happens in the background and updates the already-returned LiveData when it's ready, rather than waiting for the data and then generating the LiveData.
If you use a suspend function for your data, you would just return List<Post>. Calling this suspend function would make the request a single time, suspend until the data is ready, and then return it in your coroutine.
If you want to continually receive updated data, what you need is a coroutine Flow. Since a Flow is cold, you do not use a suspend function for it:
#Dao
interface PostDao {
#Query("SELECT * FROM post")
fun getAll(): Flow<List<Post>>
}
Then in your view model layer, you can either convert it to a LiveData:
val postsLiveData = repository.getAll().asLiveData()
or you can convert it to a hot StateFlow or SharedFlow, where consensus seems to be that it should be preferred over LiveData since it is not tied directly to Android details:
val postsSharedFlow = repository.getAll().shareIn(viewModelScope, SharingStarted.Eagerly, 1)
You can read about subscribing to SharedFlow and StateFlow in the documentation.
I made a simple example app with using Room and Flows:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val build = Room.databaseBuilder(this, FinanceDatabase::class.java, "database.db")
.fallbackToDestructiveMigration()
.build()
GlobalScope.launch {
build.currencyDao().addCurrency(CurrencyLocalEntity(1))
val toList = build.currencyDao().getAllCurrencies().toList()
Log.d("test", "list - $toList")
}
}
}
#Entity(tableName = "currency")
data class CurrencyLocalEntity(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "currencyId")
var id: Int
) {
constructor() : this(-1)
}
#Dao
interface CurrencyDao {
#Query("SELECT * FROM currency")
fun getAllCurrencies(): Flow<CurrencyLocalEntity>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addCurrency(currency: CurrencyLocalEntity)
}
#Database(entities = [CurrencyLocalEntity::class], version = 1)
abstract class FinanceDatabase : RoomDatabase() {
abstract fun currencyDao(): CurrencyDao
}
I want to use toList() function as in code above but something gets wrong and even Log doesn't print. At the same time using collect() works fine and gives me all records.
Can anybody explain to me what is wrong? Thanks.
There are a couple things wrong here but I'll address the main issue.
Flows returned by room emit the result of the query everytime the database is modified. (This might be scoped to table changes instead of the whole database).
Since the database can change at any point in the future, the Flow will (more or less) never complete because a change can always happen.
Your calling toList() on the returned Flow will suspend forever, since the Flow never completes. This conceptually makes sense since Room cannot give you the list of every change that will happen, without waiting for it to happen.
With this, I'm sure you know why collect gives you the records and toList() doesn't.
What you probably want here is this.
#Query("SELECT * FROM currency")
fun getAllCurrencies(): Flow<List<CurrencyLocalEntity>>
With this you can get the first result of the query with Flow<...>.first().
Flow in Room is for observing Changes in table.
Whenever any changes are made to the table, independent of which row is changed, the query will be re-triggered and the Flow will emit again.
However, this behavior of the database also means that if we update an unrelated row, our Flow will emit again, with the same result. Because SQLite database triggers only allow notifications at table level and not at row level, Room can’t know what exactly has changed in the table data
Make sure that the same doa object you are using for retrieving the list, is used for updating the database.
other than that converting flow to livedata is done using asLivedata extension function
For me below solution works for updating the view with database table changes.
Solution: Same Dao Object should be used when we insert details into the room database and get information from DB.
If you are using a dagger hilt then
#Singleton annotation will work.
I hope this will solve your problem.
**getAllCurrencies()** function should be suspend.
Please check the syntax to collect List from Flow:
suspend fun <T> Flow<T>.toList(
destination: MutableList<T> = ArrayList()
): List<T> (source)
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/to-list.html
There are apparently Kotlin coroutines extension functions for SqlDelight, but I don't know how to implement them since I can't find documentation.
I have a normal query that looks like this:
val allItems
get() = itemQueries.selectAll().mapToList()
Can I turn this into a suspend function?
There is currently (v1.2.1) no suspend function support for SqlDelight queries, however you can consume a Coroutines Flow object, which is even better. To do this you need to add the coroutines extension library in your app gradle:
dependencies {
implementation "com.squareup.sqldelight:coroutines-extensions:1.2.1"
}
Then turn your query into this:
val allItems: Flow<List<Item>> =
itemQueries.selectAll()
.asFlow()
.mapToList()
This flow emits the query result, and emits a new result every time the database changes for that query.
You can then .collect{} the results inside a coroutine scope.
For single-shot queries, you don't need the coroutine extension library. Instead, just do:
suspend fun getAllItems() = withContext(Dispatchers.IO) {
itemQueries.selectAll().mapToList()
}
The other answer is specific to when you want to react to changes in the database.
I am using Room persistent lib for my Android App with Kotlin. There are few cases where I would like to get a callback when room finishes the transaction. Yes, we can use LiveData and I am already using it but there are few rare cases where we app does not want to observe for changes but get a one-time callback to process ahead.
For example, I am using the following setup:
fun insertNewData(oldList: List<String>, finalList: List<String>, callback: () -> Unit) {
launch(Dispatchers.IO) {
async {
//1. Call function to delete old relations
// Delete query on JoinTable
//2.Call function to get IDs for new data
// Select or insert query on TableOne
//3. Call function to assign new relations
// Insert query on JoinTable
}.await()
callback()
}
}
So is this a good setup or there are better ways in Kotlin?
In the above function, I am passing a lambda function to get a callback instead of observing for changes using LiveData.