Room数据库Flow转LiveData后value为null但observe可正常取值问题
问题现象
使用Room、Flow、LiveData与协程开发应用时,预期返回有效数据的Flow实际会先发射null项,具体表现为:在lifecycleScope.launch中获取viewModel.book.value得到null值,但通过observe(viewLifecycleOwner)观察LiveData却能拿到正常的Book数据,需要在BookDetailViewModel的getChapters方法中正确获取book的属性值。
相关代码配置
DAO层
@Dao interface BookDao { @Query("SELECT * FROM books WHERE id = :id") fun getBook(id: Long): Flow<Book> }
Repository层
@Singleton class BookRepository @Inject constructor( private val bookDao: BookDao ) { fun getBook(id: Long) = bookDao.getBook(id).filterNotNull() }
ViewModel层
@HiltViewModel class BookDetailViewModel @Inject internal constructor( savedStateHandle: SavedStateHandle, private val bookRepository: BookRepository, private val chapterRepository: ChapterRepository, ) : ViewModel() { val bookID: Long = savedStateHandle.get<Long>(BOOK_ID_SAVED_STATE_KEY)!! val book = bookRepository.getBook(bookID).asLiveData() fun getChapters(): LiveData<PagingData<Chapter>> { val lastChapterID = book.value.let { book -> book?.lastChapterID ?: 0L } val chapters = chapterRepository.getChapters(bookID, lastChapterID) return chapters.asLiveData() } companion object { private const val BOOK_ID_SAVED_STATE_KEY = "bookID" } }
Fragment层
@AndroidEntryPoint class BookDetailFragment : Fragment() { private var queryJob: Job? = null private val viewModel: BookDetailViewModel by viewModels() override fun onResume() { super.onResume() load() } private fun load() { queryJob?.cancel() queryJob = lifecycleScope.launch() { val bookName = viewModel.book.value.let { book -> book?.name } binding.toolbar.title = bookName Log.i(TAG, "value: $bookName") } viewModel.book.observe(viewLifecycleOwner) { book -> binding.toolbar.title = book.name Log.i(TAG, "observe: ${book.name}") } } }
补充说明
- Android Jetpack官方最佳实践示例Sunflower中,
LiveData.value(如PlantDetailFragment的createShareIntent方法)可正常工作 getChapters方法返回分页数据Flow<PagingData<Chapter>>,如果让分页数据跟随book数据更新触发刷新,会导致分页重复加载,打乱UI逻辑- 通过DataBinding绑定BookDetailViewModel时,
BookDetailViewModel.book可正常工作,能够获取到book.value的有效值
问题成因
- Room的Flow查询特性:Room返回的
Flow<Book>即使声明为非空类型,当查询记录暂未返回、或首次查询空窗期时会先发射null,编译期不会做非空校验,filterNotNull()只能在数据发射后过滤空值,无法解决初始空值问题。 Flow.asLiveData()的启动规则:asLiveData()生成的LiveData默认是惰性启动的,只有存在活跃观察者时才会开始收集上游Flow,内置5s超时自动取消收集的逻辑。你在lifecycleScope.launch中直接读取value时,LiveData还没注册观察者、未触发Flow收集,值默认就是null;调用observe注册了活跃观察者后才会启动Flow收集,数据返回后自然能通过回调拿到有效值。- DataBinding和Sunflower示例正常的原因:DataBinding会自动给绑定的LiveData注册活跃观察者,在生命周期进入STARTED状态后就会提前触发Flow收集,后续读取
value时数据已经填充完成;Sunflower中读取value的时机都是用户交互触发(比如点击分享按钮),此时LiveData早已完成首次数据加载,自然能拿到有效值。
解决方案
针对不希望分页随book数据重复刷新的需求,优先使用第一种可靠方案:
方案1:挂起等待首次book数据加载(仅初始化一次分页,无重复刷新问题)
利用Flow.first()只获取首次有效book数据,再初始化分页流,不会响应book后续变更触发重复加载:
@HiltViewModel class BookDetailViewModel @Inject internal constructor( savedStateHandle: SavedStateHandle, private val bookRepository: BookRepository, private val chapterRepository: ChapterRepository, ) : ViewModel() { val bookID: Long = savedStateHandle.get<Long>(BOOK_ID_SAVED_STATE_KEY)!! val book = bookRepository.getBook(bookID).asLiveData() // 缓存分页实例,确保只初始化一次 private var chaptersLiveData: LiveData<PagingData<Chapter>>? = null fun getChapters(): LiveData<PagingData<Chapter>> { return chaptersLiveData ?: liveData(viewModelScope.coroutineContext) { // 挂起直到拿到首次非空book数据,不监听后续book变更 val currentBook = bookRepository.getBook(bookID).first() val lastChapterID = currentBook.lastChapterID // 发射分页数据 emitSource(chapterRepository.getChapters(bookID, lastChapterID).asLiveData()) }.also { chaptersLiveData = it } } companion object { private const val BOOK_ID_SAVED_STATE_KEY = "bookID" } }
方案2:预启动LiveData收集(适合简单场景,存在极小概率时序问题)
在ViewModel初始化时主动触发Flow收集,不需要等Fragment注册观察者:
init { // 立即启动Flow收集 book.observeForever { } }
该方案的问题是如果getChapters调用时机早于首次数据返回,仍然会拿到null,稳定性不如方案1。
内容的提问来源于stack exchange,提问作者excing
相关产品推荐
相关产品推荐

