【问题标题】:Kotlin Flow returned from Room does not update when an insert is performed from another Fragment/ViewModel当从另一个 Fragment/ViewModel 执行插入时,从 Room 返回的 Kotlin Flow 不会更新
【发布时间】:2020-11-16 21:33:51
【问题描述】:

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

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

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

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(应用程序):

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 的理解,或者我的设置是否在某些方面不正确,但我希望能在此问题上提供一些帮助。

【问题讨论】:

  • 只有在初始化 HomeViewModel 后才会执行您的调用。如果您想收到有关相应实体的任何数据库更新的通知,则需要再次调用 collect 函数。一种解决方案是直接从房间检索 LiveData 并观察那个。这样,您将收到发生在该数据库实体或更确切地说是该表的所有更改。
  • @Marc 你找到解决方案了吗?
  • 恐怕我无法使用 Kotlin Flow 找到解决方案,我最终只使用了 LiveData。
  • 确保房间数据库中的suspend函数返回flow,例如:Flow>
  • 您好,您找到解决方案了吗?

标签: android kotlin android-room kotlin-coroutines kotlin-flow


【解决方案1】:

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

【讨论】:

  • 恐怕不行,数据库已经是单例了
【解决方案2】:

尝试在onStart() 生命周期中调用subscribeObservers()

【讨论】:

    【解决方案3】:

    Flow 是冷流,这意味着您必须手动调用 Flow.collect{} 才能获取数据。

    要持续观察数据库的变化,

    选项 1) 将 Flow 转换为 Livedata,

    val count: LiveData<Int> = achievementRepository.getAllAchivements().map {
        it.count()
    }.asLiveData()
    

    在 Google Codelab 中查看解决方案代码,“Android Room with a View - Kotlin”

    选项 2) 将 Flow 转换为 StateFlow,这是您可以使用 StateFlow.collect {}

    观察到的热流

    【讨论】:

    • 您好,感谢您的回答,不幸的是,在尝试了这两个选项后,问题仍然存在。目前,我已经设置了将从 DAO 返回的 Flow 转换为 LiveData,类似于您和 Codelab 显示的方式。 ` class HomeViewModel ... { private val _count = MutableLiveData(null) val count = achievementRepository.allAchievements.map { it.count() }.asLiveData() } ` ` class AchievementRepository ... { val allAchievements = achievementDao.getAllAchievements() } ` (1/3)
    • 但是从对话框中添加的条目仍然不会立即显示。如果我从对话框中添加一个,然后立即从同一个片段添加,则数字会增加 2。例如:显示 1 -> 从同一片段添加 -> 显示 2 -> 从对话框添加 -> 显示 2 ->从同一片段添加 -> 显示 4 (2/3)
    • 即使从 DAO 查询返回 LiveData 并一起消除 Flow,也会出现同样的问题。 (3/3)
    • "如果 Lifecycle 对象未处于活动状态,则即使值发生更改,也不会调用观察者。" -- developer.android.com/topic/libraries/architecture/livedata 不使用 .asLiveData(),您可以通过将 HomeViewModel 类中的 init{} 块更改为方法,例如 getCount() 来发出一次性请求,然后在 onResume() 中调用此方法,主页片段。
    • 不幸的是,这也不起作用,因为当对话框打开时 HomeFragment 不会改变状态,所以当对话框关闭并且值不会改变时不会调用 onResume() 方法得到更新
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-14
    • 1970-01-01
    • 2014-04-18
    • 1970-01-01
    • 2020-11-25
    相关资源
    最近更新 更多