2

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.

Arshad Ali
  • 3,082
  • 12
  • 56
  • 99
  • 1
    Have you visited this [SO Link](https://stackoverflow.com/questions/66168609/listadapter-diff-does-not-dispatch-updates-on-same-list-instance-but-neither-on) – Crazy Coder Mar 03 '22 at 12:25
  • 1
    @CrazyCoder yes I have tried so many changes including answers in the link you suggested, but `in vain`, when I change orientation or move back and forth in fragments adapter list is updated. I want that change must show its impact without need of changing orientation or other tricks, because when I delete any item that should disappear from recyclerview, in my case it still remains there :-( – Arshad Ali Mar 03 '22 at 12:31
  • What is adapter.submitList(noteList)? submitList method does what? – Jinal Patel Mar 12 '22 at 13:24
  • Can you provide the code of adapter class? – Sergio Mar 12 '22 at 13:39
  • if you are submitting new list everytime to adapter on data change... are you calling notifydatasetchanged() on adapter? – MRamzan Mar 13 '22 at 23:49

2 Answers2

1

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.

Crazy Coder
  • 784
  • 2
  • 9
  • 24
0

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

Sergio
  • 27,326
  • 8
  • 128
  • 149