I am trying to implement a way for the menuitem button to navigate to a different fragment based on the current fragment.
For example, if I am in the fragment_home, I can access the dog_fragment and from the dog_fragment I can access another fragment like cat_fragmentand vice versa.
So far I am only able to navigate from fragment_home to dog_fragment, however when I try to access any other fragments from the dog_fragment my app crash. If I try to access another fragment from any other fragment accept from the home_fragment my app crashes
//This is in the Mainactivity
fun onComposeAction(item: MenuItem) {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
val navController = navHostFragment.navController
when (item.getItemId()) {
R.id.Hamster -> {
navController.navigate(R.id.action_home2_to_hamster)
}
R.id.Dog -> {
navController.navigate(R.id.action_home2_to_dog)
}
R.id.Cat ->{
navController.navigate(R.id.action_home2_to_cat)
}
}
}
Simply tie the menuitems to the destination fragments you have.
Related
How to add a Fragment to an Activity using jetpack compose, i couldn't able to find a proper documentation , Here is my activity code looks like
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeDemoTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
RenderTextUserFields()
}
}
}
}
}
And i have a fragment name LoginFragment i want to render the LoginFragment when the Application initially loaded then i want to navigate to Another Fragment I have DetailsFragment
You can use a standard AppCompatActivity:
For example in a Scaffold you can use something like:
findNavController().navigate(R.id.nav_profile, bundle)
scope.launch {
scaffoldState.drawerState.close()
}
with:
private fun findNavController(): NavController {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
return navHostFragment.navController
}
First I would recommend if you are using Fragments with JetpackCompose, to avoid using Activities.
So if if i understood correctly what you could do is define your NavigationGraph, which you can do by adding a navigation folder to your res folder, and there create nav_graph.xml(see: Android Navigation).
Set your initial fragment(in this case LoginFragment) as a starting fragment(this can be done in the design of nav_graph.xml after you add your fragments to it).
After that you can declare your nav controller in a fragment with for example val navController = findNavController() which you can pass into your composable, and upon a click or whatever you have you navigate to the desired fragment(which has to be defined in nav_graph.xml).
There are two ways to do this:
You can just call navController.navigate(R.id.yourFragmentId)
Or you can define a action between LoginFragment and DetailsFragment in nav_graph.xml(you can just connect them in the design view), and then later navigate with navController.navigate(R.id.loginToDetailActionId)
I have a complicated structure where a number of Fragments link to each other, so using NavController I am trying to avoid creating multiple duplicates of the same Fragment in the BackStack.
I found this post How to check Navigation Destination is in the NavController back stack or not?, which I have implemented as below:
private fun onSiteItemClicked(item: SiteObject) {
Log.d(TAG, "onItemClicked() - ${item.siteReference}")
item.siteID.let {
businessViewModel.updateCurrentSiteVMLiveData(it)
try {
val backStackEntry : NavBackStackEntry = navController.getBackStackEntry(R.id.siteFragment)
makeToast("backStackEntry = ${backStackEntry.destination.label.toString()}")
// Navigate to existing Fragment!! HOW TO DO?
} catch (ex: IllegalArgumentException) {
navController.navigate(R.id.action_contactFragment_to_siteFragment)
// Creates new Fragment as one doesn't existing in backstack. THIS WORKS!
}
}
}
So I have a reference to the Fragment in the BackStack, but I can't see how to navigate to it..
I have a navigation which looks like this
Frag1 -> Frag2 -> Frag3
Inside Frag2 there is a NavHostFragment with its own navigation
InnerFrag1 -> InnerFrag2
If I do this
Navigate to Frag2
Navigate to InnerFrag2 inside Frag2
Navigate to Frag3
Go back
then I'll see InnerFrag2 inside Frag2, when I press back normally I would go from InnerFrag2 to InnerFrag1 inside Frag2 but now it's going to Frag1 instead.
Here is my navigation handling inside Frag2
private val backPressedCallback = OnBackPressedCallback {
navHostFragment.navController.navigateUp()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().addOnBackPressedCallback(backPressedCallback)
}
override fun onDestroyView() {
activity?.removeOnBackPressedCallback(backPressedCallback)
super.onDestroyView()
}
private val navHostFragment: NavHostFragment
get() = childFragmentManager.findFragmentById(R.id.innerNavHostFragment) as NavHostFragment
When going back to Frag2 the fragment in the nav host is the correct one, but navigating back moves away from Frag2 because inner nav host's back stack is lost. Can I persist it somehow or fix it some other way?
EDIT: actually when going from Frag3 to Frag2 I see InnerFrag1 inside, the both look alike, that's why going back at this point brings me back to Frag1
EDIT2: I found my problem, I inflate Frag2s navigation from code in onViewCreated like this
val navHostFragment = (frag2NavHostFragment as? NavHostFragment) ?: return
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(navigationId)
navHostFragment.navController.graph = graph
setting it in xml makes it work, I still need to set it from code somehow, Frag2 chooses which navigation to use depending on its arguments
Now my question changes from Navigation's back stack is lost to How to preserve NavHostFragment's state when settings it's graph from code
You can now handle onBackPress on fragments. In your fragment just add this in onViewCreated method.
val navController = Navigation.findNavController(view)
requireActivity().onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navController.popBackStack(R.id.fragmentWhereYouWantToGo, false)
}
})
I would also give a check to app:popUpTo , app:popUpToInclusive or singleTop XML attributes to the fragments inside your Frag2
After looking into this for a little, original question doesn't make much sense, I'd delete it but it got 2 upvotes ¯\_(ツ)_/¯
I solved my problem by adding a check before inflating graph, so that NavHostFragment's graph is set only if it doesn't already have one.
try {
navHostFragment.navController.graph
} catch (e: IllegalStateException) {
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(navigationId)
navHostFragment.navController.graph = graph
}
NavController.getGraph doesn't return null, instead it throws IllegalStateException, hence the weird check
I need a second activity with a nav graph and have a return button in toolbar to the first activity that also contains a nav graph
In my second activity I have onSupportNavigateUp and setupActionBarWithNavController when entering the fragments if the arrow back button appears but in the activity no.
Try adding setHomeButtonEnabled and setDisplayHomeAsUpEnabled in both the activity and the fragment and if the button appears back, but when I enter some fragment in front and return to the fragment startDestination disappears the button back
I just need to keep the button back in the activity and solve my problem
You can do it by specifying a setFallbackOnNavigateUpListener:
private fun setupToolbar() {
val navController = findNavController(R.id.nav_host_fragment)
val appBarConfiguration =
AppBarConfiguration.Builder()
.setFallbackOnNavigateUpListener { onNavigateUp() }
.build()
dataBinding.toolbar.apply {
setupWithNavController(navController, appBarConfiguration)
}
}
And then do whatever you want in the Activity's:
override fun onNavigateUp(): Boolean {
finish()
return true
}
You can't, activity has their own toolbars and in your case they have two different NavControllers. So your second activity manage NavUp Button for his fragment and when start Destination fragment comes NavUpButton(Backbutton) disappear because it has no destination left behind. And if you programmatically show NavUp Button on start destination of that (2nd activity) and manage onClick and start first activity that always goes to Start destination of first activity's fragment because it has it's own Nav Controller.
Problem is that Navigation UI not works like that. The better approach is use only one activity with multiple fragments. And use any other approach to solve your problem within the same nav controller.
Add setHomeButtonEnabled func. to your returning action. If you are returning with button add it to onClick or with backPress, override backPress.
With this solve : You will set your button enable, when you try to return your startDestination.
I created an interface to show/hide up button from the nav host activity. Here is how the activity implements the interface methods to show/hide up button:
override fun showUpButton() {
val navController = this.findNavController(R.id.nav_host)
val listener = AppBarConfiguration.OnNavigateUpListener { navController.navigateUp() }
val abc = AppBarConfiguration.Builder().setFallbackOnNavigateUpListener(listener).build()
NavigationUI.setupActionBarWithNavController(this, navController, abc)
}
override fun hideUpButton() {
val navController = this.findNavController(R.id.nav_host)
NavigationUI.setupActionBarWithNavController(this, navController)
}
Here the method in the activity when up button pressed:
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.nav_host)
if(!navController.navigateUp()){ // When in start destination
onBackPressed()
}
return navController.navigateUp()
}
In a fragment can listen whenever back button (NOT up button) pressed:
private fun setupBackPress() {
requireActivity()
.onBackPressedDispatcher
.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
}
})
}
I have no idea how to, using the new navigation architecture component, navigate from my main screen (with a FloatingActionButton attatched to a BottomAppBar) to another screen without the app bar.
When I click the fab I want my next screen (fragment?) to slide in from the right. The problem is where do I put my BottomAppBar? If I put it in my MainActivity then I have the issue of the FloatingActionButton not having a NavController set. I also cannot put my BottomAppBar in my Fragment. I am at a loss.
Ran into this issue today and I found out that there is a simple and elegant solution for it.
val navController = findNavController(R.id.navHostFragment)
fabAdd.setOnClickListener {
navController.navigate(R.id.yourFragment)
}
This takes care of the navigation. Then you must control the visibility of your BottomAppBar inside your Activity.
You could have your BottomAppBar in MainActivity and access your FloatingActionButton in your fragment as follows
activity?.fab?.setOnClickListener {
/*...*/
findNavController().navigate(R.id.action_firstFragment_to_secondFragment, mDataBundle)
}
You could hide the BottomAppBar from another activity as follows
(activity as AppCompatActivity).supportActionBar?.hide()
Make sure you .show() the BottomAppBar while returning to previous fragment
Put it in MainActivity and setOnClickListener in onStart() of the activity and it will work fine.
override fun onStart() {
super.onStart()
floatingActionButton.setOnClickListener {
it.findNavController().navigate(R.id.yourFragment)
}
}
Note:This solution is like and hack and better is to follow Activity LifeCycle and setUp OnClickListener when the activity is ready to interact.
Similar question [SOLVED]
if you wanted to navigate to certain fragment (not the star one) in the beginning for some reason, and also you have to graphs for one activity, here is what I suggest:
this method will start activity
companion object {
const val REQUEST_OR_CONFIRM = "request_or_confirm"
const val IS_JUST_VIEW = "IS_JUST_VIEW"
const val MODEL = "model"
fun open(activity: Activity, isRequestOrConfirm: Boolean, isJustView: Boolean = false, model: DataModel? = null) {
val intent = Intent(activity, HostActivity::class.java)
intent.putExtra(REQUEST_OR_CONFIRM, isRequestOrConfirm)
intent.putExtra(IS_JUST_VIEW, isJustView)
intent.putExtra(MODEL, model)
activity.startActivity(intent)
}
}
and then in, onCreate method of Host Activity, first decide which graph to use and then pass the intent extras bundle so the start fragment can decide what to do:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_purchase_nav)
if (intent.getBooleanExtra(REQUEST_OR_CONFIRM, true)) {
findNavController(R.id.nav_host_fragment).setGraph(R.navigation.nav_first_scenario, intent.extras)
} else {
findNavController(R.id.nav_host_fragment).setGraph(R.navigation.nav_second_scenario, intent.extras)
}
}
and here's how you can decide what to do in start fragment:
if (arguments != null && arguments!!.getBoolean(HostActivity.IS_JUST_VIEW)){
navigateToYourDestinationFrag(arguments!!.getParcelable<DataModel>(HostActivity.MODEL))
}
and then navigate like you would do normally:
private fun navigateToYourDestinationFrag(model: DataModel) {
val action = StartFragmentDirections.actionStartFragmentToOtherFragment(model)
findNavController().navigate(action)
}
here's how your graph might look in case you wanted to jump to the third fragment in the beginning
PS: make sure you will handle back button on the third fragment, here's a solution
UPDATE:
as EpicPandaForce mentioned, you can also start activities using Navigation Components:
to do that, first add the Activity to your existing graph, either by the + icon (which didn't work for me) or by manually adding in the xml:
<activity
android:id="#+id/secondActivity"
tools:layout="#layout/activity_second"
android:name="com.amin.SecondActivity" >
</activity>
you can also add arguments and use them just like you would in a fragment, with navArgs()
<activity
android:id="#+id/secondActivity"
tools:layout="#layout/activity_second"
android:name="com.amin.SecondActivity" >
<argument
android:name="testArgument"
app:argType="string"
android:defaultValue="helloWorld" />
</activity>
in koltin,here's how you would use the argument, First declare args with the type of generated class named after you activity, in this case SecondActivityArgs in top of your activity class:
val args: SecondActivityArgsby by navArgs()
and then you can use it like this:
print(args.testArgument)
This doesn't destroy BottomAppBar. Add this to MainActivity only and don't do anything else
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
fabAdd.setOnClickListener {
findNavController(navHostFragment).navigate(R.id.fab)
}