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