在回购和视图模型中观察到具有 StateFlow 实时变化的 Firestore,但适配器未更新

Firestore with StateFlow real time change is observed in repo and view model, but adapter is not being updated

作为开发人员需要适应变化,我在某处看到它说:

If you don’t choose the right architecture for your Android project, you will have a hard time maintaining it as your codebase grows and your team expands.

我想用 MVVM

实现 Clean Architecture

我的应用数据流将如下所示:

型号class

data class Note(
    val title: String? = null,
    val timestamp: String? = null
)

Dtos

data class NoteRequest(
    val title: String? = null,
    val timestamp: String? = null
)

data class NoteResponse(
    val id: String? = null,
    val title: String? = null,
    val timestamp: String? = null
)

存储库层是

interface INoteRepository {
    fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit)
    fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit)
    fun getNoteList()
    fun deleteNoteById(noteId: String)
}

NoteRepositoryImpl 是:

class NoteRepositoryImpl: INoteRepository {

    private val mFirebaseFirestore = Firebase.firestore
    private val mNotesCollectionReference = mFirebaseFirestore.collection(COLLECTION_NOTES)

    private val noteList = mutableListOf<NoteResponse>()

    private var getNoteListSuccessListener: ((List<NoteResponse>) -> Unit)? = null
    private var deleteNoteSuccessListener: ((List<NoteResponse>) -> Unit)? = null

    override fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit) {
        getNoteListSuccessListener = success
    }

    override fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit) {
        deleteNoteSuccessListener = success
    }

    override fun getNoteList() {

        mNotesCollectionReference
            .addSnapshotListener { value, _ ->
                noteList.clear()
                if (value != null) {
                    for (item in value) {
                        noteList
                            .add(item.toNoteResponse())
                    }
                    getNoteListSuccessListener?.invoke(noteList)
                }

                Log.e("NOTE_REPO", "$noteList")
            }    
    }

    override fun deleteNoteById(noteId: String) {
        mNotesCollectionReference.document(noteId)
            .delete()
            .addOnSuccessListener {
                deleteNoteSuccessListener?.invoke(noteList)
            }
    }
}

ViewModel 层是:

interface INoteViewModel {
    val noteListStateFlow: StateFlow<List<NoteResponse>>
    val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
    fun getNoteList()
    fun deleteNoteById(noteId: String)
}

NoteViewModelImpl 是:

class NoteViewModelImpl: ViewModel(), INoteViewModel {

    private val mNoteRepository: INoteRepository = NoteRepositoryImpl()

    private val _noteListStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
    override val noteListStateFlow: StateFlow<List<NoteResponse>>
        get() = _noteListStateFlow.asStateFlow()

    private val _noteDeletedStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
    override val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
        get() = _noteDeletedStateFlow.asStateFlow()

    init {
         // getNoteListSuccessListener 
        mNoteRepository
            .getNoteListSuccessListener {
                viewModelScope
                    .launch {
                        _noteListStateFlow.emit(it)
                        Log.e("NOTE_G_VM", "$it")
                    }
            }

        // deleteNoteSuccessListener 
        mNoteRepository
            .deleteNoteSuccessListener {
                viewModelScope
                    .launch {
                        _noteDeletedStateFlow.emit(it)
                        Log.e("NOTE_D_VM", "$it")
                    }
            }
    }

    override fun getNoteList() {
        // Get all notes
        mNoteRepository.getNoteList()
    }

    override fun deleteNoteById(noteId: String) {
         mNoteRepository.deleteNoteById(noteId = noteId)
    }
}

最后但并非最不重要的片段是:

class HomeFragment : Fragment() {

    private lateinit var binding: FragmentHomeBinding

    private val viewModel: INoteViewModel by viewModels<NoteViewModelImpl>()
    private lateinit var adapter: NoteAdapter
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {

        binding = FragmentHomeBinding.inflate(inflater, container, false)
        return binding.root

    }

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

        val recyclerView = binding.recyclerViewNotes
        recyclerView.addOnScrollListener(
            ExFABScrollListener(binding.fab)
        )

        adapter = NoteAdapter{itemView, noteId ->
            if (noteId != null) {
                showMenu(itemView, noteId)
            }
        }
        recyclerView.adapter = adapter

        // initView()
        fetchFirestoreData()

        binding.fab.setOnClickListener {
            val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment()
            findNavController().navigate(action)
        }

    }

    private fun fetchFirestoreData() {
        // Get note list
        viewModel
            .getNoteList()

        // Create list object
        val noteList:MutableList<NoteResponse> = mutableListOf()
        // Impose StateFlow
        viewModel
            .noteListStateFlow
            .onEach { data ->
                data.forEach {noteResponse ->
                    noteList.add(noteResponse)
                    adapter.submitList(noteList)
                    Log.e("NOTE_H_FRAG", "$noteResponse")
                }
            }.launchIn(viewLifecycleOwner.lifecycleScope)
    }

    //In the showMenu function from the previous example:
    @SuppressLint("RestrictedApi")
    private fun showMenu(v: View, noteId: String) {
        val menuBuilder = MenuBuilder(requireContext())
        SupportMenuInflater(requireContext()).inflate(R.menu.menu_note_options, menuBuilder)
        menuBuilder.setCallback(object : MenuBuilder.Callback {
            override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
                return when(item.itemId){
                    R.id.option_edit -> {
                        val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment(noteId = noteId)
                        findNavController().navigate(action)
                        true
                    }

                    R.id.option_delete -> {
                        viewModel
                            .deleteNoteById(noteId = noteId)
                        // Create list object
                        val noteList:MutableList<NoteResponse> = mutableListOf()
                        viewModel
                            .noteDeletedStateFlow
                            .onEach {data ->
                                data.forEach {noteResponse ->
                                    noteList.add(noteResponse)
                                    adapter.submitList(noteList)
                                    Log.e("NOTE_H_FRAG", "$noteResponse")
                                }
                            }.launchIn(viewLifecycleOwner.lifecycleScope)
                        true
                    } else -> false
                }
            }

            override fun onMenuModeChange(menu: MenuBuilder) {}
        })
        val menuHelper = MenuPopupHelper(requireContext(), menuBuilder, v)
        menuHelper.setForceShowIcon(true) // show icons!!!!!!!!
        menuHelper.show()

    }
}

根据上述所有逻辑,我面临 TWO 个问题

问题 - 1 如前所述 here,我已将 SnapshotListener 添加到集合中:

override fun getNoteList() {
    mNotesCollectionReference
        .addSnapshotListener { value, _ ->
            noteList.clear()
            if (value != null) {
                for (item in value) {
                    noteList
                        .add(item.toNoteResponse())
                }
                getNoteListSuccessListener?.invoke(noteList)
            }

            Log.e("NOTE_REPO", "$noteList")
        }
}

如果我从 Firebase Console 更改文档的值,我会在 RepositoryViewModel 中获得更新的值,但不会更新传递给的注释列表adapter,所以所有的项目都是一样的。

问题 - 2
如果我使用以下方法从 list/recyclerview 中删除任何项目:

R.id.option_delete -> {
    viewModel
        .deleteNoteById(noteId = noteId)
    // Create list object
    val noteList:MutableList<NoteResponse> = mutableListOf()
    viewModel
        .noteDeletedStateFlow
        .onEach {data ->
            data.forEach {noteResponse ->
                noteList.add(noteResponse)
                adapter.submitList(noteList)
                Log.e("NOTE_H_FRAG", "$noteResponse")
            }
        }.launchIn(viewLifecycleOwner.lifecycleScope)

我仍然在 RepositoryViewModel 中获得更新的列表(即新的笔记列表,不包括已删除的笔记),但笔记列表没有更新,而是传递给 adapter , 所以所有的项目都是一样的,没有和排除已删除的项目。

问题 initialize/update 适配器到底哪里出错了?因为 ViewModelRepository 工作正常。

尝试删除 HomeFragment 片段的 fetchFirestoreData()showMenu()(对于项目 R.id.option_delete)方法中的其他项目列表,看看它是否有效:

// remove `val noteList:MutableList<NoteResponse>` in `fetchFirestoreData()` method

private fun fetchFirestoreData() {
    ...

    // remove this line
    val noteList:MutableList<NoteResponse> = mutableListOf()

    // Impose StateFlow
    viewModel
        .noteListStateFlow
        .onEach { data ->
            adapter.submitList(data)
        }.launchIn(viewLifecycleOwner.lifecycleScope)
}

删除菜单项也是如此(R.id.option_delete)。

进行以下更改: 在 NoteViewModelImplinit{} 块中:

// getNoteListSuccessListener 
mNoteRepository
    .getNoteListSuccessListener{noteResponseList ->
        viewModelScope.launch{
            _noteListStateFlow.emit(it.toList())
        }
    }

如果你想 emit 列表在 StateFlow 中获得更新通知,你必须添加 .toList(),在 HomeFragment

private fun fetchFirestoreData() {
    // Get note list
    viewModel
        .getNoteList()

    // Impose StateFlow
    lifecycleScope.launch {
        viewModel.noteListStateFlow.collect { list ->
            adapter.submitList(list.toMutableList())
        }
    }
}

就这些了,希望一切顺利