View binding of a frgament inside a constraint layout not working - android

I have the following code:
activity_reminders.xml
<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"
tools:context=".locationreminders.RemindersActivity">
<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" />
</androidx.constraintlayout.widget.ConstraintLayout>
ReminderActivity.kt
class RemindersActivity : AppCompatActivity() {
private lateinit var binding: ActivityRemindersBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRemindersBinding.inflate(layoutInflater)
setContentView(binding.root)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
(binding.navHostFragment as NavHostFragment).navController.popBackStack()
return true
}
}
return super.onOptionsItemSelected(item)
}
}
binding.navHostFragment in unresolved and i don't understand why, if i change the type of view in anything differnt then "fragment" the view binding works.
Is possible to use view vbinding for a fragment inside another view?

I found the following as a workaround solution to get the fragment.
val navHostFragment = findNavController(R.id.nav_host_fragment)
navHostFragment.popBackStack()
I think that is not possible to use view binding with a <fragment> because fragment is not a descendent of View.

Instead of:
(binding.navHostFragment as NavHostFragment).navController.popBackStack()
Use the following code:
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController.popBackStack()

Related

Change MaterialToolbar title from Fragment - Kotlin

I have implemented a MaterialToolbar and I want to change the title and functionality of the buttons, depending on which fragment is active. But I've been trying for two days and I can't get access to toolbar from fragment.
It doesn't work with any option:
activity?.actionBar?.title
(activity as AppCompatActivity).supportActionBar?.title
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="#+id/materialToolbar"
style="#style/Widget.MaterialComponents.Toolbar.Primary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="#drawable/ic_menu" />
<fragment
android:id="#+id/fragmentContainer"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/materialToolbar"
app:navGraph="#navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.navigation.NavigationView
android:id="#+id/navigationView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#color/primary_ultra_light"
app:headerLayout="#layout/header_menu_drawer"
app:menu="#menu/menu_drawer" />
</androidx.drawerlayout.widget.DrawerLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.materialToolbar)
supportActionBar?.title = "Test"
}
}
HomeFragment.kt
class HomeFragment: Fragment() {
private lateinit var binding: HomeFragmentBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.actionBar?.title = "Home 1" /* actionBar == null */
/* supportActionBar == null */
(activity as AppCompatActivity).supportActionBar?.title = "Home 2"
}
Result of the code that does not comply with the expected
The idea is to remove the top block of the fragment (Calculators) and use the Toolbar (Test). For this I need to be able to adapt the toolbar for each fragment.
Possible solution <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
So far I have solved it with a ViewModel observer. I don't know if it's a correct solution, but it works. Any comment is welcome, both to confirm that it is a valid solution and if it is not.
ToolbarVM.kt
class ToolbarVM(app: Application) : AndroidViewModel(app) {
private val _title = MutableLiveData<String>()
var title: LiveData<String> = _title
fun setTitle(newTitle: String){
_title.value = newTitle
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val toolbarVm: ToolbarVM by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.materialToolbar)
val titleObserver = Observer<String> { newTitle ->
binding.materialToolbar.title = newTitle
}
toolbarVm.title.observe(this, titleObserver)
}
}
HomeFragment
/*You have to instantiate the ToolbarVm and call the setTitle() method*/
private lateinit var toolbarVm: ToolbarVM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbarVm = activity?.let { ViewModelProvider(it)[ToolbarVM::class.java] }!!
}
// Call the method where necessary
toolbarVm.setTitle("Calculators")
From the MainActivity you can directly use:
binding.materialToolbar.title = "Test"
while when calling from a fragment, you can do the following:
activity?.findViewById<Toolbar>(R.id.materialToolbar)?.title = "Test"
I have solved it thanks to Razvan's answer. It works if I call it from the onViewCreated() function, but it doesn't work the first time the default fragment is loaded with navGraph. So I have placed it in onResume()
HomeFragment.kt
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.calculators)
}

How can I use Navigation (NavHost) in nested fragments?

I'm trying to open Fragment2 from Fragment1(TransformFragment)
throught click item in RecyclerView. I tried to use Navigation (NavHost) to solve this problem.
Fragment1 code as below:
class TransformFragment : Fragment() {
private lateinit var transformViewModel: TransformViewModel
private var _binding: FragmentTransformBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding
get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
transformViewModel = ViewModelProvider(this)[TransformViewModel::class.java]
_binding = FragmentTransformBinding.inflate(inflater, container, false)
val root: View = binding.root
val recyclerView = binding.recyclerviewTransform
val adapter = TransformAdapter()
recyclerView.adapter = adapter
transformViewModel.texts.observe(viewLifecycleOwner) { adapter.submitList(it) }
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
class TransformAdapter() :
ListAdapter<String, TransformViewHolder>(
object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
}
) {
private val drawables =
listOf(
R.drawable.avatar_1,
R.drawable.avatar_2,
R.drawable.avatar_3,
R.drawable.avatar_4,
R.drawable.avatar_5,
R.drawable.avatar_6,
R.drawable.avatar_7,
R.drawable.avatar_8,
R.drawable.avatar_9,
R.drawable.avatar_10,
R.drawable.avatar_11,
R.drawable.avatar_12,
R.drawable.avatar_13,
R.drawable.avatar_14,
R.drawable.avatar_15,
R.drawable.avatar_16,
R.drawable.avatar_17,
)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransformViewHolder {
val binding = ItemTransformBinding.inflate(LayoutInflater.from(parent.context))
return TransformViewHolder(binding)
}
override fun onBindViewHolder(holder: TransformViewHolder, position: Int) {
holder.textView.text = getItem(position)
holder.imageView.setImageDrawable(
ResourcesCompat.getDrawable(holder.imageView.resources, drawables[position], null)
)
holder.itemView.setOnClickListener {
// my navigation(NavHost) code
}
}
}
class TransformViewHolder(binding: ItemTransformBinding) :
RecyclerView.ViewHolder(binding.root) {
val imageView: ImageView = binding.imageViewItemTransform
val textView: TextView = binding.textViewItemTransform
}
}
I tried to use this:
val navController = findNavController(R.id.nav_host_fragment_content_main)
navController.navigate(R.id.nav_detail)
nav_detail to navigate Fragment2, and My Fragment Container. This's my XML. I know that to display Dynamiclly Fragments, you must use FrameLayout, but it produces errors and the fragment container is OK.
<LinearLayout 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:orientation="vertical"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:showIn="#layout/app_bar_main">
<fragment
android:id="#+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginLeft="#dimen/fragment_horizontal_margin"
android:layout_marginRight="#dimen/fragment_horizontal_margin"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="#navigation/mobile_navigation" />
<!--
<FrameLayout
android:id="#+id/nav_host_fragment_content_main"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginLeft="#dimen/fragment_horizontal_margin"
android:layout_marginRight="#dimen/fragment_horizontal_margin"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="#navigation/mobile_navigation" />
-->
</LinearLayout>
I've found the correct answer in Android developers documentation.
holder.itemView.setOnClickListener {
it.findNavController().navigate(R.id.nav_detail)
}
To use findNavController() to navigate between fragments, you've to make sure that you've setup with activity properly first.
So, in your activity layout first add container, parent need nod to be FrameLayout itself, I'm gonna make use of ConstraintLayout below.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context=".MainActivity">
<!-- We declare attribute id to hook with MainActivity.java class-->
<!-- Attribute name will change behavior of simple fragment to NavHostFragment-->
<!-- Keep width and height to match_parent so that all our fragments take whole space-->
<!-- We change this NavHost to behave default NavHost-->
<!-- Attach our app navigation graph to this NavHostFragment-->
<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" />
</androidx.constraintlayout.widget.ConstraintLayout>
Now let's hook container as an acting host for activity.
MainActivity.kt
class MainActivity : AppCompatActivity() {
lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// NavigationController to set default NavHost as nav_host_fragment.
navController = Navigation.findNavController(this, R.id.nav_host_fragment)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp()
}
}
onSupportNavigateUp function helps you to navigate back to previous fragment on action trigered or called specifically on any view with navigateUp().
Now you want to navigate from Fragment1 -> Fragment2.
Make sure you've added two fragments to navigation graph.
Connect first fragment to second with action.
Keep track of destination Id's to call correct one.
<?xml version="1.0" encoding="utf-8"?>
<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"
app:startDestination="#id/firstFragment">
<fragment
android:id="#+id/firstFragment"
android:name=".FirstFragment"
android:label="FirstFragment"
tools:layout="#layout/fragment_first">
<action
android:id="#+id/action_first_fragment_to_second_fragment"
app:destination="#id/secondFragment" />
</fragment>
<fragment
android:id="#+id/secondFragment"
android:name=".SecondFragment"
android:label="Authors"
tools:layout="#layout/fragment_second" />
</navigation>
Now finally once action is triggered call action with id like below on controller.
yourListItemView.setOnClickListener {
findNavController(R.id.action_first_fragment_to_second_fragment)
}
That's it.
Please check the documentation, it has a good information.

View does not have a navController set at [duplicate]

I am using Navigation Component for navigating in my app. It works fine inside fragments but it fails to find the nav host in the activity that holds the actual navigation host.
I am trying to open a new fragment when the user clicks on FAB, which I included in Main activity's XML. When I call findNavController() it fails to find the controller. The nav host controller is in the XML layout. I can't understand why it fails to find it.
MainActivity
class MainActivity : AppCompatActivity(), OnActivityComponentRequest {
override fun getTabLayout(): TabLayout {
return this.tabLayout
}
override fun getFap(): FloatingActionButton {
return this.floatingActionButton
}
private lateinit var tabLayout: TabLayout
private lateinit var floatingActionButton: FloatingActionButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
this.tabLayout = tabs
this.floatingActionButton = fab
fab.setOnClickListener {
it.findNavController().navigate(R.id.addNewWorkoutFragment)
}
}
}
Activity main XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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=".domain.MainActivity"
android:animateLayoutChanges="true">
<com.google.android.material.appbar.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="#style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="#style/AppTheme.PopupOverlay"/>
<com.google.android.material.tabs.TabLayout
android:id="#+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.tabs.TabItem
android:text="Test 1"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
<com.google.android.material.tabs.TabItem
android:text="Test 2"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
</com.google.android.material.tabs.TabLayout>
</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/main_navigation" />
<com.google.android.material.bottomappbar.BottomAppBar
android:id="#+id/bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchorGravity="right|top"
app:layout_anchor="#+id/bar"
android:src="#drawable/ic_add_black_24dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
I was getting the same exeception until I managed to get the navController this way:
val navHostFragment = supportFragmentManager.findFragmentById(R.id.my_fragment_container_view_id) as NavHostFragment
val navController = navHostFragment.navController
Obtained from this answer:
Before finding the nav controller, you need to set the view, if you are using binding:
binding = BaseLayoutBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
If not:
setContentView(R.layout.activity_main)
Use the corresponding binding for fragments.
Try setting up onClickListener of Fab button in onStart of the Activity as in onCreate Activity is just inflating the View and haven't set the NavHostController. So if you setup onClickListener in onStart of activity is will work as expected.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
this.tabLayout = tabs
this.floatingActionButton = fab
}
override fun onStart() {
super.onStart()
floatingActionButton.setOnClickListener {
it.findNavController().navigate(R.id.addNewWorkoutFragment)
}
}
In Java you can find NavController inside activity this way:
Navigation.findNavController(this,R.id.nav_host).navigate(R.id.YourFragment);
The problem might be that FAB was added to the activity or a fragment different from the one used by NavHost fragment. In this case, when you call it.findNavController() it cannot find the navigation controller.
You can either check that your FAB belongs to the fragment pulled by NavHost or call an Activity's findNavController(<id>) and pass it id of the fragment you're looking up
override fun onCreate(savedInstanceState: Bundle?) {
...
fab.setOnClickListener {
findNavController(R.id.nav_host_fragment)
.navigate(R.id.addNewWorkoutFragment)
}
}
Retrieve it later, in onPostCreate. like this:
#Override
public void onPostCreate(Bundle savedInstanceState ) {
super.onPostCreate(savedInstanceState);
navController = Navigation.findNavController(this, R.id.fragment_container_view);
}
Turns out that activity that holds navigation controller... doesn't have navigation component.
The solution is to manually set the NavController to each view contained in the activity.
val navController = findNavController(R.id.nav_host_fragment)
Navigation.setViewNavController(fab, navController)
Now this would work:
fab.setOnClickListener {
it.findNavController().navigate(R.id.addNewWorkoutFragment)
}
I still don't understand why this works the way it works so any explanation would be more than welcome :)
As of now, Android API simply doesn't make much sense.
Source: Navigate to fragment on FAB click (Navigation Architecture Components)
Kotlin Only
Android Navigation KTX provides an extension function for Activity
All I had to do was
findNavController(R.id.myNavHostFragment).navigate(R.id.to_someFragment)
I do have the following dependencies in my app/gradle.build.kts file (
implementation ("androidx.navigation:navigation-fragment-ktx:2.3.5")
implementation ("androidx.navigation:navigation-runtime-ktx:2.3.5")
implementation ("androidx.navigation:navigation-ui-ktx:2.3.5")
Using Directions
Alternatively you could also use generated Directions class. Since this is a global action the Directions class is found in the NavigationDirections.to_someFragment()
//very useful if you're passing args then you could do something like
//NavigationDirections.to_someFragment(myArg)
NavigationDirections.to_someFragment()findNavController(R.id.myNavHostFragment).navigate(NavigationDirections.to_someFragment())
Side-note the specific extension function is being contributed from the runtime-ktx aar
you can still get access in your oncreate by doing
//the id would be the id of the Fragment Element In Your Activity XML File
val navGraph = Navigation.findNavController(this, R.id.activity_main)
binding.mainFab.setOnClickListener {
navGraph.navigate(R.id.destinationFragment)
}
I found this answer helpful. In your activity paste this code
Navigation.findNavController(this, R.id.nav_host_fragment).navigate(R.id.yourDestination)
This is Extension version for Kotlin:
fun AppCompatActivity.findNavController(
#IdRes navHostViewId: Int
): NavController {
val navHostFragment =
supportFragmentManager.findFragmentById(navHostViewId) as NavHostFragment
return navHostFragment.navController
}
The navHostViewId is id of your androidx.fragment.app.FragmentContainerView.
The best way to access navController in OnCreate of Activity is to access it after inflating the layout
that can achieved by kotlin extention function doOnPreDraw()
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val navController: NavController
get() = Navigation.findNavController(this, R.id.nav_host_fragment)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.root.doOnPreDraw {
// here you can safely use navController
binding.toolbar.setupWithNavController(navController)
}
}
}
You can use binding.root.findNavController().navigate(id)

Navigation drawer not showing up. AM I understand it right?

I'm making slidable menu and hamburger icon/sliding not working. I'm also not sure if this working how i think: when we are setting FragmentContainerView like this
<androidx.fragment.app.FragmentContainerView
app:navGraph="#navigation/nav_graph"
We are refer to Fragment set in navigation manager, am I right?
Code:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, EggTimerFragment.newInstance())
.commitNow()
}
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_graph) as NavHostFragment
val navController = navHostFragment.navController
findViewById<NavigationView>(R.id.nav_view)
.setupWithNavController(navController)
val appBarConfiguration = AppBarConfiguration(navController.graph, binding.drawerLayout)
NavigationUI.setupActionBarWithNavController(this, navController, binding.drawerLayout)
NavigationUI.setupWithNavController(binding.navView, navController)
binding.navView.setNavigationItemSelectedListener {
when(it.itemId){
R.id.item1 -> Toast.makeText(applicationContext, "item1 clicked", Toast.LENGTH_SHORT).show()
}
true
}
super.onCreate(savedInstanceState)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if(toggle.onOptionsItemSelected(item)){
return true
}
return onOptionsItemSelected(item)
}
XML main_activity file:
<?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.drawerlayout.widget.DrawerLayout
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".app.MainActivity">
<androidx.fragment.app.FragmentContainerView
app:navGraph="#navigation/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:id="#+id/myNavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
/>
<com.google.android.material.navigation.NavigationView
android:id="#+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:headerLayout="#layout/nav_header"
app:menu="#menu/nav_header_menu"
android:layout_gravity="start"
android:fitsSystemWindows="true"/>
</androidx.drawerlayout.widget.DrawerLayout>
</layout>
Maybe I should cut some code and move it to fragment? Android guide is too briefly and unclear for me.
Please make this following changes:
Add android:fitsSystemWindows="true" to <androidx.drawerlayout.widget.DrawerLayout>
In your MainActivity override onSupportNavigateUp() method
Initialize data Binding like this:
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
Also there is no need for this code. Please remove it. As all the navigation should be managed by navigation graph.
if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .replace(R.id.container, EggTimerFragment.newInstance()) .commitNow() }
I understand the Android Guide is too brief but you can refer to this particular section: Add a navigation drawer

Navigation Component: Cannot find NavController

I am using Navigation Component for navigating in my app. It works fine inside fragments but it fails to find the nav host in the activity that holds the actual navigation host.
I am trying to open a new fragment when the user clicks on FAB, which I included in Main activity's XML. When I call findNavController() it fails to find the controller. The nav host controller is in the XML layout. I can't understand why it fails to find it.
MainActivity
class MainActivity : AppCompatActivity(), OnActivityComponentRequest {
override fun getTabLayout(): TabLayout {
return this.tabLayout
}
override fun getFap(): FloatingActionButton {
return this.floatingActionButton
}
private lateinit var tabLayout: TabLayout
private lateinit var floatingActionButton: FloatingActionButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
this.tabLayout = tabs
this.floatingActionButton = fab
fab.setOnClickListener {
it.findNavController().navigate(R.id.addNewWorkoutFragment)
}
}
}
Activity main XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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=".domain.MainActivity"
android:animateLayoutChanges="true">
<com.google.android.material.appbar.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="#style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="#style/AppTheme.PopupOverlay"/>
<com.google.android.material.tabs.TabLayout
android:id="#+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.tabs.TabItem
android:text="Test 1"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
<com.google.android.material.tabs.TabItem
android:text="Test 2"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
</com.google.android.material.tabs.TabLayout>
</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/main_navigation" />
<com.google.android.material.bottomappbar.BottomAppBar
android:id="#+id/bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchorGravity="right|top"
app:layout_anchor="#+id/bar"
android:src="#drawable/ic_add_black_24dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
I was getting the same exeception until I managed to get the navController this way:
val navHostFragment = supportFragmentManager.findFragmentById(R.id.my_fragment_container_view_id) as NavHostFragment
val navController = navHostFragment.navController
Obtained from this answer:
Before finding the nav controller, you need to set the view, if you are using binding:
binding = BaseLayoutBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
If not:
setContentView(R.layout.activity_main)
Use the corresponding binding for fragments.
Try setting up onClickListener of Fab button in onStart of the Activity as in onCreate Activity is just inflating the View and haven't set the NavHostController. So if you setup onClickListener in onStart of activity is will work as expected.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
this.tabLayout = tabs
this.floatingActionButton = fab
}
override fun onStart() {
super.onStart()
floatingActionButton.setOnClickListener {
it.findNavController().navigate(R.id.addNewWorkoutFragment)
}
}
In Java you can find NavController inside activity this way:
Navigation.findNavController(this,R.id.nav_host).navigate(R.id.YourFragment);
The problem might be that FAB was added to the activity or a fragment different from the one used by NavHost fragment. In this case, when you call it.findNavController() it cannot find the navigation controller.
You can either check that your FAB belongs to the fragment pulled by NavHost or call an Activity's findNavController(<id>) and pass it id of the fragment you're looking up
override fun onCreate(savedInstanceState: Bundle?) {
...
fab.setOnClickListener {
findNavController(R.id.nav_host_fragment)
.navigate(R.id.addNewWorkoutFragment)
}
}
Retrieve it later, in onPostCreate. like this:
#Override
public void onPostCreate(Bundle savedInstanceState ) {
super.onPostCreate(savedInstanceState);
navController = Navigation.findNavController(this, R.id.fragment_container_view);
}
Turns out that activity that holds navigation controller... doesn't have navigation component.
The solution is to manually set the NavController to each view contained in the activity.
val navController = findNavController(R.id.nav_host_fragment)
Navigation.setViewNavController(fab, navController)
Now this would work:
fab.setOnClickListener {
it.findNavController().navigate(R.id.addNewWorkoutFragment)
}
I still don't understand why this works the way it works so any explanation would be more than welcome :)
As of now, Android API simply doesn't make much sense.
Source: Navigate to fragment on FAB click (Navigation Architecture Components)
Kotlin Only
Android Navigation KTX provides an extension function for Activity
All I had to do was
findNavController(R.id.myNavHostFragment).navigate(R.id.to_someFragment)
I do have the following dependencies in my app/gradle.build.kts file (
implementation ("androidx.navigation:navigation-fragment-ktx:2.3.5")
implementation ("androidx.navigation:navigation-runtime-ktx:2.3.5")
implementation ("androidx.navigation:navigation-ui-ktx:2.3.5")
Using Directions
Alternatively you could also use generated Directions class. Since this is a global action the Directions class is found in the NavigationDirections.to_someFragment()
//very useful if you're passing args then you could do something like
//NavigationDirections.to_someFragment(myArg)
NavigationDirections.to_someFragment()findNavController(R.id.myNavHostFragment).navigate(NavigationDirections.to_someFragment())
Side-note the specific extension function is being contributed from the runtime-ktx aar
you can still get access in your oncreate by doing
//the id would be the id of the Fragment Element In Your Activity XML File
val navGraph = Navigation.findNavController(this, R.id.activity_main)
binding.mainFab.setOnClickListener {
navGraph.navigate(R.id.destinationFragment)
}
I found this answer helpful. In your activity paste this code
Navigation.findNavController(this, R.id.nav_host_fragment).navigate(R.id.yourDestination)
This is Extension version for Kotlin:
fun AppCompatActivity.findNavController(
#IdRes navHostViewId: Int
): NavController {
val navHostFragment =
supportFragmentManager.findFragmentById(navHostViewId) as NavHostFragment
return navHostFragment.navController
}
The navHostViewId is id of your androidx.fragment.app.FragmentContainerView.
The best way to access navController in OnCreate of Activity is to access it after inflating the layout
that can achieved by kotlin extention function doOnPreDraw()
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val navController: NavController
get() = Navigation.findNavController(this, R.id.nav_host_fragment)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.root.doOnPreDraw {
// here you can safely use navController
binding.toolbar.setupWithNavController(navController)
}
}
}
You can use binding.root.findNavController().navigate(id)

Categories

Resources