首页 > 解决方案 > 当从另一个 Fragment/ViewModel 执行插入时,从 Room 返回的 Kotlin Flow 不会更新

问题描述

我有一个 Room 数据库,它返回一个对象流。当我将新项目插入数据库时​​,Flow 的收集功能仅在从相同的 Fragment/ViewModel 执行插入时触发。

我录制了一段展示该问题的快速视频:https ://www.youtube.com/watch?v=7HJkJ7M1WLg

这是我对相关文件的代码设置:

成就道.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
}

成就存储库.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()
    }
}

添加AchievementBottomSheetViewModel.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(应用程序):

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(项目):

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
}

我不确定是否应该归咎于我对 Kotlin Flow 的理解,或者我的设置是否在某些方面不正确,但我希望能在这个问题上提供一些帮助。

标签: androidkotlinandroid-roomkotlin-coroutineskotlin-flow

解决方案


确保使用相同的 RoomDatabase 实例。在您提供 AppDB 的地方添加一个 @Singleton 可能会成功。


推荐阅读