'Kotlin Flow returned from Room does not update when an insert is performed from another Fragment/ViewModel
I have a Room database that returns a Flow of objects. When I insert a new item into the database, the Flow's collect function only triggers if the insert was performed from the same Fragment/ViewModel.
I have recorded a quick video showcasing the issue: https://www.youtube.com/watch?v=7HJkJ7M1WLg
Here is my code setup for the relevant files:
AchievementDao.kt:
@Dao
interface AchievementDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(achievement: Achievement)
@Query("SELECT * FROM achievement")
fun getAllAchievements(): Flow<List<Achievement>>
}
AppDB.kt:
@Database(entities = [Achievement::class], version = 1, exportSchema = false)
abstract class AppDB : RoomDatabase() {
abstract fun achievementDao(): AchievementDao
}
AchievementRepository.kt:
class AchievementRepository @Inject constructor(appDB: AppDB) {
private val achievementDao = appDB.achievementDao()
suspend fun insert(achievement: Achievement) {
withContext(Dispatchers.IO) {
achievementDao.insert(achievement)
}
}
fun getAllAchievements() = achievementDao.getAllAchievements()
}
HomeFragment.kt:
@AndroidEntryPoint
class HomeFragment : Fragment() {
private val viewModel: HomeViewModel by viewModels()
private lateinit var homeText: TextView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindViews()
subscribeObservers()
}
private fun bindViews() {
homeText = requireView().findViewById(R.id.txt_home)
requireView().findViewById<ExtendedFloatingActionButton>(R.id.fab_add_achievement).setOnClickListener {
AddAchievementBottomSheet().show(parentFragmentManager, "AddAchievementDialog")
}
requireView().findViewById<ExtendedFloatingActionButton>(R.id.fab_add_achievement_same_fragment).setOnClickListener {
viewModel.add()
}
}
private fun subscribeObservers() {
viewModel.count.observe(viewLifecycleOwner, { count ->
if(count != null) {
homeText.text = count.toString()
} else {
homeText.text = resources.getString(R.string.app_name)
}
})
}
}
HomeViewModel.kt:
class HomeViewModel @ViewModelInject constructor(private val achievementRepository: AchievementRepository) :
ViewModel() {
private val _count = MutableLiveData<Int>(null)
val count = _count as LiveData<Int>
init {
viewModelScope.launch {
achievementRepository.getAllAchievements()
.collect { values ->
// FIXME this is only called when inserting from the same Fragment
_count.postValue(values.count())
}
}
}
fun add() {
viewModelScope.launch {
achievementRepository.insert(Achievement(0, 0, "Test"))
}
}
}
AddAchievementBottomSheet.kt:
@AndroidEntryPoint
class AddAchievementBottomSheet : BottomSheetDialogFragment() {
private val viewModel: AddAchievementViewModel by viewModels()
private lateinit var addButton: MaterialButton
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_add_achievement, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addButton = requireView().findViewById(R.id.btn_add_achievement)
addButton.setOnClickListener {
viewModel.add(::close)
}
}
private fun close() {
dismiss()
}
}
AddAchievementBottomSheetViewModel.kt:
class AddAchievementViewModel @ViewModelInject constructor(private val achievementRepository: AchievementRepository) :
ViewModel() {
fun add(closeCallback: () -> Any) {
viewModelScope.launch {
achievementRepository.insert(Achievement(0, 0, "Test"))
closeCallback()
}
}
}
build.gradle (app):
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileSdkVersion 30
defaultConfig {
applicationId "com.marcdonald.achievementtracker"
minSdkVersion 23
targetSdkVersion 30
versionCode 1
versionName "1.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
implementation 'androidx.core:core-ktx:1.3.2'
// Android
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation "androidx.activity:activity-ktx:1.1.0"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
// Navigation
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
// Testing
testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
// Dagger Hilt
implementation 'com.google.dagger:hilt-android:2.29.1-alpha'
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
kapt 'com.google.dagger:hilt-android-compiler:2.29.1-alpha'
// Timber for logging
implementation 'com.jakewharton.timber:timber:4.7.1'
// Room
implementation 'androidx.room:room-runtime:2.2.5'
implementation 'androidx.room:room-ktx:2.2.5'
kapt 'androidx.room:room-compiler:2.2.5'
androidTestImplementation 'androidx.room:room-testing:2.2.5'
}
build.gradle (project):
buildscript {
ext.kotlin_version = "1.4.10"
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.0-alpha16'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.29.1-alpha'
}
}
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
I'm not sure if my understanding of Kotlin Flow is to blame or whether my setup is incorrect in some way, but I'd appreciate some help with the issue.
Solution 1:[1]
Make sure you use the same instance of your RoomDatabase. Add a @Singleton where you provide AppDB might do the trick.
Solution 2:[2]
Try calling subscribeObservers()
in the onStart()
lifecycle.
Solution 3:[3]
You need to add this dependency:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
Then don't collect the Flow in your ViewModel. Instead map it to your needs and expose it as LiveData like this:
class HomeViewModel @ViewModelInject constructor(private val achievementRepository: AchievementRepository) :
ViewModel() {
val count: LiveData<Int> = achievementRepository
.getAllAchievements()
.map {it.size}
.asLiveData()
fun add() {
viewModelScope.launch {
achievementRepository.insert(Achievement(0, 0, "Test"))
}
}
}
Solution 4:[4]
Flow is a cold stream which means you have to manually call Flow.collect{} to get the data.
To continuously observe changes in the database,
Option 1) convert Flow to Livedata,
val count: LiveData<Int> = achievementRepository.getAllAchivements().map {
it.count()
}.asLiveData()
Checkout the solution code in Google Codelab, "Android Room with a View - Kotlin"
Option 2) convert Flow to StateFlow which is a hot stream that you can observe on with StateFlow.collect {}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Wärting |
Solution 2 | voxobscuro |
Solution 3 | StefanTo |
Solution 4 |