【问题标题】:Android Flow-backed firestore realtime listener gets canceled on configuration changeAndroid Flow 支持的 Firestore 实时侦听器在配置更改时被取消
【发布时间】:2021-10-30 05:14:19
【问题描述】:

项目结构

我的目标是构建一个实时的 firebase (firestore) 笔记应用程序(用 Kotlin 编写)。 该应用程序仅显示笔记列表,并与服务器实时同步。

我正在使用 MVVM 模式和 Firestore 数据库。

应用的骨架包含:

  • NoteRepository 连接到 firebase 并获取笔记并监听实时文档更改;
  • NotesFragmentViewModel 通过Flow 向视图公开注释(以及后续注释更新);
  • NoteActivity 只是一个容器(没有附加视图模型);
  • NoteFragment,包含在 NoteActivity 中,并观察自己的 ViewModel (NotesFragmentViewModel)。

我没有在任何地方使用LiveData,而是使用Flow

到目前为止一切正常,应用程序在启动时获取注释,当我从服务器进行更改时,我会获得实时应用程序 UI 更新。

代码

存储库

class NoteRepository {

    @ExperimentalCoroutinesApi
    fun notesFlow(): Flow<Result<List<Note>>> = callbackFlow {
        trySend(Result.Loading)

        val subscription = firebaseFirestore
                .collection("notes")
                .addSnapshotListener { value, error ->
                    // Real-time observer
                    if (error != null) {
                        trySend(Result.Error)
                    } else {
                        value?.let {
                            val data = it.toObjects(Note::class.java)
                            trySend(Result.Success(data))
                        }
                    }
                }
    
        // Suspends until the flow is not used anymore
        awaitClose {
            // Dismisses real time listener
            subscription.remove()
        }
    }
}

视图模型

@HiltViewModel
@ExperimentalCoroutinesApi
class NotesFragmentViewModel @Inject constructor(
    val repository: NoteRepository,
) : ViewModel() {

    private val _notes = repository
        .notesFlow()

    val notes: Flow<Result<List<Note>>>
        get() = _notes
            .onStart { emit(Result.Loading) }

}

片段

@ExperimentalCoroutinesApi
@AndroidEntryPoint
class NoteListFragment : Fragment() {

    private val viewModel: NotesFragmentViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return inflater.inflate(R.layout.main_fragment, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Consume data when fragment is started
        viewLifecycleOwner.lifecycleScope.launch {

            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
                // Since collect is a suspend function it needs to be called
                // from a coroutine scope
                viewModel.notes.collect {
                    when (it) {
                        Result.Loading -> {
                            Toast.makeText(context, "Loading...", Toast.LENGTH_SHORT).show()
                        }
                        is Result.Success -> {
                            val data = it.data
                            Toast.makeText(context, "Got the notes!", Toast.LENGTH_LONG).show()
                            
                            //
                            // Display the data on the UI
                            //
                        }
                        Result.Error -> {
                            Toast.makeText(context, "Error", Toast.LENGTH_SHORT).show()
                        }
                    }
                }
            }
        }
    }
}

结果

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    object Loading : Result<Nothing>()
    object Error : Result<Nothing>()
}

轮换(配置更改)问题

设备旋转时会出现问题。 (或者派发任何配置更改,这会破坏 Activity 和 Fragment)

方向改变后,重新创建活动,重新创建片段,令我惊讶的是出于某种原因再次从服务器获取文档。

确实,当我打开应用程序时,我看到一个“正在加载”吐司,然后是“得到笔记!”,然后当我旋转设备时,我仍然看到“正在加载”和“得到笔记!”。就像视图模型也被破坏,应用程序被杀死并重新打开一样。

这(我想)是非常错误和不可接受的,因为如果不使用 MVVM/Flow,我会得到相同的行为。重点是在 ViewModel 层中确保数据安全无虞。

我希望底层的Flow(在存储库层中声明,并在 ViewModel 中公开)保持活动状态。相反,我认为当没有人“观看”它时,它会被取消,然后在下次从它收集片段时重新创建。

我正在尝试找到一种方法来使实时观察者(firebase 快照侦听器)即使在旋转更改之后也保持活跃。或者至少不要在旋转更改后重新获取所有文档并以某种方式将它们缓存在 ViewModel 中。

我以存储库LiveDataWithFlow 为基础,以及文章A Safer Way to Collect FlowsFlows with Firestore

编辑 1:

也许我应该使用 LiveData 将 ViewModel 和 Fragment 或 StateFlow 或其他东西粘合在一起。这种方法合适吗?

编辑 2:

通过使用 LiveData 将 ViewModel 与 View 连接起来,一切都按预期工作,防旋转。不过,我不完全理解您应该如何处理仅限 Flow 的场景,以及有哪些优势。

【问题讨论】:

  • 我要改变的是片段应该从 VM 观察(LiveData 或 StateFlow),而不是对它是否成功进行收集/决策。观察onViewCreated 中的流,让 ViewModel 与 repo 对话。为您的 UI 公开一个更好的 Sealed 类,其中包括注释列表(如果您获得单镜头列表)并让 VM 处理“结果”。这也为您提供了在 VM 内随意转换结果的空间。不过,这并不能解释重新获取。我们一定遗漏了一些东西,因为它看起来很简单。我还没怎么用流量呢。
  • @MartinMarconcini 你能看看这个question吗?

标签: android firebase kotlin google-cloud-firestore kotlin-coroutines


【解决方案1】:

您的代码的问题在于您正在从生命周期范围调用所有操作,这意味着所有流程都将在轮换更改时触发,因为这是生命周期范围。要完成您想要的操作并在旋转更改时维护信息,您必须使其依赖于视图模型范围。

这是一个外观示例:

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is 
               cleared.
        }
    }
}

有关更多信息,您可以访问以下几个链接以更好地理解它: https://developer.android.com/topic/libraries/architecture/coroutines#lifecycle-aware https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-07-05
    • 1970-01-01
    • 2020-11-11
    • 2020-01-07
    • 1970-01-01
    • 2018-08-08
    • 2018-03-16
    • 2020-12-24
    相关资源
    最近更新 更多