I'm trying to create a single activity Android application.
I have MainActivity (only activity) with BottomNavigationView, three top level fragments and some child fragments. My requirement is whenever the screen is showing top level fragments, bottom navigation should be visible such that switching is possible. But when I'm viewing any of the child fragments, bottom navigation should be hidden.
Is there any out-of-box way using the Navigation component or need to change the visibility manually ?
Update (Navigation component 1.0)
As of Navigation component 1.0.0-alpha08, method addOnNavigatedListener(controller: NavController, destination: NavDestination) was changed to addOnDestinationChangedListener(controller: NavController, destination: NavDestination, arguments: Bundle). Its behavior was also slightly changed (it is also called if the destinations arguments change).
Old Answer
You can use NavController.OnNavigatedListener to achieve this behavior (set it in Activity onCreate):
findNavController(R.id.container).addOnNavigatedListener { _, destination ->
when (destination.id) {
R.id.dashboardFragment -> showBottomNavigation()
else -> hideBottomNavigation()
}
}
private fun hideBottomNavigation() {
// bottom_navigation is BottomNavigationView
with(bottom_navigation) {
if (visibility == View.VISIBLE && alpha == 1f) {
animate()
.alpha(0f)
.withEndAction { visibility = View.GONE }
.duration = EXIT_DURATION
}
}
}
private fun showBottomNavigation() {
// bottom_navigation is BottomNavigationView
with(bottom_navigation) {
visibility = View.VISIBLE
animate()
.alpha(1f)
.duration = ENTER_DURATION
}
}
Using addOnDestinationChangedListener works, and it's the solution recommended in the official documentation, but it does cause some flickering, as the callback is executed before the fragment is attached.
I find the below answer more flexible, and handles animations better:
supportFragmentManager.registerFragmentLifecycleCallbacks(object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(fm: FragmentManager, f: Fragment, v: View, savedInstanceState: Bundle?) {
TransitionManager.beginDelayedTransition(binding.root, Slide(Gravity.BOTTOM).excludeTarget(R.id.nav_host_fragment, true))
when (f) {
is ModalFragment -> {
binding.bottomNavigation.visibility = View.GONE
}
else -> {
binding.bottomNavigation.visibility = View.VISIBLE
}
}
}
}, true)
You can customize it depending on the transitions between your fragments, by choosing different animation (on my example it's a Slide), or by making the call at another lifecycle callback.
You have to make a method in MainActivity for visibility. Do call that method from fragments where you want to show or hide.
One thing I faced with such scenario is, bottom navigation visibility is not being properly gone. So I put bottom navigation view in Relative layout and hide that parent view.
you just need to write this code in MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//Getting the Navigation Controller
navController = Navigation.findNavController(this, R.id.fragment)
//Setting the navigation controller to Bottom Nav
bottomNav.setupWithNavController(navController)
//Setting up the action bar
NavigationUI.setupActionBarWithNavController(this, navController)
//setting the Bottom navigation visibiliy
navController.addOnDestinationChangedListener { _, destination, _ ->
if(destination.id == R.id.full_screen_destination ){
bottomNav.visibility = View.GONE
}else{
bottomNav.visibility = View.VISIBLE
}
}
}
get more details from the android developer documentation:
Update UI components with NavigationUI
So even tho this question was already answered and the accepted answer is one that works, here is the code to actually achieve this behaviour:
MainActivity
fun hideBottomNav() {
bottomNavigationView.visibility = View.GONE
}
fun showBottomNav() {
bottomNavigationView.visibility = View.VISIBLE
}
Then you call the functions in your fragment onViewCreated(), onDetach() function, like:
Fragment
class FragmentWithOutBottomNav() : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as MainActivity).hideBottomNav()
}
override fun onDetach() {
super.onDetach()
(activity as MainActivity).showBottomNav()
}
}
Hope I could help some people. Happy coding!
navController.addOnDestinationChangedListener { _, destination, _ ->
val isMainPage = bottomNavigationView.selectedItemId == destination.id
bottomNavigationView.isVisible = isMainPage
}
Related
I am trying to follow single activity pattern with android navigation component and my %99 of fragment are portrait but I need to make a new fragment can be portrait or landscape without adding new activity how can I achieve. I could't find any resource. is it possible ? if it is how ?
You can add NavController.OnDestinationChangedListener and set orientation according to the current fragment.
Add this in your activity's onCreate:
val navHostFragment = supportFragmentManager.findFragmentById(R.id.your_nav_host_fragment) as NavHostFragment
navHostFragment.navController..addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.destination_with_orientation) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
} else {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
The following steps could be useful for you:
Don't lock screen orientation from AndroidManifest.xml.
Register a listener inside Activity on the childFragmentManager of the NavHostFragment, that will execute callbacks on fragment lifecycle change
override fun onCreate(savedInstanceState: Bundle?) {
//...
val fragments = supportFragmentManager.fragments
check(fragments.size == 1) {
val baseMessage = "Expected 1 fragment to host the app's navigation. Instead found ${fragments.size}."
if (fragments.size == 0) {
val suggestion = "(Make sure you specify `android:name=\"androidx.navigation.fragment." +
"NavHostFragment\"` or override setUpNavigationGraph)"
"$baseMessage $suggestion"
} else baseMessage
}
with(fragments[0].childFragmentManager) {
registerFragmentLifecycleCallbacks(CustomFragmentLifecycleCallbacks(), false)
}
}
private inner class CustomFragmentLifecycleCallbacks : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(fm: FragmentManager, f: Fragment, v: View, savedInstanceState: Bundle?) {}
override fun onFragmentViewDestroyed(fm: FragmentManager, f: Fragment) {}
}
Follow this guide to lock/unlock screen orientation depending upon which Fragment is visible, from the above callback.
NOTE
Fragment tags or instance type could be used for writing conditional statements inside the lifecycle callbacks, based on app's navigation design.
Don't forget to unregisterFragmentLifecycleCallbacks from Activity.onDestroy()
Cheers 🍻
this my Auth Activity
class AuthActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityAuthBinding.inflate(layoutInflater)
setContentView(binding.root)
navController = Navigation.findNavController(this, fragment.id)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, null)
}
}
LoginFragment -> if login is success goto "AcceptCodeFragment"
viewModel.loginResponse.observe(viewLifecycleOwner, { response ->
viewBinding.pbLogin.visible(response is Resource.Loading)
when (response) {
is Resource.Success -> {
viewBinding.tvResponse.text = response.value.message
val action = LoginFragmentDirections.actionLoginFragmentToAcceptCodeFragment()
findNavController().navigate(action)
}
is Resource.Error -> if (response.isNetworkError) {
requireView().snackBar("Check your connection")
} else {
requireView().snackBar(response.errorBody.toString())
}
}
in AcceptCodeFragment Back button not work.
Two fragments using same viewmodel.
Your issue is not with the back button not working, it is that LiveData is for state, not events like your loginResponse. As LiveData is for events, it redelivers the previous response when you go back to your LoginFragment. This then triggers your navigate() call again, pushing you right back to your AcceptCodeFragment.
As per the LiveData with SnackBar, Navigation, and other events blog post, LiveData cannot be directly used for events. Instead, you should consider using an event wrapper or another solution (such as a Kotlin Flow) that allow your events to only be handled once.
I am trying to use Jetpack Navigation component. so navigation component will automatically handle if I want to segue from a destination to another destination when I click a menu in the navigation drawer
but I want to perform an action if a menu is clicked, say for example if a menu in drawer is clicked then I want to show a toast message.
in old way, ie using fragment transaction, I can easily check from onNavigationItemSelected, but I am no longer find that method
so how to do that in navigation component ?
I have tried to check onDestinationChanged , but it doesn't work
override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
if (destination.id == R.id.my_destination {
// show toast in here
// but it doesn't work
}
}
here is my MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var navController : NavController
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
appBarConfiguration = AppBarConfiguration(setOf(
R.id.destination_share,
R.id.destination_message,
R.id.destination_chat),
drawer_layout
)
// init nav controller
navController = Navigation.findNavController(this,R.id.nav_host_fragment)
// set toolbar
setSupportActionBar(toolbar)
// set up navigation drawer
NavigationUI.setupActionBarWithNavController(this,navController, appBarConfiguration)
NavigationUI.setupWithNavController(navigation_view,navController)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController,appBarConfiguration)
}
}
You can handle Menu clicks as following
navView.menu.findItem(R.id.logout)
.setOnMenuItemClickListener { menuItem: MenuItem? ->
// write your code here
true
}
Have you tried using
bottom_nav_view.setOnNavigationItemSelectedListener {
if (destination.id == R.id.my_destination {
// show toast in here
// but it doesn't work
}
}
I am using the navigation component in my Android app, which automatically provides me with back and up navigation. Now I don't want to change any of those behaviours, but I want to add some logging specific to the fragment where the user presses either the up button in the toolbar or the back button.
I tried this, and it worked only for the back button, and I didn't figure out how to leave the default navigation behaviour intact. Plus it seems like this adds a callback at the activity level, so it's not specific to the fragment where I add the callback.
And it seems like onOptionsItemSelected is called for normal menu items, but not for the Up button.
How can I handle this consistently without changing the behaviour of my entire app?
You are very close to the answer.
Try this.
Note: This code is copied from my app. Change as per your requirement.
In Activity:
In onCreate():
// Observe action state live data
activityViewModel.actionStateMutableLiveData.observe(this, Observer { actionState ->
actionState?.let {
if (actionState != "NO_ACTION") {
when (actionState) {
"NAVIGATE_UP" -> {
if (!navController.navigateUp()) {
finish()
}
}
}
// Reset action state
activityViewModel.setActionState("NO_ACTION")
}
}
})
And,
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
navController.currentDestination?.id?.let { currentDestinationId ->
return when (currentDestinationId) {
R.id.fragmentToLog -> {
false
}
else -> {
activityViewModel.setActionState("NAVIGATE_UP")
true
}
}
}
activityViewModel.setActionState("NAVIGATE_UP")
return true
}
else -> {
item.onNavDestinationSelected(findNavController(R.id.fragment_activitymain))
|| super.onOptionsItemSelected(item)
}
}
}
Fragment:
In onViewCreated():
requireActivity().onBackPressedDispatcher.addCallback(this) {
handleNavigateBack()
}
And
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
handleNavigateBack()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun handleNavigateBack() {
// TODO: Add your fragment logs here
activityViewModel.setActionState("NAVIGATE_UP")
}
Activity ViewModel:
// Action state
var actionStateMutableLiveData = MutableLiveData(NO_ACTION)
private set
fun setActionState(actionStateValue: String?) =
actionStateMutableLiveData.postValue(actionStateValue)
Concepts used:
MVVM architecture
Live Data & View model
Android architecture navigation components
Please comment if anything is not clear.
in your activity you should set up NavigationUI first by following code
val navController = this.findNavController(R.id.nav_app_id)
NavigationUI.setupActionBarWithNavController(this,navController)
in Fragment you could detect when user click back at toolbar in onOptionsItemSelected and then call findNavController().navigateUp()
One possible solution involves three steps: Define an interface, let the desired fragments implement that interface and override the onSupportNavigateUp / onNavigateUp / onBackPressed method in your hosting activity.
First, define an interface, e.g.:
interface CustomNavAction {
fun logSomeStuff()
}
Secondly, add the interface to the desired Fragment, e.g.:
class AFragment : Fragment(), CustomNavAction {
...
override fun logSomeStuff() {
Log.d("TAG", "Up or back")
}
...
}
Finally, override the onSupportNavigateUp / onNavigateUp / onBackPressed method in the Activity that hosts the navigation component, find the current fragment and check if it implements the interface. If so, you can call the action. For example:
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
// find current fragment, and invoke custom action if up button is pressed
val navHostFragment = supportFragmentManager.fragments[0]
val currentFragment = navHostFragment.childFragmentManager.fragments[0]
if (currentFragment is CustomNavAction) {
(currentFragment as CustomNavAction).logSomeStuff()
}
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
override fun onBackPressed() {
super.onBackPressed()
...
// similar logic as onSupportNavigateUp
}
In this way, you don't mess with the overall navigation, and only the fragments implementing the interface will log something.
This solution is not specific to the fragment but in this way, you can detect the back press and navigation up with less code
In Fragment, onCreate add this
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// enable the callback, when we receive a back press event disable the callback and dispatch the event to the activity
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Log.d(
"SomeTag",
"User pressed up back button to navigate back from fragment."
)
isEnabled = false
requireActivity().onBackPressed()
}
})
}
Now, in your activity, you need to override the default ToolBar#onNavigationOnClickListener()
toolbar.setNavigationOnClickListener {
val currentFragment = navHostFragment.childFragmentManager.primaryNavigationFragment
Log.d(
"SomeTag",
"User pressed up arrow to navigate back from fragment: $currentFragment"
)
navController.navigateUp(appBarConfiguration)
}
I want to hide bottomNavigationView in some fragments.
I have tried the below code, but it has a flicker effect. (bottomNavigationView hide before the nextFragment becomes visible.
val navController = this.findNavController(R.id.nav_host_home)
navController.addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.searchArticlesFragment -> bnvMain.visibility = View.GONE
R.id.articleFragment -> bnvMain.visibility = View.GONE
else -> bnvMain.visibility = View.VISIBLE
}
}
I have also tried another code. But it resizes the fragment. And giving OutOfMemoryException in Destination Fragment.
supportFragmentManager.registerFragmentLifecycleCallbacks(object :
FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(
fm: FragmentManager,
f: Fragment,
v: View,
savedInstanceState: Bundle?
) {
when (f) {
is SearchArticlesFragment -> bnvMain.visibility = View.GONE
is ArticleDetailsFragment -> bnvMain.visibility = View.GONE
else -> bnvMain.visibility = View.VISIBLE
}
}
}, true)
Please help me how can I hide the bottomNavigationView in the proper and best possible way? Is this the only way I can hide the bottomNavigationView? How youtube and Instagram achieve this behavior?
If your code follows single activity design pattern then the following solution suites you.
Create a method inside the parent activity to hide/show bottomNavigationView.
Create a BaseFragment class(create your fragments by extending this BaseFragment Class)
In the BaseFragment create a variable to hold the bottomNavigationViewVisibility (hide/show)
In onActivityCreated method of the BaseFragment, get the activity reference and set the bottomNavigationViewVisibility by calling the method which we created in STEP1.
In each fragment you create, just set the bottomNavigationViewVisibility variable.
Example:
In parentAcitivty layout, file add bottomNavigationView
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/main_bottom_navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/windowBackground"
app:labelVisibilityMode="labeled"
app:menu="#menu/main_nav" />
Step 1: In parent activity, create a method to change the visibility.
fun setBottomNavigationVisibility(visibility: Int) {
// get the reference of the bottomNavigationView and set the visibility.
activityMainBinding.mainBottomNavigationView.visibility = visibility
}
Step 2 & 3 & 4:
abstract class BaseFragment : Fragment() {
protected open var bottomNavigationViewVisibility = View.VISIBLE
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// get the reference of the parent activity and call the setBottomNavigationVisibility method.
if (activity is MainActivity) {
var mainActivity = activity as MainActivity
mainActivity.setBottomNavigationVisibility(bottomNavigationViewVisibility)
}
}
override fun onResume() {
super.onResume()
if (activity is MainActivity) {
mainActivity.setBottomNavigationVisibility(bottomNavigationViewVisibility)
}
}
}
Step 5:
class SampleFragment1 : BaseFragment() {
// set the visibility here, it takes care of setting the bottomNavigationView.
override var navigationVisibility = View.VISIBLE
// override var navigationVisibility = View.GONE
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_sampleFragment1, container, false)
}
}