Problem with android toolbar using navigation drawer - android

List the item
I'm developing an app using a navigation drawer and navigation components and I'm facing two issues:
I settled specifically each toolbar title where it is supposed to be, but every time I change the fragments, in the toolbar, for an instant, I can see the previous name from the fragment, which is the fragment name itself. So, it quickly changes from MySpecificFragment to MyFragmentName and I would like it to not happen. I've settled the title even onCreateView or onViewCreated. It didn't matter, still happening.
How could I decide the direction in which the back button of the fragment goes? I would like to create a standard position where the back button arrow goes, always the same. But it just travels back to the previous fragment (which is not a real problem, but I would like to improve its behavior)
Sorry for the lack of code, I don't know what I am supposed to display since I'm going against the standard android behavior.
P.S.: Using android studio and kotlin

Regarding the first issue, one way to avoid the brief display of the previous fragment name in the toolbar is to set the toolbar title in the parent activity and then update it from the fragment's onResume() method. This ensures that the toolbar title is set correctly when the fragment is resumed after being pushed onto the back stack. Here's an example code snippet:
In your activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
}
fun setToolbarTitle(title: String) {
supportActionBar?.title = title
}
}
In your fragment:
class MySpecificFragment : Fragment() {
override fun onResume() {
super.onResume()
(activity as? MainActivity)?.setToolbarTitle("MySpecificFragment")
}
}
Regarding the second issue, you can customize the back button behavior by using a custom NavController.OnDestinationChangedListener. In the listener, you can set the back button icon and its behavior based on the current and previous destinations. Here's an example code snippet:
class MyNavigationController(activity: AppCompatActivity, navController: NavController) {
init {
navController.addOnDestinationChangedListener(
activity, object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
if (destination.id == R.id.my_fragment) {
activity.supportActionBar?.setDisplayHomeAsUpEnabled(false)
} else {
activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back)
activity.supportActionBar?.setHomeActionContentDescription(R.string.back)
activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back)
activity.supportActionBar?.setDisplayShowHomeEnabled(true)
}
}
})
}
}
Here, you can adjust the back button icon and behavior based on the current and previous destinations by setting setDisplayHomeAsUpEnabled, setHomeAsUpIndicator, and setHomeActionContentDescription.

Related

How to save fragment state while using Navigation component and Bottom Navigation?

I have four fragment in bottom navigation and one activity, Bottom navigation is set up using NavController like this
navController = Navigation.findNavController(this, R.id.dashboardNavHostFragment).apply {
setGraph(R.navigation.nav_graph , bundle)
}
bottomNavigationView.setupWithNavController(navController)
I'm passing some data in bundle that I require in my first fragment of bottom nav.
The problem is it works fine the data comes in the fragment like this
private val args: PlayerFeedFragmentArgs by navArgs()
private var data: String? = args.name
but when I navigate using bottom nav and come back to my first fragment the data comes as null
I tried saving data using
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("name", data)
}
and getting data back in onViewCreated but it didn't work because onSaveInstanceState does not get called when switching between fragments of bottom nav.
How can I save the incoming args from the activity in my first fragment so that when I switch fragments from bottom nav it stays the same.
You can try using a SavedStateViewModel handler
https://developer.android.com/reference/kotlin/androidx/lifecycle/SavedStateViewModelFactory
A quick example can be found here:
Using this method, you don't have to share your data with other fragments.

Kotlin delegate disrupting Navigation

I'm trying Jetpack Navigation component and have set up a very basic navigation graph with just 2 Fragments with one home fragment (Foo) containing a button which calls a navigation action to open the other fragment (Bar).
With only the basic Android usage and functions it works as intended, I can navigate back to Foo by pressing the back button and navigate forward to Bar again.
I implemented this convenience delegate class for binding views by id in my preferred way (Im originally an iOS dev).
class FindViewById<in R, T: View>(private val id: Int) {
private var view: T? = null
operator fun getValue(thisRef: R, property: KProperty<*>): T {
var view = this.view
if (view == null) {
view = when (thisRef) {
is Activity -> thisRef.findViewById(id)!!
is Fragment -> thisRef.requireView().findViewById(id)!!
is View -> thisRef.findViewById(id)!!
else -> throw NullPointerException()
}
this.view = view // Comment out to never cache reference
}
return view
}
}
This allows me to write code like this
class FragmentFoo: Fragment() {
private val textView: TextView by FindViewById(R.id.text_view)
private val button: Button by FindViewById(R.id.button)
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
button.setOnClickListener {
findNavController().navigate(R.id.action_foo_to_bar)
}
}
}
Now all of a sudden when I navigate to Bar and then press the back button I arrive at Foo again but I cannot navigate forward to Bar. If I remove the line this.view = view in FindViewById it works again.
My guess is there is some memory related issue, though I tried wrapping the view inside a WeakReference but it didn't solve the problem.
I think it is a good idea performance-wise to cache the found view in the delegate.
Any idea why this is occurring and how I can resolve the problem while caching the found view?
Edit
My intention is not to find another way of referencing views but rather why this delegate implementation disrupts the Navigation component so I don't experience it again if I were to make another custom delegate in the future.
Solution
is Fragment -> {
thisRef.viewLifecycleOwnerLiveData.value!!.lifecycle.addObserver(object: LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_STOP) this#FindViewById.view = null
}
})
return thisRef.requireView().findViewById(id)!!
}
In android Fragment view has its own well-defined lifecycle and this lifecycle is managed independently from that of the fragment's Lifecycle.
When you are using navigation component it uses a fragment replace transaction under the hood and adds the previous fragment to the back stack. At this point this fragment goes into CREATED state and as you can see on this diagram its view is actually destroyed. At this point your delegate still keeps the reference to this old view hierarchy, causing a memory leak.
Later, when you are navigating back, the fragment goes back through STARTED into RESUMED state, but the view hierarchy is recreated - onCreateView and onViewCreated methods are called again during this process. So while the fragment displays a completely new view hierarchy, your delegate still references the old one.
So if you'd like to manually cache any view references, you need to override onDestroyView and clear these references to avoid memory leaks and this kind of incorrect behavior. Also for this particular problem I'd recommend using ViewBinding.
If you'd like to have your own implementation, but do not like to clear references in onDestroyView (e.g. because it breaks nice and self-contained abstraction), viewLifecycleOwnerLiveData might be useful to observe current view state and clear all references when view is destroyed.
Please check out the fragments documentation, it has been recently updated and covers most aspects of fragments.

why my progress bar from my activity doesn't show in my fragment after I press hardware back button?

so I am using navigation controller component in Android. I have a progress bar in my MainActivity that will be used in all my fragments when the user need to wait while fetching data from server.
in my onCreate MainActivity it will be declared like this:
progressBar = findViewById(R.id.progressBar_main_activity)
and in my FragmentA, it will be declared like this :
lateinit var mActivity : FragmentActivity
lateinit var progressBar : ProgressBar
override fun onAttach(context: Context) {
super.onAttach(context)
activity?.let {
mActivity = it
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
fragmentView = inflater.inflate(R.layout.fragment_user_control, container, false)
progressBar = mActivity.progressBar_main_activity
return fragmentView
}
override fun onResume() {
super.onResume()
progressBar.visibility = View.VISIBLE
}
let say I have 2 fragments
I navigate from FragmentA to FragmentB. using the code below
val eventDetailDestination = UserControlFragmentDirections.actionGlobalDestinationEventDetail(selectedEvent)
Navigation.findNavController(fragmentView).navigate(eventDetailDestination)
in FragmentB, after the user do some actions in FragmentB, then they need to go back to FragmentA
here is the problem ....
if the user goes back from FragmentB to FragmentA using back button in the top left corner in action bar/toolbar, the progress bar in FragmentA will show up.
but if the user goes back using hardware back button in the bottom right, the progress bar in FragmentA will never show. even though I am sure progressBar.visibility = View.VISIBLE has been executed in FragmentA ?
I have tried to read the difference between back button in toolbar and hardware back button. but I have no Idea why this happened. please help :)
That happens because fragmentA is getting deleted and recreated from scratch when using the hardware back button which is the expected result. You can override the default behaviour like below:
In MainActivity
override fun onSupportNavigateUp(): Boolean {
//Use component backstack pop
return Navigation.findNavController(fragmentView).navigateUp()
}
Sheding some more light
Up vs. Back
The Up button is used to navigate within an app based on the
hierarchical relationships between screens. For instance, if screen A
displays a list of items, and selecting an item leads to screen B
(which presents that item in more detail), then screen B should offer
an Up button that returns to screen A.
If a screen is the topmost one in an app (that is, the app's home), it
should not present an Up button.
..
The system Back button is used to navigate, in reverse chronological
order, through the history of screens the user has recently worked
with. It is generally based on the temporal relationships between
screens, rather than the app's hierarchy.
When the previously viewed screen is also the hierarchical parent of
the current screen, pressing the Back button has the same result as
pressing an Up button—this is a common occurrence. However, unlike the
Up button, which ensures the user remains within your app, the Back
button can return the user to the Home screen, or even to a different
app.
Reference:
Android Navigation with Back and Up

How to handle navigation properly

I have one question, what should I use to navigate from 1 Activity that hosts multiple fragments.
The goal is 1 Activity that hosts multiple fragments.
I'm using the Navigation Components Architecture
My goal is to know which is the best way to implement the navigation
The currently implemented way of doing navigation is this
class MainMenuActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_menu)
}
override fun onSupportNavigateUp() = findNavController(R.id.nav_host_fragment).navigateUp()
}
Then to navigate between Fragments after inflating the default one is this (From Fragment A to Fragment B
Fragment A : Fragment() {
onViewCreated(...){
btn.setOnClickListener{
findNavController.navigate(R.id.nextAction)
}
From Fragment B to Fragment C
Fragment B : Fragment() {
onViewCreated(...){
btn.setOnClickListener{
findNavController.navigate(R.id.nextAction)
}
My question is, is it a good practice navigating between fragments this way ? Because I feel like Im doing a navigation between fragments but without caring about the Main container Activity.
What I'm thinking to do is something like this
class MainMenuActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_menu)
}
override fun onSupportNavigateUp() = findNavController(R.id.nav_host_fragment).navigateUp()
fun navigateToFragment(id:Int){
findNavController.navigate(id)
}
}
And then call this from each fragment to go to a desired destination
Fragment A : Fragment() {
onViewCreated(...){
btn.setOnClickListener{
requireActivity().navigateToFragment(R.id.nextAction)
}
Is this better to have 1 activity that hosts a stack of Fragments, or its better the first way ?
Doing it the first way I think Im hosting fragments within fragments, making me do childFragmentManager to get the fragment manager of those fragments.
And also makes it harder to extend some methods from the activity itself.
Thanks
First of all, you are doing the same thing in both methods. Calling NavigationController from fragment, activity or any other view if that matters will return you the same NavigationController.
Second of all, the point of Navigation Component is to split navigation from its containing Activity. In fact the direct parent of all your fragments are the NavHostFragment that you have defined in your xml. So, activity has nothing to do with navigating between fragments.
Third, regardless of doing "first way" or "second way" (technically they are same thing as I mentioned in my first point) while navigating it does not mean that you are hosting fragments within fragments. Instead Navigation Component will replace your container with new fragment every time you visit new destination.
And finally, it's better to stick with what the developers suggested. Try reading the documentation and you don't see anywhere where they change destination through Activity.
You can use an interface for communicating with the MainActivity from both fragments and do the fragment transaction from MainActivity.

Signup flow with advanced navigation example

I am currently using bottom navigation like in navigation advanced example, I am trying direct user to signup flow if user is not authenticated. I use following code in side defaultly selected fragment to direct user to sign up flow (login_nav_graph) if they are not authenticated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if(!authenticated){
view.findNavController().navigate(R.id.action_frag1Fragment_to_login_nav_graph)
}
}
But there are few problems
Shows back button when user in signup flow
Bottom navigation is shown in signup flow
These problems make sense, Reasons:
since signup flow (login_nav_graph) is nested inside bottom navs first items(defaultly selected) navigation graph.
Bottom nav is on activity_main layout.
So how could I integrate signup flow into navigation advanced example and overcome above mentioned issues with a better approach?
Note:
code is very similar to navigation advanced example, I introduced separate nav graph for signup flow called login_nav_graph and above mentioned code in defautly selected fragment
Fixed the issue by doing following.
Add login_nav_graph to the nav graph which contains defaultly selected fragment as a nested nav graph.
Create an action/path from defaultly selected fragment (frag1Fragment) to the login_nav_graph and set the Pop To behavior of the action to the frag1Fragment's nav graph.
Create following two methods inside the MainActivity in order to toggle the visibility of the action bar and bottom nav.
fun toggleBottomNavVisibility(){
if(bottom_nav.visibility == View.VISIBLE){
bottom_nav.visibility = View.GONE
}else{
bottom_nav.visibility = View.VISIBLE
}
}
...
fun toggleActionBarVisibility(){
if(supportActionBar!!.isShowing){
supportActionBar?.hide()
}
else{
supportActionBar?.show()
}
}
Update the onViewCreated method of the frag1Fragment as follows
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if(!authenticated){
// hide bottom navigation and action bar
val activity = activity as MainActivity
activity.toggleBottomNavVisibility()
activity.toggleActionBarVisibility()
findNavController().navigate(R.id.action_frag1Fragment_to_login_nav_graph)
}
}

Categories

Resources