gawron103
gawron103

Reputation: 309

Firebase recycler view delete items in MVVM architecture

I have weird situation with recycler view.

This is my fragment, which has recycler view:

class CarsListFragment : Fragment() {

    private var _binding: FragmentCarsListBinding? = null
    private val binding get() = _binding!!

    private lateinit var carsListViewModel: CarsListViewModel
    private lateinit var carsAdapter: CarsListAdapter

    private val swipeCallback: ItemTouchHelper.SimpleCallback =
        object: ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT) {
            override fun onMove(
                recyclerView: RecyclerView,
                viewHolder: RecyclerView.ViewHolder,
                target: RecyclerView.ViewHolder
            ): Boolean {
                return false
            }

            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                carsAdapter.deleteCar(viewHolder.adapterPosition)
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        carsListViewModel = ViewModelProvider(requireActivity(),
            CarsListViewModelFactory(
                CarsListRepository()
            )
        ).get(CarsListViewModel::class.java)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentCarsListBinding.inflate(inflater, container, false)

        binding.btnAddCar.setOnClickListener {
            findNavController().navigate(CarsListFragmentDirections.actionCarsListFragmentToAddCarFragment())
        }

        carsAdapter = CarsListAdapter(::adapterDeleteCarCallback)
        binding.rvCars.apply {
            layoutManager = LinearLayoutManager(requireContext())
            adapter = carsAdapter
        }.also {
            val itemTouchHelper = ItemTouchHelper(swipeCallback)
            itemTouchHelper.attachToRecyclerView(it)
        }

        observeCarsData()

        return binding.root
    }

    override fun onStart() {
        super.onStart()
        carsListViewModel.attachListener()
    }

    override fun onStop() {
        super.onStop()
        carsListViewModel.detachListener()
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }

    private fun observeCarsData() {
        carsListViewModel.addedCar.observe(viewLifecycleOwner, { data ->
            Log.d("CarsVM", "Car object added")
            Log.d("CarsVM", "Car: $data")
            data?.let {
                carsAdapter.addCar(it)
            }
        })
    }

    private fun adapterDeleteCarCallback(id: String) {
        carsListViewModel.removeCar(id)
    }

This is my view model:

class CarsListViewModel(
    private val repository: CarsListRepository
): ViewModel() {

    val addedCar = repository.addedCar

    fun removeCar(id: String) = repository.removeCar(id)

    fun attachListener() = repository.addListener()

    fun detachListener() = repository.removeListener()

}

This is my repository:

object CarsListRepository {

    private val _firebaseAuth: FirebaseAuth by lazy { FirebaseAuth.getInstance() }
    private var _dbCarsListReference: DatabaseReference = FirebaseDatabase.getInstance().getReference("users")
        .child(_firebaseAuth.currentUser!!.uid)
        .child("cars")

    private var _addedCar =  MutableLiveData<Car>()
    val addedCar: LiveData<Car> get() = _addedCar

    private val userCarsListener = object: ChildEventListener {
        override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
            Log.d("Repo", "added car. Response: ${snapshot}")
            snapshot.getValue(Car::class.java)?.let { car -> _addedCar.value = car }
        }

        override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
            TODO("Not yet implemented")
        }

        override fun onChildRemoved(snapshot: DataSnapshot) {
            Log.d("Repo", "deleted car. Response: ${snapshot}")
        }

        override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
            TODO("Not yet implemented")
        }

        override fun onCancelled(error: DatabaseError) {
            TODO("Not yet implemented")
        }
    }

    operator fun invoke(): CarsListRepository {
        return this
    }

    fun removeCar(carId: String) {
        Log.d("Repo", "Passed ID: $carId")
        _dbCarsListReference.child(carId).removeValue()
    }

    fun addListener() {
        Log.d("Repo", "Listener added")
        _dbCarsListReference.addChildEventListener(userCarsListener)
    }

    fun removeListener() {
        Log.d("Repo", "Listener removed")
        _dbCarsListReference.removeEventListener(userCarsListener)
    }

}

And this is my recycler view adapter:

class CarsListAdapter(
    private val carDeleteCallback: (id: String) -> Unit
): RecyclerView.Adapter<CarsListAdapter.CarViewHolder>() {

    private val data = mutableListOf<Car>()

    class CarViewHolder(private val binding: ItemCarBinding): RecyclerView.ViewHolder(binding.root) {
        fun bind(car: Car) {
            binding.tvCarName.text = car.name
            binding.tvCarBrand.text = car.brand
            binding.tvCarModel.text = car.model
            binding.tvCarPlates.text = if(car.plates.isEmpty()) "----" else car.plates
        }
    }

    fun addCar(car: Car) {
        if(!data.contains(car)) {
            data.add(car)
            notifyItemInserted(data.size)
        }
    }

    fun deleteCar(position: Int) {
        carDeleteCallback(data[position].id)
        data.remove(data[position])
        notifyItemRemoved(position)
        notifyItemRangeChanged(position, itemCount)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CarViewHolder {
        val itemBinding = ItemCarBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return CarViewHolder(itemBinding)
    }

    override fun onBindViewHolder(holder: CarViewHolder, position: Int) {
        holder.bind(data[position])
    }

    override fun getItemCount(): Int = data.size
}

So basically I have recycler view adapter, to which I have added item touch helper for deleting items using swipe. Problem is that items are presented in weird order - when I am navigating to different fragment and then goes to this one again - order is different. No idea why... Also I believe that I should be deleting items in firebase using some different approach. Probably I should call callback in adapter, call delete method in repository and then when onChildRemoved is called, remove item from recycler view?

Hope someone knows if this approach is correct according to the MVVM architecture or if there is something better (examples, git repos would be great).

Thanks!

Upvotes: 0

Views: 251

Answers (1)

Frank van Puffelen
Frank van Puffelen

Reputation: 599541

Probably I should call callback in adapter, call delete method in repository and then when onChildRemoved is called, remove item from recycler view?

That is indeed the normal approach here: your execute the action on the database, which then reactively updates your application state and user interface. This is sometimes also referred to as CQRS: Command Query Responsibility Segregation.

An added bonus here is that the Firebase SDK immediately fires local events for local write operations, such as onChildAdded when you add a child node. This means you won't need to wait for the database to respond, and it even works when there's no database connection. In the (less common) situation that the database (typically your security rules) reject the writes, the SDK will fire so-called reconciliation events, such as a onChildRemoved, to ensure your UI can show the correct state from the database.

Upvotes: 2

Related Questions