I'm using BottomAppbar with floating action button in single activity.The Fab has different responsibility in different fragment.
HomeFragment -> adds new item
AddProblemFragment ->saves item(also icon changes)
DetailsFragment -> adds items to selected collections(also icon changes)
I have tried to use listener between activity and fragments. Icon and position changes perfectly but I couldn't handle calling methods that in fragments when clicked.
To solve clicking problem I'm getting fab reference from activity with
fab = requireActivity().findViewById(R.id.fab) in each fragment (in onViewCreated).
So, problem is after orientation changes gives
java.lang.NullPointerException: requireActivity().findViewById(R.id.fab) must not be null
Is there way to avoid getting this error after orientation changes? or Which lifecycle should choose to initialize fab?
Is it possible to initialize fab in BaseFragment and use it because almost each fragment have fab?
The Code Snippets:
//Each fragment same approach
private lateinit var fab:FloatingActionButton
override fun onViewCreated(view:View,savedInstanceState:Bundle?){
fab = requireActivity().findViewById(R.id.fab_btn_add)
fab.setImageResource(R.drawable.ic_related_icon)
fab.setOnClickListener {
//calls method
}
First you need to create an interface for you fab click listener for Example:
interface FabClickCallback {
void onFabClick()
}
and on your activity,
You Need create an instance from this Callback
private FabClickCallback fabCallback;
and you want create new method inside your Activity called for Example:
public void handleFabClickListener(FabClickCallback callback){
// TODO Here put callback
this.fabCallback = callback;
}
and you want trigger this callback when user click on Fab Button like This:
fab.setOnClickListener ( view -> {
if (this.fabCallback != null ) {
this.fabCallback.onFabClick();
}
});
finally on Each Fragment you want handle Fab Click you want call this function and pass your actual fab click code :
((YourActivity) requireActivity()).handleFabClickListener(this);
and your fragment must implements FabClickCallback interface
I have three top-level destination fragments in my activity:
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.trackerFragment,
R.id.nutrientFragment,
R.id.settingsFragment
),
drawerLayout
)
Which means all these fragments will show the hamburger button instead of the normal back arrow. The problem is that I have a "Go to Nutrient" button in my App that navigates to NutrientFragment, which is a top level destination. I would like that this fragment still showed the hamburger icon when navigating with the drawer, but a normal back/up button when navigating through the "Go to Nutrient" button.
My initial idea was to pass a boolean value as an argument when navigating to NutrientFragment, and inside onCreateView(), depending on the value of the boolean, remove the hamburger icon or add the back button. But that seems a little ugly and inefficient, specially if in the future I add more destinations that could have the same problem. I also have no idea how to "remove the hamburger icon and replace with the normal back button" either. Any idea on how to implement that? All the answers that I found are either in old deprecated java code or not quite what I am looking for.
Thanks.
Given:
Now you have a navigation drawer with three fragments (trackerFragment, nutrientFragment, & settingsFragment).
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.trackerFragment,
R.id.nutrientFragment,
R.id.settingsFragment
),
drawerLayout
)
Required:
You want to keep the burger in the home fragment (NutrientFragment) and replace it with the UP button for some fragment.
Solution:
Assuming that settingsFragment is the fragment is that you need to have the UP button instead of the burger.
First remove this fragment from the appBarConfiguration:
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.trackerFragment, R.id.nutrientFragment
), drawerLayout
)
Then, in order to control the Burger button, you need to sync the drawer layout and the SupportActionBar with an ActionBarDrawerToggle and here is a method for that:
fun getActionBarDrawerToggle(
drawerLayout: DrawerLayout,
toolbar: Toolbar
): ActionBarDrawerToggle {
val toggle = ActionBarDrawerToggle(
this,
drawerLayout,
toolbar,
R.string.open,
R.string.close
)
drawerLayout.addDrawerListener(toggle)
toggle.syncState()
return toggle
}
R.string.open & R.string.close are notation strings, you can create them in strings.xml:
<resources>
<string name="open">Open</string>
<string name="close">Close</string>
</resources>
And the toolBar is the Toolbar which is inflated from the layout and set as the supportActionBar with setSupportActionBar()
Then, add a listener to the navigation to detect when the settingFragment is selected, so that you can disable the toggle functionality of the burger using toggle.isDrawerIndicatorEnabled:
val toggle = getActionBarDrawerToggle(drawerLayout, toolbar) // replace the `drawerLayout` & `toolbar` according to yours
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.settingsFragment)
toggle.isDrawerIndicatorEnabled = false
}
Of course, now the UP button won't work whenever you click on it, to fix this we need to setToolbarNavigationClickListener in order to enable the toggle functionality and navigate back to the Home fragment (assuming the home fragment is nutrientFragment):
toggle.setToolbarNavigationClickListener {
toggle.isDrawerIndicatorEnabled = true
navController.navigate(R.id.nutrientFragment)
}
Preview:
Here's the Gallery fragment is where is the UP button is placed:
I managed to find a solution. I added this variable to my MainActivity:
// If true, disable drawer and enable back/up button
private var isUpButton = false
And these methods:
// Disable drawer and enable the up button
fun useUpButton() {
supportActionBar?.setHomeAsUpIndicator(
androidx.appcompat.R.drawable.abc_ic_ab_back_material
)
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
isUpButton = true
}
// Enable the drawer and disable up button
private fun useHamburgerButton() {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
isUpButton = false
}
useUpButton() replaces the hamburger icon with the default up icon, locks the drawer slider and sets isUpButton to true. I also overrode onOptionsItemsSelected:
// If isUpButton is true, and the home button is clicked, navigate up and
// enable drawer again, if false, just normal drawer behaviour
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (isUpButton && item.itemId == android.R.id.home) {
navController.navigateUp()
useHamburgerButton()
true
} else super.onOptionsItemSelected(item)
}
So if the home button (either the hamburger icon or up icon) is clicked, and isUpButton is set to true, the activity will navigate up, just like normal up behaviour, and reset the isUpButton to false, so other fragments can enjoy the drawer again.
Now all I need to do is use these methods to solve my problem: I want the NutrientFragment to show the "up button" ONLY when I navigate to it WITHOUT using the drawer. To do that I need to create an argument to the NutrientFragment destination on my nav_graph.xml:
<fragment
android:id="#+id/nutrientFragment"
android:name="com.example.elegantcalorietracker.ui.fragments.NutrientFragment"
android:label="#string/nutrients"
tools:layout="#layout/fragment_nutrient">
<action
android:id="#+id/action_nutrientFragment_to_trackerFragment"
app:destination="#id/trackerFragment" />
<argument
android:name="upButtonNeeded"
android:defaultValue="false"
app:argType="boolean" />
And add this to the onCreateView() method on NutrientFragment:
upButtonNeeded = arguments?.getBoolean("upButtonNeeded") ?: false
if (upButtonNeeded) {
(activity as MainActivity).useUpButton()
}
Now, if I have a button in another fragment that navigates to NutrientFragment, all I need to do is pass true as an argument to the navigate method:
val argument = bundleOf("upButtonNeeded" to true)
this#TrackerFragment
.findNavController()
.navigate(
R.id.action_trackerFragment_to_nutrientFragment,
argument
)
Result:
When navigating to NutrientFragment using the drawer, the hamburger icon shows normally, but when navigating to it another way, like a button, the up button shows instead:
I am trying to see if it is possible to create an observable, that would notify when action bar visibility changes.
Something in a way of
LiveData<Boolean> actionBarVisibility;
So that other UI can be updated when actionBar is shown/hidden?
I found this little trick to identify the view visibility change events, but cannot figure out how to apply it to actionBar, since its view is not accessible to me.
Yes, you can observe the state of Boolean!
Inside ViewModel
var isActionBarVisible: MutableLiveData<Boolean> = MutableLiveData()
set visibility
isActionBarVisible.postValue(true)
set invisibility
isActionBarVisible.postValue(false)
====
Inside View (Activity or Fragment)
viewProvider!!.isActionBarVisible.observe(this, Observer {
if (it!!) {
// on visible of action bar
} else {
// on invisible of action bar
}
})
mDrawerLayout = findViewById(R.id.drawer_layout)
val navigationView: NavigationView = findViewById(R.id.nav_view)
navigationView.setNavigationItemSelectedListener { menuItem ->
// set item as selected to persist highlight
menuItem.isChecked = true
// close drawer when item is tapped
mDrawerLayout.closeDrawers()
// Add code here to update the UI based on the item selected
// For example, swap UI fragments here
true
}
I have a navigation drawer in my app, and I can access it and click on the items listed there. However, what is the code I need to add above so that when an item is clicked, it opens up a new activity? I have 10 activities and don't know anything about fragments yet so have to set it so it opens a new activity for now.
#override
public boolean onNavigationItemSelected(MenuItem item)
{int id = item.getItemId();
// then use if and else to know which is selected}
'
I am new to Android development. I want to use Espresso to test that my drawer opens, then click on an item and check that it opens a new activity. I've been searching for examples about this but haven't had any luck.
#Test
public void clickOnYourNavigationItem_ShowsYourScreen() {
// Open Drawer to click on navigation.
onView(withId(R.id.drawer_layout))
.check(matches(isClosed(Gravity.LEFT))) // Left Drawer should be closed.
.perform(DrawerActions.open()); // Open Drawer
// Start the screen of your activity.
onView(withId(R.id.nav_view))
.perform(NavigationViewActions.navigateTo(R.id.your_navigation_menu_item));
// Check that you Activity was opened.
String expectedNoStatisticsText = InstrumentationRegistry.getTargetContext()
.getString(R.string.no_item_available);
onView(withId(R.id.no_statistics)).check(matches(withText(expectedNoStatisticsText)));
}
This does exactly what you are looking for.
Other examples are available here or here
Incase someone else lands to this question and he/she is using kotlin you can add extension function on ActivityScenario class to grab drawer icon or back button
For more description check this GOOGLE-TESTING-CODELAB-8
fun <T: Activity> ActivityScenario<T>.getToolbarNavigationContentDescriptor(): String {
var description = ""
onActivity {
description = it.findViewById<Toolbar>(R.id.toolbar).navigationContentDescription as String
}
return description
}
On your tests, you can do something like this
// Check drawer is closed
onView(withId(R.id.drawer_layout)).check(matches(isClosed(Gravity.START)))
// Click drawer icon
onView(
withContentDescription(
scenario.getToolbarNavigationContentDescriptor()
)
).perform(click())
// Check if drawer is open
onView(withId(R.id.drawer_layout)).check(matches(isOpen(Gravity.START)))
Happy coding . . .