【发布时间】: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 Flows 和Flows 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