How can I find NavController if I use view binding? - android

Normally, I use val navController: NavController = findNavController(R.id.nav_host_fragment) in Code A to find NavController, it's based R.id.nav_host_fragment.
Now I use view binding in the app just like Code B, how can I NavController if I use view binding ?
BTW, in my mind R.id.nav_host_fragment will not be available in view binding , right?
Code A
class TasksActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.tasks_act)
val navController: NavController = findNavController(R.id.nav_host_fragment)
}
}
Code B
class TasksActivity : AppCompatActivity() {
private lateinit var binding: TasksActBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = TasksActBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
//val navController: NavController = findNavController(R.id.nav_host_fragment)
}
}
tasks_act.xml
<androidx.drawerlayout.widget.DrawerLayout
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/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".tasks.TasksActivity"
tools:openDrawer="start">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="#style/Toolbar"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light" />
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</LinearLayout>
..
</androidx.drawerlayout.widget.DrawerLayout>

The code B should still be working fine .
I made a look into findNavController() . This is an extension function for simpifying the code , the code for extension function is
fun Activity.findNavController(#IdRes viewId: Int): NavController =
Navigation.findNavController(this, viewId)
Now looking in to the code of findNavController() inside Navigation we see below
#NonNull
public static NavController findNavController(#NonNull Activity activity, #IdRes int viewId) {
View view = ActivityCompat.requireViewById(activity, viewId);
NavController navController = findViewNavController(view);
if (navController == null) {
throw new IllegalStateException("Activity " + activity
+ " does not have a NavController set on " + viewId);
}
return navController;
}
The viewId we are passing in argument is used in the first line
View view = ActivityCompat.requireViewById(activity, viewId);
now looking into requireViewById() inside ActivityCompat we see
#NonNull
public static <T extends View> T requireViewById(#NonNull Activity activity, #IdRes int id) {
if (Build.VERSION.SDK_INT >= 28) {
return activity.requireViewById(id);
}
T view = activity.findViewById(id);
if (view == null) {
throw new IllegalArgumentException("ID does not reference a View inside this Activity");
}
return view;
}
for api 28 and plus the method which gets called is
#NonNull
public final <T extends View> T requireViewById(#IdRes int id) {
T view = findViewById(id);
if (view == null) {
throw new IllegalArgumentException("ID does not reference a View inside this Activity");
}
return view;
}
So as long as the view (inside which the nav_host_fragment is present) is attached to activity the code which you wrote for finding the nav controller should work completely fine .
class TasksActivity : AppCompatActivity() {
private lateinit var binding: TasksActBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = TasksActBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view) //you are attaching view to activity here , make sure you always call this before the next line , else you will get IllegalStateException
val navController: NavController = findNavController(R.id.nav_host_fragment)
}
}
I have not tested the code personally but from what I see it should work perfectly fine.

We are using ViewBinding to get a reference of view itself. If you are using viewbinding in TextView, you will get a reference of the View.
In the case of NavCotroller, If you use Viewbinding, you will get a reference to a fragment, while findNavController expect ViewId of type integer.
findNavController(#IdRes viewId: int)

The correct working way is as follows:
FragmentContainerView has to be accessed from the supportFragmentManager:
val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment
val navController = navHostFragment.navController

Related

How now to create Option menu in Fragment (setHasOptionMenu is deprecated)

When trying to write setHasOptionsMenu(true) in onCreate and override fun onCreateOptionsMenu as usual, Android Studio crosses out these functions saying that they are deprecated.
I looked at what they suggest
https://developer.android.com/jetpack/androidx/releases/activity?authuser=5#1.4.0-alpha01
and it turns out that they are asking to insert some new functions in Activity (MainActivity.kt) and some in Fragment (DogListFragment.kt). But in my app, all menu customization was done only in Fragment, so Activity can't do that. Activity simply doesn't have access to the RecyclerView, which is in the layout (fragment_god_list.xml) that belongs to Fragment. Activity only has androidx.fragment.app.FragmentContainerView in its activity_main.xml
Does anyone know how this can be done in Fragment without having to do anything with the menus in Activity?
GitHub project: https://github.com/theMagusDev/DogglersApp
MainActivity.kt:
package com.example.dogglers
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
import com.example.dogglers.databinding.ActivityMainBinding
private lateinit var navController: NavController
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Setup view binding
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Setup navController
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
setupActionBarWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
DogListFragment.kt:
package com.example.dogglers
import android.os.Bundle
import android.view.*
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.dogglers.adapter.DogCardAdapter
import com.example.dogglers.const.Layout
import com.example.dogglers.databinding.FragmentDogListBinding
class DogListFragment : Fragment() {
private var _binding: FragmentDogListBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private lateinit var recyclerView: RecyclerView
private var layoutType = Layout.VERTICAL
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDogListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recyclerView = binding.verticalRecyclerView
setUpAdapter()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.layout_manu, menu)
val layoutButton = menu.findItem(R.id.action_switch_layout)
// Calls code to set the icon
setIcon(layoutButton)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_switch_layout -> {
layoutType = when (layoutType) {
Layout.VERTICAL -> Layout.HORIZONTAL
Layout.HORIZONTAL -> Layout.GRID
else -> Layout.VERTICAL
}
setUpAdapter()
return true
}
// Otherwise, do nothing and use the core event handling
// when clauses require that all possible paths be accounted for explicitly,
// for instance both the true and false cases if the value is a Boolean,
// or an else to catch all unhandled cases.
else -> return super.onOptionsItemSelected(item)
}
}
fun setUpAdapter() {
recyclerView.adapter = when(layoutType){
Layout.VERTICAL -> {
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
DogCardAdapter(
context,
Layout.VERTICAL
)
}
Layout.HORIZONTAL -> {
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
DogCardAdapter(
context,
Layout.HORIZONTAL
)
}
else -> {
recyclerView.layoutManager = GridLayoutManager(context, 2, RecyclerView.VERTICAL, false)
DogCardAdapter(
context,
Layout.GRID
)
}
}
}
private fun setIcon(menuItem: MenuItem?) {
if (menuItem == null)
return
menuItem.icon = when(layoutType) {
Layout.VERTICAL -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_vertical_layout)
Layout.HORIZONTAL -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_horizontal_layout)
else -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_grid_layout)
}
}
}
ActivityMain.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph"/>
</FrameLayout>
FragmentDogList:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/vertical_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/vertical_list_item"/>
</FrameLayout>
When you call setUpActionBarWithNavController() method , you are setting up toolbar inside the activity. Your Fragment is inside this activity. Your fragment has this actionBar too. To use Menu provider inside fragment, you need to call below method inside of onViewCreated() method of fragment.
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
Also, You need to make your fragment implement MenuProvider Interface
class DogListFragment : Fragment(),MenuProvider {...
IDE will ask you to implement its provider method i.e onCreateMenu and onMenuItemSelected
Inside OnCreateMenu, use menu Inflator to inflate menu layout
example:-
menuInflater.inflate(R.menu.search_menu,menu)

Android: Problem showing an empty view or data on a recyclerview (probably because of lifecycle)

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

Changing toolbar title in each fragment using MVVM and Databinding

I am looking to change the toolbar title,which is in my main activity, in my fragments page. My project is based on MVVM Architecture, with databinding.
This is my main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="viewModel"
type="com.lalsoft.toolbar_mvvm_databinding.viewmodel.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:elevation="0dp"
android:theme="#style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:minHeight="?attr/actionBarSize"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:titleTextColor="#android:color/white"
app:navigationIcon="#drawable/ic_arrow_back"
android:background="?attr/colorPrimary"
app:popupTheme="#style/AppTheme.PopupOverlay"
app:navigationOnClickListener="#{()->viewModel.navBackClicked()}"
app:title="#{viewModel.toolbarTitle}"/>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
This is my mainActivity.kt
private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var dataBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
//setSupportActionBar(dataBinding.toolbar)
//dataBinding.toolbar.setNavigationIcon(R.drawable.ic_arrow_back)
viewModel.navClicked.observe(this, navClickObserver)
viewModel.toolbarTitle.observe(this, toolbarTitleObserver)
dataBinding.viewModel = viewModel
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction().replace(
R.id.fragment_container,
FirstFragment()
).commit()
}
}
private val navClickObserver = Observer<Boolean> {
supportFragmentManager.popBackStack()
Log.e(TAG, "Nav Back clicked")
}
private val toolbarTitleObserver = Observer<String> {
Log.e(TAG, "Title set : $it")
}
}
And this is my MainViewModel
private const val TAG = "MainViewModel"
open class MainViewModel : ViewModel() {
val toolbarTitle: MutableLiveData<String> = MutableLiveData()
private val _navClicked: MutableLiveData<Boolean> = MutableLiveData()
val navClicked: LiveData<Boolean> = _navClicked
init {
Log.e(TAG, "Inside Init")
//toolbarTitle.value ="Main Activity"
}
fun navBackClicked() {
_navClicked.value = true
}
}
Now i am trying to change the toolbar title in FragmentViewModel by changing the mutable toolbarTitle of my mainActivityViewModel.
private const val TAG = "FirstViewModel"
class FirstViewModel : MainViewModel() {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToFragment: LiveData<Event<String>>
get() = _navigateToDetails
init {
Log.e(TAG, "Inside Init")
toolbarTitle.value="First Fragment"
}
fun onBtnClick() {
_navigateToDetails.value = Event("Second Fragment")
}
}
This is my fragment class
private const val TAG = "FirstFragment"
class FirstFragment : Fragment() {
private lateinit var viewModel: FirstViewModel
private lateinit var dataBinding: FirstFragmentBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
dataBinding = DataBindingUtil.inflate(inflater, R.layout.first_fragment, container, false)
return dataBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(FirstViewModel::class.java)
viewModel.toolbarTitle.observe(viewLifecycleOwner, toolbarTitleObserver)
viewModel.navigateToFragment.observe(viewLifecycleOwner, navigateToFragmentObserver)
//(activity as MainActivity?)!!.toolbar.title = "Check"
dataBinding.viewModel = viewModel
}
private val toolbarTitleObserver = Observer<String> {
Log.e(TAG, "Title set : $it")
//(activity as MainActivity?)!!.toolbar.title = "Check"
//Log.e(TAG, "Title set : Check")
}
private val navigateToFragmentObserver = Observer<Event<String>> { it ->
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
Log.i(TAG, "checkIt string $it")
parentFragmentManager.beginTransaction().replace(
R.id.fragment_container,
SecondFragment()
).addToBackStack(null).commit()
}
}
}
Eventhough its observing the toolbarTitle correctly,the Title in my program is not changing..
Hope to get some help to get out of this issue.
This is my sample git project where i am trying to do this : github
I struggle with the same!
I think the problem is the lifecycleowner. But could not find an answer.
Currently I observe the values from the the ViewModel and assign the values inside the observer.
But I think there is a better way!
If your Fragment is using a ViewModel should be scoped to a host Activity, use by activityViewModels() delegate:
#AndroidEntryPoint
class HomeFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
}
I think also this answer will help.
https://stackoverflow.com/a/62560605
When you use by viewModels, you are creating a ViewModel scoped to that individual Fragment - this means each Fragment will have its own individual instance of that ViewModel class. If you want a single ViewModel instance scoped to the entire Activity, you'd want to use by activityViewModels

How to create a custom fragment that extends NavHostFragment with it's own back stack?

I'm looking for a generic way to create my custom fragment with that has OnBackPressedCallback and viewModel that extends NavHostFragment using navigation graph i intend to put as child fragments into it's back stack.
Normally i create NavHostFragment for each tab or fragment with their FragmentContainerView, it's easy but repetitive to create for each host with
fragment_nav_host_home.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nested_nav_host_fragment_home"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="false"
app:navGraph="#navigation/nav_graph_home"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
and writing databinding for their layouts, setting and using id nested_nav_host_fragment_home
class HomeNavHostFragment : BaseDataBindingFragment<FragmentNavhostHomeBinding>() {
override fun getLayoutRes(): Int = R.layout.fragment_navhost_home
private val appbarViewModel by activityViewModels<AppbarViewModel>()
private var navController: NavController? = null
private val nestedNavHostFragmentId = R.id.nested_nav_host_fragment_home
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val nestedNavHostFragment =
childFragmentManager.findFragmentById(nestedNavHostFragmentId) as? NavHostFragment
navController = nestedNavHostFragment?.navController
// Listen on back press
listenOnBackPressed()
}
private fun listenOnBackPressed() {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
}
override fun onResume() {
super.onResume()
callback.isEnabled = true
// Set this navController as ViewModel's navController
appbarViewModel.currentNavController.value = navController
}
override fun onPause() {
super.onPause()
callback.isEnabled = false
}
val callback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
// Check if it's the root of nested fragments in this navhost
if (navController?.currentDestination?.id == navController?.graph?.startDestination) {
/*
Disable this callback because calls OnBackPressedDispatcher
gets invoked calls this callback gets stuck in a loop
*/
isEnabled = false
requireActivity().onBackPressed()
isEnabled = true
} else {
navController?.navigateUp()
}
}
}
}
Instead i tried to write more generic class for creating NavHostFragment directly instead of putting it inside another fragment
class BaseNavHostFragment private constructor() : NavHostFragment() {
private val appbarViewModel by activityViewModels<AppbarViewModel>()
companion object {
fun create(
#NavigationRes navGraphId: Int,
startDestinationArgs: Bundle? = null
): BaseNavHostFragment {
return NavHostFragment.create(navGraphId, startDestinationArgs) as BaseNavHostFragment
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listenOnBackPressed()
}
private fun listenOnBackPressed() {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
}
override fun onResume() {
super.onResume()
callback.isEnabled = true
// Set this navController as ViewModel's navController
appbarViewModel.currentNavController.value = navController
}
override fun onPause() {
super.onPause()
callback.isEnabled = false
}
/**
* This callback should be created with Disabled because on rotation ViewPager creates
* NavHost fragments that are not on screen, destroys them afterwards but it might take
* up to 5 seconds.
*
* ### Note: During that interval touching back button sometimes call incorrect [OnBackPressedCallback.handleOnBackPressed] instead of this one if callback is **ENABLED**
*/
private val callback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
// Check if it's the root of nested fragments in this nav host
if (navController?.currentDestination?.id == navController?.graph?.startDestination) {
/*
Disable this callback because calls OnBackPressedDispatcher
gets invoked calls this callback gets stuck in a loop
*/
isEnabled = false
requireActivity().onBackPressed()
isEnabled = true
} else {
navController?.navigateUp()
}
}
}
}
When i call create function it returns ClassCastException. Any way to create a NavHostFragment only passing R.navitation.x is simple way for creating to add or replace with fragment manager or ViewPager2, but couldn't find how to create a fragment this way.

onClick not triggering associated function - kotlin

EDIT - I tried to set android:focusable and android:clickable attributes to true in the XML, but it didn't changed anything. Still looking!
I can't understand why my onClick handler isn't working on this particular case.
I want to make a CardView clickable, using databinding. I've seen a lot of code for onClickListener, but i decided to use this pattern, as described in the Android doc.
I have a class, named Recipe(), which contains 3 elements : an ID, a Title and an URL. The ultimate goal of this click handler is to navigate to a new fragment, passing the ID as a parameter to display new elements. I'll later get the ID in the Fragment using observer.
In this example, I already use data provided by the ViewModel in the XML (recipe.image and recipe.title), and it works just fine: data is correctly binded and displayed.
However, clicking on the CardView don't result in anything: as the Log isn't display, I suppose clicking don't trigger the onClick event.
You'll find element from the ViewModel and XML below. Thanks in advance!
class DefaultRecipeListViewModel : ViewModel() {
private val _recipeList = MutableLiveData<List<Recipe>>()
val recipeList: LiveData<List<Recipe>>
get() = _recipeList
//Defining coroutine
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main )
//Livedata observed in the Fragment
private var _navigateToRecipe = MutableLiveData<Int>()
val navigateToRecipe: LiveData<Int>
get() = _navigateToRecipe
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
init {
getRecipesForNewbie()
}
fun getRecipesForNewbie() {
coroutineScope.launch {
var getRecipes = service.getRecipe().await()
try {
_recipeList.value = getRecipes.results
} catch (e: Exception) {
Log.i("ViewModel","Error: $e")
}
}
}
fun onRecipeClicked(id: Int) {
_showSnackbarEvent.value = true
_navigateToRecipe.value = id
Log.i("ViewModel", "Item clicked, id: $id")
}
fun doneNavigating(){
Log.i("ViewModel", "done navigating, navigateToRecipe set to -1")
_navigateToRecipe.value = -1
}
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Here's the XML
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="recipe"
type="com.example.recipesfornewbies.recipes.Recipe" />
<variable
name="viewModel"
type="com.example.recipesfornewbies.defaultrecipelist.DefaultRecipeListViewModel" />
</data>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<androidx.cardview.widget.CardView
android:id="#+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
app:cardCornerRadius="10dp"
app:cardElevation="6dp"
android:onClick="#{()-> viewModel.onRecipeClicked(recipe.id)}">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/recipe_image"
android:layout_width="match_parent"
android:layout_height="100dp"
android:contentDescription="#{recipe.title}"
app:imageFromUrl="#{recipe.image}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="0dp" />
<TextView
android:id="#+id/recipe_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="#{recipe.title}"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="#id/recipe_image"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</layout>
Fragment code:
class DefaultRecipeListFragment: Fragment(){
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentDefaultRecipeListBinding.inflate(inflater)
val viewModel: DefaultRecipeListViewModel by lazy {
ViewModelProviders.of(this).get(DefaultRecipeListViewModel::class.java)
}
binding.viewModel = viewModel
// Allows Data Binding to Observe LiveData with the lifecycle of this Fragment
binding.setLifecycleOwner(this)
//Creating the RecyclerView
val manager = LinearLayoutManager(activity)
binding.recyclerRecipeList.layoutManager = manager
binding.recyclerRecipeList.adapter = RecipeListAdapter()
viewModel.navigateToRecipe.observe(this, Observer{id ->
if (id != -1){
Log.i("Fragment","Navigate to ${id}")
}
viewModel.doneNavigating()
})
viewModel.showSnackBarEvent.observe(this, Observer {
if (it == true) {
Snackbar.make(
activity!!.findViewById(android.R.id.content),
"Clicked!",
Snackbar.LENGTH_SHORT
).show()
viewModel.doneShowingSnackbar()
}
})
return binding.root
}
}
ViewHolder class, used in the RecyclerView Adapter
class RecipeViewHolder(private var binding: RecipeViewBinding):
RecyclerView.ViewHolder(binding.root) {
fun bind(Recipe: Recipe) {
val imageURI = "https://spoonacular.com/recipeImages/"
Recipe.image = imageURI + Recipe.image
binding.recipe = Recipe
// Forces the data binding to execute immediately,to correctly size RecyclerVieW
binding.executePendingBindings()
}
I finally decided to use this solution on my code, which works pretty well.
I stopped trying to access the function in my ViewModel directly from the XML: I actually listen for event on the Fragment, even if I find the solution to be less pretty that the one I wanted.
In the Fragment, this is how I handle the click:
binding.recyclerRecipeList.addOnItemTouchListener(RecyclerItemClickListener(this.context!!,
binding.recyclerRecipeList, object : RecyclerItemClickListener.OnItemClickListener {
override fun onItemClick(view: View, position: Int) {
viewModel.recipeList.value?.let {
val ident = it[position].id
findNavController().navigate(
DefaultRecipeListFragmentDirections.actionDefaultRecipeListFragmentToDetailedRecipeFragment(ident))
Log.i("Fragment", "id: $ident")
}
}
override fun onItemLongClick(view: View?, position: Int) {
TODO("do nothing")
}
}))
Still haven't understood why my first solution didn't work.

Categories

Resources