I have two activities, one holds all the fragments for the Login process, and the other one holds all the fragments for the main app.
Let's say I want to navigate from Activity1 (that holds all navigation graph of Login) to Activity2 (That holds all the navigation graph for the main app)
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
}
fun goToMainActivity(){
startActivity(Intent(this,MainActivity::class.java))
finish()
}
}
Here I call the method goToMainActivity()
class LoginFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_login,container,false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_go.setOnClickListener {
// call the method goToMainActivity() to kill all fragments contained by that Activity and move foward to MainActivity with another nav_graph
}
}
}
Since LoginActivity holds a nav_graph and is the navigation host for all the Login Fragments, now I want to kill all the fragments contained to LoginActivity and move towards a new Activity (MainActivity) that holds a different nav graph
Is this the good way to do it? Or I should navigate differently ?
You don't need to define a second activity, simply add a second navigation graph to your nav_graph.xml file. Something like:
<?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/loginFragment">
<fragment
android:id="#+id/loginFragment"
android:name="com.mycompany.loginFragment"
tools:layout="#layout/fragment_login"
android:label="Login" >
<action
android:id="#+id/action_loginFragment_to_new_graph"
app:destination="#id/new_graph" />
</fragment>
<include app:graph="#navigation/new_graph" />
</navigation>
Then, with your navController, navigate the action:
navController.navigate(R.id.action_loginFragment_to_new_graph)
You can migrate to a single Activity Navigation. In your Nav Graph add an Action to navigate between the last LoginFragemnt and MainFragment and select:
Pop Behaviour:
Pop To - Self
Inclusive - YES
This should automatically clear the stack for you and pressing back will close the App.
EDIT:
Or just manually add these two lines to your nav xml under the action that moves from the LoginFragment to the MainFragment:
app:popUpTo="#id/loginFragment"
app:popUpToInclusive="true"
Related
I have FragmentA, FragmentB and DialogFragment(BottomDialogFragment). I I abbreviated them as A,B and D
D will be shown after the button in A is clicked. It means A -> D
B will be shown after the button in D is clicked. It means D -> B
I config them in navigation.xml
<fragment
android:id="#+id/A"
android:name="com.example.A">
<action
android:id="#+id/A_D"
app:destination="#id/D" />
</fragment>
<dialog
android:id="#+id/D"
android:name="com.example.D">
<action
android:id="#+id/D_B"
app:destination="#id/B" />
</dialog>
<fragment
android:id="#+id/B"
android:name="com.example.B">
</fragment>
Now when I click the button in A, the fragment will jump to D.
Then I click the button in D, the fragment will jump to B.
But when I pop the navigation stack in B, it will back to A, and the D doesn't show.
What should I do? I want the D still exists on the surface of A.
What should I do? I want the D still exists on the surface of A.
So far, this is not possible, because dialogs are handled in a separate window than the activities/fragments; and therefore their back stack is handled differently And this is because Dialog implements the FloatingWindow interface.
Check this answer for more clarification.
But to answer your question, there are two approaches in mind:
Approach 1: Change fragment B to a DailogFragment, and in this case both B & D are dialogs and therefore when you popup the stack to back from B to D, you'll still see D showing.
Approach 2: To have a flag that is set if you return from B to D, and when if so, you re-show D.
Actually, approach 2 isn't that good, because it doesn't keep D in the back stack while you go from D to B; it's just a workaround; also the user would see the dialog transitioning/fade animation while it returns from B to D; so it's not natural at all. So, here only approach 1 will be discussed.
Approach 1 in detail:
Pros:
It's very natural and will keep the back stack the same as you would like to.
Cons:
DialogFragment B has a limited window than the normal fragment/activity.
B is no longer a normal fragment, but a DialogFragment, so you might encounter some other limitations.
To solve the limited window of B, you can use the below theme:
<style name="DialogTheme" parent="Theme.MyApp">
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowIsFloating">false</item>
</style>
Where Theme.MyApp is your app's theme.
And apply it to B using getTheme():
class FragmentB : DialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return layoutInflater.inflate(R.layout.fragment_b, container, false)
}
override fun getTheme(): Int = R.style.DialogTheme
}
Also you need to change B in the navigation graph to a dialog:
<dialog
android:id="#+id/B"
android:name="com.example.B">
</dialog>
Preview:
Refer to this link for a full working example.
You need to leverage the NavigationUI global action (as explained here) in order to be able to navigate "back" to a destination. Put this code into the main_graph xml:
<action android:id="#+id/action_global_fragmentD" app:destination="#id/fragmentD"/>
Next, in your activity add these to catch back press:
class MainActivity: AppCompatActivity {
...
var backPressedListener: OnBackPressedListener? = null
override fun onBackPressed() {
super.onBackPressed()
backPressedListener?.backHaveBeenPressed()
}
}
interface OnBackPressedListener {
fun backHaveBeenPressed()
}
this is like delegates in swift
Next in FragmentB, add these:
class FragmentB: Fragment(), OnBackPressedListener {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
(activity as MainActivity).backPressedListener = this
return inflater.inflate(R.layout.fragment_b, container, false)
}
override fun backHaveBeenPressed() {
// show Dialog
findNavController().navigate(R.id.action_global_fragmentD)
}
}
You can then navigate back to the DialogFragment as you require. This approach does not use the popBackStack because your use case is a custom behavior not handled by the NavigationUI framework (you need to implement it).
There is no need to pop the stack with controller.popBack() as the Stack is managed by the Navigation Library. Note that the stack operates on a LIFO basis so that is why the Fragment disappeared.
You need to add more navigation graph actions:
<?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"
android:id="#+id/navigation"
app:startDestination="#id/A">
<fragment
android:id="#+id/A"
android:name="com.example.A"
android:label="A" >
<action
android:id="#+id/action_A_to_D"
app:destination="#id/D" />
</fragment>
<dialog
android:id="#+id/D"
android:name="com.example.D"
android:label="D" >
<action
android:id="#+id/action_D_to_B"
app:destination="#id/B" />
<action
android:id="#+id/action_D_to_A"
app:destination="#id/A" />
</dialog>
<fragment
android:id="#+id/B"
android:name="com.example.B"
android:label="B" >
<action
android:id="#+id/action_B_to_D"
app:destination="#id/D" />
</fragment>
</navigation>
Then in your dialog Fragment add the following:
For OK/Yes:--
private fun doNav() {
NavHostFragment.findNavController(this).navigate(R.id.action_fragmentD_to_fragmentB)
}
For CANCEL/No:--
private fun doBackNav() {
NavHostFragment.findNavController(this).navigate(R.id.action_fragmentD_to_fragmentA)
}
Finally in the B Fragment, override the back press button and execute this:
Navigation.findNavController(requireView()).navigate(R.id.action_fragmentB_to_fragmentD)
What I'm trying to do
I am using Android Navigation component to handle navigation in my app. In this example, I have two screens, screen A and screen B.
I want the user to be able to click a button in screen A and be able to navigate to screen B; and then be prevented from going back to the previous screen (screen A).
The problem
When the user navigates to screen B from screen A, the back button on the action bar still allows the user to go back to the previous screen, however when clicking on the back button in the bottom bar it exits the app so this part works OK.
What do I need to do in order to remove the back button in the Action Bar?
What I've read so far
I have followed the guidance within these three articles but I think they might be ignoring the ActionBar's back button:
Stackoverflow - How to clear navigation Stack after navigating to
another fragment in Android
Android Developer Guide - Conditional navigation
Android Developer Guide - Navigate to a destination
My Code
Navigation Graph - nav_graph.xml
<?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"
android:id="#+id/nav_graph"
app:startDestination="#id/screen_a">
<fragment
android:id="#+id/screen_a"
android:name="com.example.conditionalnavigation.AFragment"
android:label="screen A">
<action
android:id="#+id/action_AFragment_to_BFragment"
app:destination="#id/screen_b"
app:launchSingleTop="true"
app:popUpTo="#id/screen_a"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="#+id/screen_b"
android:name="com.example.conditionalnavigation.BFragment"
android:label="screen B" />
</navigation>
MainActivity - This acts as my Single Activity navhost.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val navController = this.findNavController(R.id.myNavHostFragment)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.myNavHostFragment)
return navController.navigateUp()
}
}
In your activity class add the following member (in Kotlin):
private lateinit var appBarConfiguration: AppBarConfiguration
Inside the onCreate method add the following lines:
....
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
...
...
appBarConfiguration = AppBarConfiguration(
setOf([**ID of the fragment layout you want without back button**],
), drawerLayout
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
....
In this way your fragment will be a root fragment and the back button is removed. Hope it helps.
Try to disable home button at the creation of screen b fragment:
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
var rootView = inflater?.inflate(R.layout.fragment_screen_b, container, false)
(activity as AppCompatActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
return rootView
}
If it didn't work, then try it in onViewCreated() method.
If not worked, try to add below as well:
setHasOptionsMenu(false)
I am creating a game where the user goes through a series of 5 screens. At the last screen, the user has the choice to end the game, at which point they are taken back to the starting screen. My problems come in when a user ends the game and then starts again. While navigating through the app, the navigation host fragment cannot be found.
The first time through the app, it navigates at usual, but the second time, the navigation host cannot be found.
I have tried using different views to find the navigation host, and while debugging, I saw that for the fragment where it can not be found, the parent is equal to null.
This is where I navigate, in the fragments onViewCreated()
viewModel.getGameUpdates().observe(activity!!, Observer { updatedGame ->
if(updatedGame.playerList.size == 0){
Log.d("END","END")
viewModel.endGame()
}
adapter?.players = updatedGame.playerList
if(updatedGame.started){
Navigation.findNavController(view).navigate(R.id.action_waitingFragment_to_gameFragment)
}
})
and this is the moment where the user clicks to navigate back to the first screen:
btn_end_game.setOnClickListener {
viewModel.endGame()
timer.cancel()
Navigation.findNavController(view).navigate(R.id.action_gameFragment_to_startFragment)
}
The layout for my MainActivity that holds the navigation host fragment is:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</FrameLayout>
I do realize that I am just adding on top of the back stack when I would rather pop back to the first fragment. I am just lost as to how the fragment is null.
The following is the nav_graph.xml
<?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"
android:id="#+id/nav_graph" app:startDestination="#id/startFragment">
<fragment android:id="#+id/startFragment" android:name="com.dangerfield.spyfall.start.StartFragment"
android:label="StartFragment">
<action android:id="#+id/action_startFragment_to_joinGameFragment" app:destination="#id/joinGameFragment"/>
<action android:id="#+id/action_startFragment_to_newGameFragment" app:destination="#id/newGameFragment"/>
</fragment>
<fragment android:id="#+id/newGameFragment" android:name="com.dangerfield.spyfall.newGame.NewGameFragment"
android:label="NewGameFragment">
<action android:id="#+id/action_newGameFragment_to_waitingFragment" app:destination="#id/waitingFragment"/>
</fragment>
<fragment android:id="#+id/joinGameFragment" android:name="com.dangerfield.spyfall.joinGame.JoinGameFragment"
android:label="JoinGameFragment">
<action android:id="#+id/action_joinGameFragment_to_waitingFragment" app:destination="#id/waitingFragment"/>
</fragment>
<fragment android:id="#+id/waitingFragment" android:name="com.dangerfield.spyfall.waiting.WaitingFragment"
android:label="WaitingFragment">
<action android:id="#+id/action_waitingFragment_to_gameFragment" app:destination="#id/gameFragment"/>
<action android:id="#+id/action_waitingFragment_to_startFragment" app:destination="#id/startFragment"/>
</fragment>
<fragment android:id="#+id/gameFragment" android:name="com.dangerfield.spyfall.game.GameFragment"
android:label="GameFragment">
<action android:id="#+id/action_gameFragment_to_startFragment" app:destination="#id/startFragment"/>
</fragment>
</navigation>
This is the message given after crash:
java.lang.IllegalStateException: View android.widget.ScrollView{637e4ce VFED.V... ......ID 0,0-1440,2308} does not have a NavController set
LiveData remembers the current data and will automatically redeliver it when the observer becomes started again, making it inappropriate for events that trigger navigation operations: your operation to navigate() is going to be triggered every time your Fragment is started, making it impossible to actually pop back to that Fragment.
Note that Fragments are not destroyed while on the back stack. If you're changing the underlying data that your Fragment relies on while that Fragment is on the back stack, you should use the viewLifecycleOwner instead of this (representing the Fragment) for your LifecycleOwner passed to observe() when observing in onViewCreated(). This ensures that you will no longer get observer callbacks once your view is destroyed (i.e., you go onto the back stack).
activity!! is absolutely always wrong to use as the LifecycleOwner from within a Fragment, since that means the observer will not be cleaned up even if the Fragment is completely destroyed (it'll only be cleaned up when the activity is destroyed).
As per the conditional navigation documentation, the recommended approach is to ensure that your LiveData is tracking state rather than events. That way, after you call navigate(), you can update the state to ensure that when the callback happens a second time, you don't call navigate() a second time. This approach is recommended over the SingleLiveEvent approach.
Even I Was facing the same issue when I used to navigate from current fragment to the next fragment, and on the back press of hardware navHost would be null, The mistake I was doing is that I had made the variable navController global where I used to instantiate like this in onCreate()
Before:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
navController = this.findNavController(R.id.myNavHostFragment)
NavigationUI.setupActionBarWithNavController(this,navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp()
}
}
After:
now it's working after this change
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
navController = this.findNavController(R.id.myNavHostFragment)
NavigationUI.setupActionBarWithNavController(this,navController)
}
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.myNavHostFragment)
return navController.navigateUp()
}
}
Dont why navController would be null if made it global??
I've follow the docs to implement shared view transitions with the new Navigation component and it is not working. This is what I have:
Fragment A has this code to call fragment B
val extras = FragmentNavigatorExtras(
taskNameInput to "taskName")
findNavController().navigate(R.id.action_aFragment_to_BFragment,
null), // Bundle of args
null, // NavOptions
extras)
Taking a look to the layout, the id has the transition name set as follows:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/taskNameInput"
android:transitionName="taskName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
...
Then FragmentB has the following view in the layout:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/taskNameInput"
android:transitionName="taskName"
android:layout_width="0dp"
android:layout_height="wrap_content"
...>
When going from fragmentA to fragmentB, the enter animation is played but not the sharedView transition. Any clue? Thanks
It is missing to setup the sharedTransition to FragmentB, which can be done in onCrateView() as follows:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(R.transition.move)
return inflater.inflate(com.meanwhile.flatmates.R.layout.fragment_b, container, false)
}
Also you need to create the transaction file move.xml:
<transitionSet>
<changeBounds/>
<changeTransform/>
<changeClipBounds/>
<changeImageTransform/>
</transitionSet>
At the time of this post it is not written in the docs for the new Navigation Component, but this is just the old way of doing. Since the navigation component is doing some magic for the enter/exit transition, I was expecting to do also some more for shared view ones. In any case, it is not a big deal to add those lines.
I am trying out the new Navigation Architecture Component, and I can't figure out how to do this:
I have 1 Activity (MainActivity) + 3 Fragments:
SplashFragment (Home)
MainFragment
SignUpFragment
I would like to use SplashFragment to determine if I should navigate to MainFragment or SignUpFragment, but once it reaches either of those 2, you should not be able to pop back to SplashFragment. How can I do that with the new navigation component?
I tried popBackStack before and after calling navigate(R.id.action_xxx), but neither of them work (which make sense: before it has nothing to pop; after it just closes the fragment that just got added). Does that mean the only way to do that is to override onBackPress to intercept it and make sure navigateUp does not get call in those cases?
Thanks!
First, add attributes app:popUpTo='your_nav_graph_id' and app:popUpToInclusive="true" to the action tag.
<fragment
android:id="#+id/signInFragment"
android:name="com.glee.incog2.android.fragment.SignInFragment"
android:label="fragment_sign_in"
tools:layout="#layout/fragment_sign_in" >
<action
android:id="#+id/action_signInFragment_to_usersFragment"
app:destination="#id/usersFragment"
app:launchSingleTop="true"
app:popUpTo="#+id/main_nav_graph"
app:popUpToInclusive="true" />
</fragment>
Second, navigate to the destination, using the above action as parameter.
findNavController(fragment).navigate(SignInFragmentDirections.actionSignInFragmentToUserNameFragment())
NOTE: If you navigate using method navigate(#IdRes int resId), you won't get the desired result. Hence, I used method navigate(#NonNull NavDirections directions).
This worked for me in alpha05 release. Add app:popUpTo="#id/nav_graph" in the action tag(inside your nav_graph.xml file).
Here "#id/nav_graph is the id of my graph or also called as the Root.
<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/startFragment">
.......
<action
android:id="#+id/action_startFragment_to_homeFragment"
app:destination="#id/homeFragment"
app:popUpTo="#id/nav_graph"/>
.......
You can also do this in design tab:- select "SplashFragment" and select the action you want to change and then pick "root" for "Pop To"
WARNING: clearTask has been deprecated and will be remove in future release, not sure what the solution is. Please follow this issue for now to keep up to date
Oh after 10 minutes finally found the key: use clearTask.
All I have to do is add app:clearTask="true" to that specific action, or use .navigate(R.id.actionXXXX, null, NavOptions.Builder().setClearTask(true).build()), and it's done. Just make sure you add it to all the children of SplashFragment (in this case, both MainFragment and SignUpFragment).
So if you have splash fragment and main fragment and you don't want to go back to splash fragment after the main fragment below method you can achieve this
<fragment
android:id="#+id/splashFragment"
android:name="com.example.youappname.views.SplashFragment"
android:label="fragment_splash"
tools:layout="#layout/fragment_splash">
<action
android:id="#+id/action_splashFragment_to_mainFragment"
app:destination="#id/mainFragment"
app:popUpTo="#id/splashFragment"
app:popUpToInclusive="true"/>
</fragment>
In you Kotlin Splash Fragment:
private lateinit var navController: NavController
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
}
private fun navigateToMainFrag() {
navController.navigate(R.id.action_splashFragment_to_mainFragment)
}
Now when you press back button it will close the app instead of showing the splash screen
For anyone wanted to do this purely in code:
Navigation.findNavController(v)
.navigate(R.id.action_splashFragment_to_userProfileFragment2, null,
new NavOptions.Builder().setPopUpTo(R.id.splashFragment, true).build())
The sample solution is add a onBackPressedDispatcher on Owner Activity of fragment/navigation:
https://developer.android.com/guide/navigation/navigation-custom-back#implement_custom_back_navigation