In my app I use bottomNavigationView with jetpack navigation
Look at the picture
I have 2 pages
When I click to second page, then click back press, I return to first fragment, but I want to close app
some code
private fun initView() {
setSupportActionBar(toolbar)
bottomNavigationView = findViewById(R.id.bottom_navigation)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
navController?.apply {
appBarConfiguration = AppBarConfiguration(setOf(
R.id.action_home,
R.id.action_favorite
))
appBarConfiguration?.let {
setupActionBarWithNavController(this, it)
}
bottomNavigationView?.let {
NavigationUI.setupWithNavController(it, this)
}
}
}
One way of doing it is if you want custom behaviour from the back button, you will need to override onBackPressed() in your activity
override fun onBackPressed() {
if (R.id.action_favorite == navController?.currentDestination?.id) {
finish()
} else {
super.onBackPressed()
}
}
Related
I use Navigatation component latest stable version.
I would like to create a shortcut option menu item in my application, which is navigates to a fragment.
This fragment is also used by the drawer menu.
So when I use the drawer menu for navigation, everything is good, but when I navigate to for example Fragment B and after that I use the options menu to navigate to Fragment C, and after the I can not navigate back to Fragment B from drawer, because it always shows Fragment C.
I can solve this problem by adding navController.popBackStack() before the navigation to Fragment C, but this is not good at all, becaouse the previous fragment is destroyed, and I can not go back to it, by pressing back button.
Is there any solution of this problem?
MainActivity
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_main,menu)
menu?.findItem(R.id.action_worksheet_management)?.setOnMenuItemClickListener {
val navController = findNavController(R.id.nav_host_fragment_content_main)
navController.popBackStack()
navController.navigate(R.id.nav_work)
return#setOnMenuItemClickListener true
}
return true
}
private fun setUpNavigation() {
val navController = findNavController(R.id.nav_host_fragment_content_main)
setSupportActionBar(binding.appBarMain.toolbar)
val menuItems = mutableListOf(
R.id.nav_worksheet_browser,
R.id.nav_product,
R.id.nav_about,
R.id.nav_company_info,
R.id.nav_services,
R.id.nav_stock_receive,
R.id.nav_synchronization,
R.id.nav_order_return,
R.id.nav_work,
R.id.nav_settings,
R.id.nav_certificates
)
appBarConfiguration = AppBarConfiguration(
menuItems.toSet(),
binding.drawerLayout
)
setupActionBarWithNavController(navController, appBarConfiguration)
binding.navView.setupWithNavController(navController)
}
override fun onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
I resolved it, by changed the nav version from 2.4.1 to 2.3.5. I hope it will be fixed later.
I am developing an App with the google recommended SingleActivity pattern. My scenario is: My App starts with the startDestination at HomeFragment. And If the user is not loggedIn I want to start from LoginFragment. My logic is I save loggedIn status in DataStore which is initially false. When My app starts launching. In my MainActivity, I observe that loggedIn status through ViewModel. Then I passed that status to a function. This is my function.
private fun setDestinationForApp(isLoggedIn: Boolean) {
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.dest_home) {
if (!isLoggedIn) {
val authNavOptions = NavOptions.Builder()
.setPopUpTo(R.id.dest_home, true)
.build()
navController.navigate(R.id.action_home_to_login, Bundle(), authNavOptions)
}
}
}
}
This is fine enough. But what I faced is : My app starts show HomeFragment default and then if the user is not loggedIn, destinate to LoginFragment.
I want to display corresponding destination fragment according to my loggedIn status from DataStore. Not want to display the default HomeFragment first.
This may be of Activity lifecycle and Navigation Components callback listeners. But I am not smart enough like that. Please help me I am stuck in this. If my question is not clear or cant be solved. Please help me the appropriate way.
This is my whole MainActivity.
#AndroidEntryPoint
class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate) {
private val navHostFragment: NavHostFragment by lazy {
supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
}
private val navController: NavController by lazy {
navHostFragment.navController
}
private val appBarConfiguration: AppBarConfiguration by lazy {
AppBarConfiguration(navController.graph)
}
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(this) }
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupNavigation()
}
override fun observe() {
super.observe()
lifecycleScope.launchWhenCreated {
viewModel.authState.collect {
setDestinationForApp(it)
}
}
}
private fun setupNavigation() {
setSupportActionBar(binding.authToolbar)
setupActionBarWithNavController(navController, appBarConfiguration)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
fun showLoadingDialog(text: String) {
loadingDialog.apply {
setMessage(text)
setCanceledOnTouchOutside(false)
setCancelable(false)
show()
}
}
fun hideLoadingDialog() {
loadingDialog.hide()
}
private fun setDestinationForApp(isLoggedIn: Boolean) {
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.dest_home) {
if (!isLoggedIn) {
val authNavOptions = NavOptions.Builder()
.setPopUpTo(R.id.dest_home, true)
.build()
navController.navigate(R.id.action_home_to_login, Bundle(), authNavOptions)
}
}
}
}
}
This is because the navigation component starts with the start graph fragment. And anyway if you want to go back from LoginFragment. it gets back to HomeFragment. So in this case you need to check in startup process for loggedIn status and if the user is not loggedIn you can change the starting point of graph programmatically. This way the starting point will be LoginFragment and it will never navigate to HomeFragment. And after the login process you can revert this logic to set HomeFragment as starting point of your navigation graph.
I have two fragments and I want to create interaction between them using backward button in action bar. Ideally, I would like the state of previous fragment was saved.
I could find information for only activities.
For fragments I found this
private fun setupBackButton() {
if (activity is AppCompatActivity) {
(activity as AppCompatActivity?)?.supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
}
But it only displays back button, nothing happens by clicking.
EDIT
In the first fragment I call the second like:
val fragment = UserContentFragment()
fragment.setUser(item.user)
if (fragmentManager != null) {
fragmentManager!!
.beginTransaction()
.replace(R.id.main_layout, fragment)
.addToBackStack(null)
.commit()
}
This is my UserContentFragment second fragment:
class UserContentFragment : Fragment() {
private lateinit var user: SearchUser
fun setUser(user: SearchUser) {
this.user = user
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val root = inflater.inflate(R.layout.fragment_user_content, container, false)
val userImage = root.findViewById(R.id.user_img) as ImageView
if (context != null) {
Glide.with(context!!)
.load(user.profile_pic_url)
.circleCrop()
.into(userImage)
}
val userName: TextView = root.findViewById(R.id.user_name)
userName.text = user.full_name
val toolbar: Toolbar = root.findViewById(R.id.toolbar)
toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() }
setupBackButton()
return root
}
private fun setupBackButton() {
if (activity is AppCompatActivity) {
(activity as AppCompatActivity?)?.supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
}
}
And this its .xml file:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:background="#color/colorBlack">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/user_title"
android:layout_width="match_parent"
android:layout_height="100dp">
<ImageView
android:id="#+id/user_img"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="#+id/user_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="16dp"
android:textColor="#color/colorWhite"
android:textSize="22sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/user_img"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>
In order to have a response when you hit the Home/up Button, here are a couple of options to solve this:
First Option
In the fragments that you show up the Home/UP button, override onOptionsItemSelected() method and call the activity's onBackPressed() for home button id
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle presses on the action bar menu items
when (item.itemId) {
android.R.id.home -> {
activity?.onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
Side note:
Instead of showing the Home/Up button on the ActionBar using below, and you've to set the boolean value for each fragment that you need to show up the home button using the method below:
private fun setupBackButton() {
if (activity is AppCompatActivity) {
(activity as AppCompatActivity?)?.supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
}
You can instead setup the ActionBar with AppBarConfiguration in onCreate() method of the activity as below:
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
val host: NavHostFragment = supportFragmentManager
.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment? ?: return
val navController = host.navController
appBarConfiguration = AppBarConfiguration(
setOf(R.id.second_fragment, R.id.third_fragment)) // IDs of fragments you want without the ActionBar home/up button
setupActionBarWithNavController(navController, appBarConfiguration)
}
By doing this, the up button will show up in all fragments, but R.id.second_fragment, R.id.third_fragment and you no longer need to set the setDisplayHomeAsUpEnabled() to each individual fragment to show up the home/up button. But still you need to override onOptionsItemSelected as mentioned above.
Second Option
Which is neater than the first option. First, you've to implement the above side node to allow the NavController auto controls/configure the ActionBar.
So, the past side note is a mandatory part of this option.
Then override onSupportNavigateUp() in the activity which allows NavigationUI to support proper ActionBar up navigation.
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.my_nav_host_fragment).navigateUp(appBarConfiguration)
}
And then override onOptionsItemSelected() in activity to make Have Navigation UI Handle the OptionsMenu/ActionBar item selection
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return item.onNavDestinationSelected(findNavController(R.id.my_nav_host_fragment))
|| super.onOptionsItemSelected(item)
}
I would say that option 2 is neater than 1, because you write all the code in one place (activity) without touching fragments, and also it automatically configure all the fragments, no need to manually setDisplayHomeAsUpEnabled() or activity.onBackPressed() for individual fragments that you want the Home/Up button to show up.
In Kotlin Fragment with Navigation.
First, you add setHasOptionsMenu(true) in onCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
Then override the onOptionsItemSelected and when R.id.home you can control BACK Button
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.getItemId()) {
android.R.id.home ->
findNavController().navigate(R.id.action_FragmentTwo_to_FragmentOne)
}
return true
}
You need to attach a click listener to the toolbar like :
toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() }
class StartActivity : FragmentActivity() {
/**
* The pager widget, which handles animation and allows swiping horizontally to access previous
* and next wizard steps.
*/
private lateinit var mPager: ViewPager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.start_activity)
val loginButton = findViewById<Button>(R.id.login_button)
loginButton.setOnClickListener {
this.didTapLoginButton()
}
}
private fun didTapLoginButton() {
val i = Intent(this, LoginActivity::class.java)
startActivity(i)
}
}
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.login_activity)
//actionbar
val actionbar = supportActionBar
//set actionbar title
actionbar!!.title = "New Activity"
//set back button
actionbar.setDisplayHomeAsUpEnabled(true)
actionbar.setDisplayHomeAsUpEnabled(true)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
// it is important function, you need to write this function in which class/activity you want to show back arrow
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return super.onOptionsItemSelected(item)
}
}
Add this in the NavigationClickListener.
FragmentManager fm = getFragmentManager();
if (fm.getBackStackEntryCount() > 0) {
Log.i("MainActivity", "popping backstack");
fm.popBackStack();
} else {
Log.i("MainActivity", "nothing on backstack, calling super");
super.onBackPressed();
}
Zain's Second Option is very good, but I'll show a different way for the part made in onCreate, with usage of DataBinding:
ActivityMainBinding.inflate(layoutInflater).run {
setContentView(root)
setSupportActionBar(toolbar)
(supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment)
as NavHostFragment).navController.let { navController ->
val appBarConfiguration = AppBarConfiguration(navController.graph)
toolbar.setupWithNavController(navController, appBarConfiguration)
}
}
It's a late answer but I hope it will help someone.
I did it in Kotlin and the way I managed to make it work is:
In the MainActivity I overrode the onSupportNavigateUp:
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_activity_main)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
}
And declared the AppBarConfiguration outside the onCreate()
I'm following single activity approach. I have navigation toolbar, whenever i go to other screens (fragments) instead of hamburger icon i will have back arrow.
What i want to achieve is, pop my current fragment using action on pressing toolbar back arrow.
I've tried
requireActivity().getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
NavHostFragment.findNavController(EventDetailsFragment.this)
.navigate(R.id.action_nav_event_details_to_nav_home);
}
});
But not getting the call over there, i checked by running app in debug mode.
in Activity oncreate:
navController = findNavController(R.id.my_nav_host)
//my_nav_host defined in activity xml file as id of fragment or FragmentContainerView
val appBarConfiguration = AppBarConfiguration(navController.graph)
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration)
and:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return true
}
then in your fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callback: OnBackPressedCallback =
object : OnBackPressedCallback(true /* enabled by default */) {
override fun handleOnBackPressed() {
//do what you want here
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
Add this code in parent activity
Add in onCreate method
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Add this method also in parent activity
#Override
public boolean onSupportNavigateUp() {
return super.onSupportNavigateUp();
}
If you are using custom toolbar in your xml, you may consider using below approach
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat)
setSupportActionBar(activity_chat_toolbar)
activity_chat_toolbar.setNavigationOnClickListener {
onBackPressed() <-- custom toolbar's back press
}
val navHostFrag =
supportFragmentManager.findFragmentById(R.id.chat_fragment_container_view) as NavHostFragment
navController = navHostFrag.navController
navController.setGraph(R.navigation.chat_nav_graph, intent.extras)
setupActionBarWithNavController(navController)
}
// Function to check startDestination to finish() the parent activity
override fun onBackPressed() {
if(navController.graph.startDestination == navController.currentDestination?.id) {
finish()
} else {
super.onBackPressed()
}
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
Toolbar toolbar = findviewbyid(R.id.toolbar);
toolbar.setnavigationonclicklistener(new view.onclicklistener(){
code here
});
setsupportactionbar(toolbar);
I am making a simple note taking app, I have 2 fragments with navigation component, one fragment has a list of notes and the other is for editing or creating a new note.
In MainActivity I added
val navController = this.findNavController(R.id.host_fragment)
NavigationUI.setupActionBarWithNavController(this, navController)
and then override onSupportNavigateUp()
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.host_fragment)
return navController.navigateUp()
}
In NoteEditFragment
requireActivity().onBackPressedDispatcher.addCallback(this) {
saveOrUpdateNote(noteId, note)
}
now it all works well when pressing the "back button" in the device, However onBackPressedDispatcher.addCallback() is note triggered when I press the "up button" the one on the top left of the screen.
My question is : How do I handle this up button from my NoteEditFragment?
Thanks in advance
Finally, I found the solution.
First In the activity onCreate method I had to connect the navigation like I did:
val navController = this.findNavController(R.id.host_fragment)
NavigationUI.setupActionBarWithNavController(this, navController)
Then still in MainActivity override onSupportNavigateUp() :
override fun onSupportNavigateUp(): Boolean
{
val navController = this.findNavController(R.id.host_fragment)
return navController.navigateUp()
}
Then In the Fragment onCreateView I had to enable option menu:
setHasOptionsMenu(true)
then in the fragment I overridden onOptionsItemSelected :
override fun onOptionsItemSelected(item: MenuItem): Boolean
{
// handle the up button here
return NavigationUI.onNavDestinationSelected(item!!,
view!!.findNavController())
|| super.onOptionsItemSelected(item)
}
Note: I think if you have more than one option menu, then I think you have to do a when (item) statement to check what option has been chosen.
Also if you want to handle the device back button then you can do like this in your fragment onCreateViewMethod :
requireActivity().onBackPressedDispatcher.addCallback(this)
{
// handle back button
// change this line to whatever way you chose to navigate back
findNavController().navigate(NoteEditFragmentDirections.actionNoteEditFragmentToNoteListFragment())
}
I think the accepted answer is a bit messy. So, i found a clean code. It is perfect for my use case as i don't need to do anything special when the device back button is pressed. Therefore, anyone here with the same requirements can follow my code.
In onCreate of MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navController = findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(navController.graph)
// Check if androidx.navigation.ui.NavigationUI.setupActionBarWithNavController is imported
// By default title in actionbar is used from the fragment label in navigation graph
// To use the app name, remove label else if you want to add customized label specify it there
setupActionBarWithNavController(this, navController, appBarConfiguration)
...
}
Again, in the MainActivity itself:
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.fragment)
// Check if androidx.Navigation.ui navigateUp is imported and used
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
I have added comments for better understanding. If you think the answer is helpful then please upvote. Thank You. Happy Development :)
EDIT 1:
No need to use supportActionBar.setDisplayHomeAsUpEnabled(true) in the fragments. AppBarConfiguration will take care of it.
if you use noActionBar themes and you want to use your own toolbar as an actionBar for the fragment, you can use this method
in the host activity
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.navHostFragment).navigateUp()
}
in the onCreateView of the fragment
(activity as AppCompatActivity).setSupportActionBar(binding.toolbar)
(activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
in very simple way you just need to set the supportNavigateUp on the Activity.
in MainActivity:
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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()
}
in my case its worked