0

I am working on an android app using Kotlin that lets a user record details about a vehicle then a list of saved vehicles is shown in a RecyclerView on the home screen. When there are no saved vehicles an empty view with a message is shown.

I have looked at other solutions like the ones on this page How to show an empty view with a RecyclerView? but my problem appears to be with the fragment lifecycle.

The problem I'm having is that when a vehicle is saved and the app returns to the home screen, the vehicle isn't shown unless you leave the app and come back, or try register another vehicle then cancel and go back to the home screen. In addition, when a saved vehicle is deleted, the app returns to the home screen but the empty view isn't shown unless again you leave that page and come back.

I'm using one activity with multiple fragments, a ViewModel to get the data, a ListAdapter and an EmptyDataObserver to check if there is data or not.

Here is a gif of the app

Here is the MainActivity.kt

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Set up view binding
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    // Get the navigation host fragment from this Activity
    val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment

    // Instantiate the navController using the NavHostFragment
    navController = navHostFragment.navController

    // Make sure actions in the ActionBar get propagated to the NavController
    setupActionBarWithNavController(this, navController)
}

/**
 * Enables back button support. Simply navigates one element up on the stack.
 */
override fun onSupportNavigateUp(): Boolean {
    return navController.navigateUp() || super.onSupportNavigateUp()
}
}

The HomeFragment layout

<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
tools:context=".ui.fragments.HomeFragment">

<include
    android:id="@+id/empty_data_parent"
    layout="@layout/empty_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:visibility="gone"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/vehicles_list_recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:visibility="gone"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:listitem="@layout/vehicles_list_item" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:id="@+id/newVehicleFab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="24dp"
    android:layout_marginBottom="24dp"
    android:contentDescription="@string/new_vehicle_fab"
    android:src="@drawable/ic_baseline_add_24"
    android:tintMode="@color/white"
    app:borderWidth="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

The HomeFragment

class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!

private val viewModel: VehiclesViewModel by activityViewModels {
    VehiclesViewModelFactory((activity?.application as CarMaintenanceApplication).database.vehicleDao())
}

private lateinit var vehicleListAdapter: VehicleListAdapter
private lateinit var emptyDataView: View

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    // Retrieve and inflate the layout for this fragment
    _binding = FragmentHomeBinding.inflate(inflater, container, false)
    return binding.root
}

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

    emptyDataView = view.findViewById(R.id.empty_data_parent)

    setupRecyclerView(view)

    binding.newVehicleFab.setOnClickListener {
        val action = HomeFragmentDirections.actionHomeFragmentToVehicleRegistrationFragment(
            getString(R.string.register_vehicle)
        )
        this.findNavController().navigate(action)
    }
}

private fun setupRecyclerView(view: View) {
    vehicleListAdapter = VehicleListAdapter {
        val action = HomeFragmentDirections.actionHomeFragmentToVehicleDetailsFragment(it.id)
        view.findNavController().navigate(action)
    }

    binding.vehiclesListRecyclerView.apply {
        layoutManager = LinearLayoutManager(this.context)
        adapter = vehicleListAdapter
    }

    viewModel.allVehicles.observe(this.viewLifecycleOwner) { vehicles ->
        vehicleListAdapter.submitList(vehicles)
        val emptyDataObserver = EmptyDataObserver(vehicles.isEmpty(), binding.vehiclesListRecyclerView,
            emptyDataView)
        vehicleListAdapter.registerAdapterDataObserver(emptyDataObserver)
    }
}

/**
 * Frees the binding object when the Fragment is destroyed.
 */
override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}
}

The Adapter class

class VehicleListAdapter(private val onItemClicked: (Vehicle) -> Unit) :
ListAdapter<Vehicle, VehicleListAdapter.VehicleViewHolder>(DiffCallback) {
    private lateinit var context: Context

    /**
     * Provides a reference for the views needed to display items in the list.
     */
    class VehicleViewHolder(private var binding: VehiclesListItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(vehicle: Vehicle, context: Context) {
            binding.apply {
                vehicleName.text = context.getString(R.string.vehicle_name, vehicle.manufacturer, vehicle.model)
                vehicleLicense.text = vehicle.licensePlate
                vehicleOdometer.text = vehicle.mileage.toString()
            }
        }
    }

    /**
     * Creates new views with R.layout.vehicles_list_item as its template.
     */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VehicleViewHolder {
        context = parent.context
        val layoutInflater = VehiclesListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    return VehicleViewHolder(layoutInflater)
    }

    /**
     * Replaces the content of an existing view with new data.
     */
    override fun onBindViewHolder(holder: VehicleViewHolder, position: Int) {
        val currentVehicle = getItem(position)
        holder.itemView.setOnClickListener {
            onItemClicked(currentVehicle)
        }
        holder.bind(currentVehicle, context)
    }

    companion object {
         private val DiffCallback = object : DiffUtil.ItemCallback<Vehicle>() {
            override fun areItemsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {
                return oldItem == newItem
            }
        }
    }
}

The EmptyDataObserver

class EmptyDataObserver(
    private val isEmpty: Boolean, private val recyclerView: RecyclerView?, private val emptyView: View?) :
    RecyclerView.AdapterDataObserver() {

    init {
        checkIfEmpty()
    }

    private fun checkIfEmpty() {
        emptyView?.visibility = if (isEmpty) View.VISIBLE else View.GONE

        recyclerView?.visibility = if (isEmpty) View.GONE else View.VISIBLE
    }

    override fun onChanged() {
        super.onChanged()
        checkIfEmpty()
    }

    override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
        super.onItemRangeChanged(positionStart, itemCount)
        checkIfEmpty()
    }

    override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
        super.onItemRangeInserted(positionStart, itemCount)
        checkIfEmpty()
    }

    override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
        super.onItemRangeRemoved(positionStart, itemCount)
        checkIfEmpty()
    }
}

The Navigation graph code

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph.xml"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.pkndegwa.mycarmaintenance.ui.fragments.HomeFragment"
        android:label="@string/app_name"
        tools:layout="@layout/fragment_home">
        <action
            android:id="@+id/action_homeFragment_to_vehicleRegistrationFragment"
            app:destination="@id/vehicleRegistrationFragment" />
        <action
            android:id="@+id/action_homeFragment_to_vehicleDetailsFragment"
            app:destination="@id/vehicleDetailsFragment" />
    </fragment>
    <fragment
        android:id="@+id/vehicleRegistrationFragment"      
    android:name="com.pkndegwa.mycarmaintenance.ui.fragments.VehicleRegistrationFragment"
        android:label="{title}"
        tools:layout="@layout/fragment_vehicle_registration">
        <action
            android:id="@+id/action_vehicleRegistrationFragment_to_homeFragment"
            app:destination="@id/homeFragment"
            app:popUpTo="@id/homeFragment"
            app:popUpToInclusive="true" />
        <argument
            android:name="title"
            app:argType="string" />
        <argument
            android:name="vehicle_id"
            app:argType="integer"
            android:defaultValue="-1" />
    </fragment>
    <fragment
        android:id="@+id/vehicleDetailsFragment"
        android:label="@string/vehicle_details"
        tools:layout="@layout/fragment_vehicle_details">
        <argument
            android:name="vehicle_id"
            app:argType="integer" />
        <action
            android:id="@+id/action_vehicleDetailsFragment_to_vehicleRegistrationFragment"
            app:destination="@id/vehicleRegistrationFragment" />
        <action
            android:id="@+id/action_vehicleDetailsFragment_to_homeFragment"
            app:destination="@id/homeFragment"
            app:popUpTo="@id/homeFragment"
            app:popUpToInclusive="true"/>
    </fragment>
</navigation>

an image of the navGraph

What mistake I'm I making?

Edit - I got a solution. In the DAO file, the return type for getting a list of vehicles was a Flow object. When I changed it to LiveData it seems to work fine. I don't understand why yet because in the Android developer codelabs it's recommended to use Flow as the return type from database queries. So, the previous query was

@Query("SELECT * FROM vehicles ORDER BY manufacturer ASC")
 fun getAllVehicles(): Flow<List<Vehicle>>

And I changed it to

@Query("SELECT * FROM vehicles ORDER BY manufacturer ASC")
fun getAllVehicles(): LiveData<List<Vehicle>>
  • The other question has answers that are just fine. You may not notify the `RecyclerView.Adapter`, that the data has changed. This may be useful: https://developer.android.com/guide/fragments/communicate#fragment-result – Martin Zeitler Aug 19 '22 at 07:35
  • On coming back its it coming inside observer? viewModel.allVehicles.observe – Gowtham K K Aug 19 '22 at 09:00
  • @GowthamKK Yes it's coming back inside observer to check for any changes in the data – Peter Kingori Aug 19 '22 at 09:16
  • In observer you can call this method, adapter.notifyItemInserted(items.size-1) . If it doesn't workout , you can try calling adapter.notifyDataSetChanged() in observer. – Gowtham K K Aug 19 '22 at 09:26

0 Answers0