Why does suspend doesn't work with LiveData? - android

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.

Related

Android Observable Queries not working after adding suspend word

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.

What approach should I use in the DAO to manipulate data in a database using Room?

For now I use this code in my DAO:
#Dao
interface NotesDao {
#Query("SELECT * FROM Notes")
fun getAllNotes(): List<Notes?>?
#Query("SELECT * FROM Notes WHERE not hidden AND not grouped")
fun getNotes(): List<Notes?>?
#Query("SELECT * FROM Notes WHERE id = :id")
fun getById(id: Long): Notes?
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(notes: Notes?)
#Update
fun update(notes: Notes?)
#Delete
fun delete(notes: Notes?)
}
But in many tutorials I either see Flowable<List<Notes>> instead of List<Notes> or LiveData<List<Notes>>.
Which approach is preferable?
There are two factors you should consider:
All interactions with database (both queries and updates) should be outside main thread.
In some use-cases you want single "one-shot" query, in other - you want your query to be "live" - i.e. to return updated result whenever data was changed (Observer pattern).
Using Kotlin you have two main options (what's better is a matter of taste. In some tutorials you can see also AsyncTask or Thread executors for switching off the main thread):
Use RxJava (use Flowable for "live" queries, Single - for "one-shot" queries and one of the Single/Completable/Maybe for insert/update/delete). Using RxJava you can use built-in mechanism for switching off the main thread.
Use Coroutines (and keyword suspend) for insert/update/delete, and LiveData or Flow (from coroutines library) for "live" queries, or suspend - for "one-shot" queries. suspend lets to switch from the main thread. If you use LiveData/Flow you don't need suspend, since Room does it itself.
See also official documentation for additional hints.

Observe the Room database 'insert' callback

How can we get the callback for the 'insert' operation in repository as Livedata and I can pass back to viewmodel using coroutines?
Dao
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(user : User): Long
I am not sure you need callback if you use coroutines in your code.
If you make your insert function as suspend, you can call it from your ViewModel from inside coroutine (for example with viewModelScope). And inside this coroutine you'll move to the next line of code only after insert method would be done:
viewModelScope.launch{
val userId = repository.insert(someUser)
// <- there coroutine is suspended until insert is done. In a sense it's like a callback
someOtherMethod()
}

Android Room library with Kotlin Flow toList() doesn't work

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

Why calling database dao function on UI?

I am following kotlin android fundamental codelabs. There is a part where the app need a reference of all data from database
The following code is placed on ViewModel, database is passed as the instance of dao
val nights = database.getAllNights()
The implementation of getAllNights is as follows (In DAO)
#Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
fun getAllNights(): LiveData<List<SleepNight>>
All other database calls are delegated to private suspend function and dispatched to IO thread except that one.
Isn't this call block the main thread
Why is it allowed
CODELAB

Categories

Resources