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 🍻
Related
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 need to implement a custom onBackButtonPress method for Fragments. example in LogoutFragment, after logout is handled. user cannot go backStack but a message is shown like press again to exist and is exited. I used this solution. But is not working. Then I saw this Android Doc with onBackPressedDispatcher callback method CODE BELOW. I guess this will work. I added dependencies, but how to implement this in a different fragments, with only 1 activity and fragment container. Kotlin version.
class LogoutFragment : DaggerFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
...
}
...
}
...
}
onBackPressedDispatcher gives an opportunity to handle back press differently in every fragment. Hence you may have to give this piece of code in every fragment.
In the fragment:
var doubleBackToExitPressedOnce = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
val callback = object : OnBackPressedCallback(true ) {
override fun handleOnBackPressed() {
if (doubleBackToExitPressedOnce)
requireActivity().finish()
Toast.makeText(requireContext(), "Press again to go back",
Toast.LENGTH_LONG).show()
doubleBackToExitPressedOnce = true
}
}
If you want to handle back presses of all fragments in one place, you can do so by manipulating the doBack() method in your reference: How to implement onBackPressed() in Fragments?
//pseudocode
fun doBack()
{
//find the fragment
val fragment = supportFragmentManager.findFragmentByTag(
//your fragment tag
)
if(fragment is FragmentA)
{
//do something
}
else if(fragment is FragmentB)
{
//do something else
}
else
{
activity.getSupportFragmentManager().popBackStack(null,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
I have an activity using fragments. To communicate from the fragment to the activity, I use interfaces. Here is the simplified code:
Activity:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
private val diaryFragment: DiaryFragment = DiaryFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
}
The fragment:
class DiaryFragment: Fragment() {
private var onEntryClickedListener: IAddEntryClickedListener? = null
private var onDeleteClickedListener: IDeleteClickedListener? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_diary, container, false)
//Some user interaction
onDeleteClickedListener!!.onEntryDeleteClicked()
onDeleteClickedListener!!.onEntryDeleteClicked()
return view
}
interface IAddEntryClickedListener {
fun onAddEntryClicked()
}
interface IDeleteClickedListener {
fun onEntryDeleteClicked()
}
fun setOnEntryClickedListener(listener: IAddEntryClickedListener) {
onEntryClickedListener = listener
}
fun setOnDeleteClickedListener(listener: IDeleteClickedListener) {
onDeleteClickedListener = listener
}
}
This works, but when the fragment is active and the orientation changes from portrait to landscape or otherwise, the listeners are null. I can't put them to the savedInstanceState, or can I somehow? Or is there another way to solve that problem?
Your Problem:
When you switch orientation, the system saves and restores the state of fragments for you. However, you are not accounting for this in your code and you are actually ending up with two (!!) instances of the fragment - one that the system restores (WITHOUT the listeners) and the one you create yourself. When you observe that the fragment's listeners are null, it's because the instance that has been restored for you has not has its listeners reset.
The Solution
First, read the docs on how you should structure your code.
Then update your code to something like this:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
// DO NOT create new instance - only if starting from scratch
private lateinit val diaryFragment: DiaryFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
// Null state bundle means fresh activity - create the fragment
if (savedInstanceState == null) {
diaryFragment = DiaryFragment()
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
else { // We are being restarted from state - the system will have
// restored the fragment for us, just find the reference
diaryFragment = supportFragmentManager().findFragment(R.id.content_frame)
}
// Now you can access the ONE fragment and set the listener on it
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
}
}
Hope that helps!
the short answer without you rewriting your code is you have to restore listeners on activiy resume, and you "should" remove them when you detect activity losing focus. The activity view is completely destroyed and redrawn on rotate so naturally there will be no events on brand new objects.
When you rotate, "onDestroy" is called before anything else happens. When it's being rebuilt, "onCreate" is called. (see https://developer.android.com/guide/topics/resources/runtime-changes)
One of the reasons it's done this way is there is nothing forcing you to even use the same layout after rotating. There could be different controls.
All you really need to do is make sure that your event hooks are assigned in OnCreate.
See this question's answers for an example of event assigning in oncreate.
onSaveInstanceState not working
There is a huge bug I am trying to figure out in my latest project using Kotlin. So how the app works is that it has a Bottom Navigation Activity, and on click on the icons on the Bottom menu, it replaces the container with the respected fragment. On onCreate of the activity, I instantiate all the different fragments with a function defined in a companionObject within the fragments, which returns itself (kind of like a static factory method in java):
override fun onCreate(savedInstanceState: Bundle?) {
{...}
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
fragment1 = Fragment1.newInstance()
fragment2 = Fragment2.newInstance()
}
Then I have this switch to replace the container to the respected fragment on click:
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.f1 -> {
replaceFragment(fragment1)
return#OnNavigationItemSelectedListener true
}
R.id.f2 -> {
replaceFragment(fragment2)
return#OnNavigationItemSelectedListener true
}
}
false
}
{...}
fun replaceFragment(destFragment: Fragment) {
supportFragmentManager
.beginTransaction()
.replace(R.id.container, destFragment)
.addToBackStack(destFragment.toString())
.commit()
}
The fragments are communicating. If you where to click on both fragments, and thereby cause their lifecycles to execute, the code works fine. However, if you do changes in frag1, invoking a communication between the fragments, without having clicked on frag2 in advance, the app crashes. The reason is this line in frag2:
fun saveList(){
val sharedpref : SharedPreferences = context!!.getSharedPreferences("sharedPrefs", MODE_PRIVATE)
{...}
}
Turns out that the context is null, if the user has never invoked the frag2 lifecycle by clicking it. Any ideas to how to fix this problem?
Use Activity context for these case
fun saveList() {
activity?.let {
val sharedpref: SharedPreferences =
it.getSharedPreferences("sharedPrefs", MODE_PRIVATE)
{ }
}
}
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
}