Arshad Ali
Arshad Ali

Reputation: 3274

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

As a developer one needs to adapt to change, I read somewhere it says:

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.

I wanted to implement Clean Architecture with MVVM

My app data flow will look like this:

OneNote_Clean_Architecture_MVVM_Data_Flow

Model class

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

Dtos

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

and

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

Repository layer is

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

NoteRepositoryImpl is:

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 layer is:

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

NoteViewModelImpl is:

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)
    }
}

and last but not least Fragment is:

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()

    }
}

With all the above logic I'm facing TWO issues

issue - 1 As mentioned here, I have added SnapshotListener on collection as:

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")
        }
}

with it if I change values of a document from Firebase Console, I get updated values in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same.

issue - 2
If I delete any item from list/recyclerview using:

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)

still I get updated list(i.e new list of notes excluding deleted note) in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same, no and exclusion of deleted item.

Question Where exactly I'm making mistake to initialize/update adapter? because ViewModel and Repository are working fine.

Upvotes: 2

Views: 875

Answers (2)

Crazy Coder
Crazy Coder

Reputation: 804

Make following changes: In init{} block of NoteViewModelImpl :

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

you must add .toList() if you want to emit list in StateFlow to get notified about updates, and in HomeFragment

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

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

That's it, I hope it works fine.

Upvotes: 1

Sergio
Sergio

Reputation: 30595

Try to remove additional lists of items in the fetchFirestoreData() and showMenu() (for item R.id.option_delete) methods of the HomeFragment fragment and see if it works:

// 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)
}

And the same for the delete menu item (R.id.option_delete).

Upvotes: 0

Related Questions