So far I've been using this pattern whenever I want to access the database:
runBlocking {
launch {
// fetch something from the database and put it to some view
}
}
Now that I'm diving deeper into Kotlin coroutines, I'm increasingly convinced, that this is a bad pattern. Essentially, I might as well just allowMainThreadQueries
, as my pattern blocks the main thread anyway.
Unfortunately, I haven't found a proper pattern yet. How to effectively use Kotlins coroutines to access the database?
Is runBlocking
the only entry point into coroutines?
Consider this scenario:
override fun onCreate() {
setContentView(someLayout)
// concurrently fetch something from the database and put it in some view
// onCreate may return before this has finished
someButton.setOnClickListener {
// concurrently insert or update something in the database
}
}
CodePudding user response:
You should never use runBlocking
in an Android project, unless you are mixing Kotlin coroutines code with some Java code that can't use coroutines and it needs a way to call some coroutine in a blocking manner on one of its own background threads. In this case, you might use runBlocking
to create a bridge function for the Java code to call, but you would never call this function from Kotlin and certainly never call it from the main thread. Calling blocking code on the main thread freezes the UI, which makes your app feel janky and risks triggering an ANR (application not responding) error.
The correct way to begin a coroutine is to use a CoroutineScope to launch your coroutine. These are already provided for you by the Android Jetpack framework for Activities, Fragments, and ViewModels.
In an Activity, use lifecycleScope.launch
. In a Fragment, you should usually use viewLifecycleOwner.lifecycleScope.launch
. In a ViewModel, use viewModelScope.launch
.
What does using a CoroutineScope instead of runBlocking
do? It prevents the long-running suspending actions (like reading the database from the disk) from blocking the main thread and freezing your UI. And it automatically cancels the long-running work when the Activity/Fragment/ViewModel is torn down, so it prevents memory leaks and wasted resources.
CodePudding user response:
Assume that you are using Room
,
runBlocking
and allowMainThreadQueries
are usually used for Test purpose and you should never use them in release product.
what allowMainThreadQueries
do is give you permission to access database from Main Thread which is you should Never do, because it may freeze the UI.
use lifecycleScope.launch
to launch coroutine from Fragment/Activity
or viewModelScope.launch
from ViewModel
, you might need to explicitly add the dependencies
def lifecycleVersion = '2.4.0'
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
// Lifecycles only (without ViewModel or LiveData)
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
Lifecycle release note https://developer.android.com/jetpack/androidx/releases/lifecycle
you should call the database
operation from the ViewModel
to prevent cancelation from configuration change, If the user rotate the screen while the operation is in progress, it'll be canceled and the result won't be cached.
In Activity/Fragment
// read data from db
lifecycleScope.launch {
viewModel.someData.collect {
//do some stuff
}
}
// insert
someButton.setOnClickListener {
viewModel.insertToDatabase(someData)
}
In ViewModel
class MainViewModel (val dao: Dao) : ViewModel() {
// you should expose your data as Observable
val someData : Flow<List<SomeData>> = dao.getAllSomeData()
fun insertToDatabase(data:SomeData) = viewModelScope.launch {
dao.insert(data)
}
}
// query
@Query("SELECT * FROM some_data_table")
abstract fun getAllSomeData(): Flow<List<SomeData>>