I am updating my app to Navigation Architecture Components and I see that it has a lag replacing fragments which is visible in the NavigationDrawer that does not close smoothly.
Until now, I was following this approach:
https://vikrammnit.wordpress.com/2016/03/28/facing-navigation-drawer-item-onclick-lag/
So I navigate in onDrawerClosed instead than in onNavigationItemSelected to avoid the glitch.
This has been a very common issue, but it is back again. Using the Navigation Component, it is laggy again and I don't see a way to have it implemented in onDrawerClosed.
These are some older answers prior to Navigation Component
Navigation Drawer lag on Android
DrawerLayout's item click - When is the right time to replace fragment?
Thank you very much.
I'm tackling this issue as I write this answer. After some testing, I concluded that code I'm executing in fragment right after its created (like initializing RecyclerView adapter and populating it with data, or configuring UI) is causing the drawer to lag as its all happening simultaneously.
Now the best idea I got is similar to some older solutions that rely on onDrawerClosed. We delay the execution of our code in fragment until the drawer has closed. The layout of the fragment will become visible before the drawer is closed, so it will still look fast and responsive.
Note that I'm also using navigation component.
First, we are going to create an interface and implement it fragments.
interface StartFragmentListener {
fun configureFragment()
}
In activity setup DrawerListener like:
private fun configureDrawerStateListener(){
psMainNavDrawerLayout.addDrawerListener(object: DrawerLayout.DrawerListener{
override fun onDrawerStateChanged(newState: Int) {}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
override fun onDrawerOpened(drawerView: View) {}
override fun onDrawerClosed(drawerView: View) {
notifyDrawerClosed()
}
})
}
To notify a fragment that the drawer has been closed and it can do operations that cause lag:
private fun notifyDrawerClosed(){
val currentFragment =
supportFragmentManager.findFragmentById(R.id.psMainNavHostFragment)
?.childFragmentManager?.primaryNavigationFragment
if(currentFragment is StartFragmentListenr && currentFragment != null)
currentFragment.configureFragment()
}
In case you are not navigating to the fragment from the drawer (for example pressing back button) you also need to notify fragment to do its things. We will implement FragmentLifecycleCallbacksListener:
private fun setupFragmentLifecycleCallbacksListener(){
supportFragmentManager.findFragmentById(R.id.psMainNavHostFragment)
?.childFragmentManager?.registerFragmentLifecycleCallbacks(object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentActivityCreated(fm: FragmentManager, f: Fragment, savedInstanceState: Bundle?) {
super.onFragmentActivityCreated(fm, f, savedInstanceState)
if (!psMainNavDrawerLayout.isDrawerOpen(GravityCompat.START)) {
if (f is StartFragmentListener)
f.configureFragment()
}
}
}, true)
}
In fragment:
class MyFragment: Fragment(), MyActivity.StartFragmentListener {
private var shouldConfigureUI = true
...
override fun onDetach() {
super.onDetach()
shouldConfigureUI = true
}
override fun configureFragment() {
if(shouldConfigureUI){
shouldConfigureUI = false
//do your things here, like configuring UI, getting data from VM etc...
configureUI()
}
}
}
A similar solution could be implemented with a shared view model.
Avoid the lag caused while changing Fragment / Activity onNavigationItemSelected- Android
Navigation Drawer is the most common option used in the applications, when we have more than five options then we go towards the navigation menu.
I have seen in many applications that when we change the option from the navigation menu, we observe that it lags, some people on StackOverflow recommended that use Handler like the below code:
private void openDrawerActivity(final Class className) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
ProjectUtils.genericIntent(NewDrawer.this, className, null, false);
}
}, 200);
}
But in the above code, it’s still not smooth & I thought why we add handler may be there is another solution after so much R&D, what I figure it out that we need to change the fragment/activity when the drawer is going to be close. let’s see with the implementation.
For more details with solution kindly go through https://android.jlelse.eu/avoid-the-lag-caused-while-changing-fragment-activity-onnavigationitemselected-android-28bcb2528ad8. It really help and useful.
Hope you find better solutions in it!
Related
Since the old Activity.onBackPressed() becomes deprecated starting Android 33, what is the better way to call it programmatically?
Example:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
// Handle default back arrow click
android.R.id.home -> {
onBackPressed()
}
...
We could create and add OnBackPressedCallback to the onBackPressedDispatcher like this.
onBackPressedDispatcher.addCallback(
this, // Lifecycle owner
backPressedCallback
)
private val backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (viewPager.currentItem != 0)
viewPager.setCurrentItem(0, true)
else
finish()
}
}
Then replace the old onBackPressed with
// Handle default back arrow click
android.R.id.home -> {
backPressedCallback.handleOnBackPressed()
}
But I saw this public method in onBackPressedDispatcher and wondering if I could use it instead.
onBackPressedDispatcher.onBackPressed()
Does this method iterates on each OnBackPressedCallback that has been added in the onBackPressedDispatcher?
So basically onBackPressedDispatcher.onBackPressed() is the same as Activity.onBackPressed() and you can use it in the same manner if you don't care about precise navigation. How do I know that? - well, you can see the source code of the ComponentActivity(basically a parent of a regular Activity you are using), and its onBackPressed() looks like this:
#Override
#MainThread
public void onBackPressed() {
mOnBackPressedDispatcher.onBackPressed();
}
Regarding it calling over the callbacks queue - you are correct also, but it is not iterating over it - but just calling the most recent one - one at a time(per back press or per onBackPressed() call trigger), the documentation states:
public void onBackPressed()
Trigger a call to the currently added callbacks in reverse order in which they were added. Only if the most recently added callback is not enabled will any previously added callback be called.
If hasEnabledCallbacks is false when this method is called, the fallback Runnable set by the constructor will be triggered.
So your strategy here might be like this - if you need some specific stuff to be executed before the back navigation - you add it to the handleOnBackPressed of the callback. If no special behavior needed - you can just call mOnBackPressedDispatcher.onBackPressed() - it will still call the most recently added(if it is there of course) callback method, but if it is empty - the back will work just fine.
You need to keep in mind, though, that there are two overrides of addCallback methods:
addCallback(#NonNull OnBackPressedCallback onBackPressedCallback)
and
public void addCallback(
#NonNull LifecycleOwner owner,
#NonNull OnBackPressedCallback onBackPressedCallback
)
In the former - you have to handle the callback queue by yourself calling remove on callback when you need it not to be executed anymore. In the latter - LifecycleOwner state change has to handle all the needed stuff for you.
More info here and here.
I'm using single activity as a container for my fragments, my second fragment in its onCreate method runs a for loop in coroutine scope, but if the user presses the system's back button the app crashes with null pointer exception...How can I disable the back button functionality until my coroutine job is completed?
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setClickListeners()
//This creates buttons on the layout dynamically
GlobalScope.launch(Dispatchers.Main) {
delay(100)
for (i in 0 until runTillAndHowMany) {
createButton()
delay(25)
}
}
}
I know this is way too wrong to use GlobalScope like this in onCreate, but didn't find any alternative, I want that animation of custom buttons getting created one by one on screen.
I'm using NavigationComponent library, and Transition Animations
You could create a boolean variable to handle this. So in your Activity you can declare it like this:
var shouldGoBack: Boolean = false
And then you override your onBackPressed method to go as follows
override fun onBackPressed() {
if(shouldGoBack)
super.onBackPressed()
}
Finally you access the variable on your Fragment and set it to true once the coroutine is done like this:
(activity as YourActivity).shouldGoBack = true
Let me know if it works!
I have a fragment A which sends a search query to the network, and if the result is positive uses Android navigation component to navigate to fragment B, and its done using observers.
After navigation to fragment B, i click on "<-" arrow on the top of the screen, but instead of navigating back to fragment A it reloads fragment B again. And if using the native "back" button on the device, the app crashes with "illegalArgumentException navigation destination unknown" error.
I check the internet for clues on this issue, but all i learned is that this happens because i am using .observe in onViewCreated() and when i go back, it gets called again, and because livedata has something in it already, it just navigates me back to B.
I have tried observing in onActivityCreated(), and using getViewLifeCycleOwner, but no success... the only thing that helped is checking if livedata has observers and returning if true, before using .observe, but it seems incorrect.
This is the viewModel:
private val getAssetResult = MutableLiveData<GeneralResponse<Asset>>()
private val updateAssetResult = MutableLiveData<GeneralResponse<Int>>()
private val deleteAssetResult = MutableLiveData<GeneralResponse<Int>>()
init {
state.value = ViewState(false)
Log.d(TAG, "State in init: $state")
}
fun getAssetResult(): LiveData<GeneralResponse<Asset>>{
return getAssetResult
}
fun findAsset(req: GetAssetRequest) {
scope.launch {
setProgressIndicator(true)
val result = repository.getAsset(req)
getAssetResult.postValue(result)
setProgressIndicator(false)
}
}
This is the fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(EditAssetViewModel::class.java)
setupViewModel()
initFields()
}
private fun setupViewModel() {
if (viewModel.getAssetResult().hasObservers()) // <- This is the part that prevents the app from crashing.
return
viewModel.getAssetResult().observe(this, Observer {
if (it == null) return#Observer
handleSearchResult(it)
})
if (viewModel.getState().hasObservers())
return
viewModel.getState().observe(this, Observer { handleState(it) })
}
private fun handleSearchResult(response: GeneralResponse<Asset>) {
if (response.singleValue == null) {
Toast.makeText(context!!, response.errorMessage, Toast.LENGTH_SHORT).show()
return
}
targetFragment?.let { it ->
val bundle = bundleOf("asset" to response.singleValue)
when(it) {
"UpdateLocation" ->
Navigation.findNavController(view!!).navigate(R.id.updateLocation, bundle)
"EditAsset" -> {
Navigation.findNavController(view!!).navigate(R.id.editAsset, bundle)
}
}
}
}
if i remove this part from the setupViewModel function:
if (viewModel.getAssetResult().hasObservers())
return
the app will either crash when clicked "back" using the device button or go back to fragment A, just to be navigated back to fragment B because of the .observe function.
Override the method onBackPressed() to handle the "<-" arrow
Seems like the LiveData that you use to signal to fragment A that it should navigate to fragment B is actually an event. An event happens only once and once it is consumed (navigation event is done), it is gone. Therefore, after navigating you need to send a message to the viewmodel that the navigation took place and that the corresponding data holder should be (e.g.) null again. In Fragment A you check that the new value is unequal to null, and only if this is the case, you issue the navigation event. This would prevent fragment A to immediatelly switch to B again in the back scenario.
If you want to learn more about ways to use live data for events, please refer to this article.
I am using the new android Navigation Framework in one of my Applications.
The purpose of Application is to behave as a launcher.
Sometimes when I try to change the fragment (navigate using the navcontroller) it doesn't change the fragment instead it logs
Ignoring navigate() call: FragmentManager has already saved its state
i know this question been asked before here Ignoring navigate() call: FragmentManager has already saved its state
but it doesn't have a Solution.
I am navigating using the following code:
Navigation.findNavController(view).navigate(R.id.action_next, bundle)
I had the same problem, in my case I was trying to use navigate() inside the Mopub ad callback onInterstitialDismissed, and was getting this info.
My solution for this case to use LiveData like this:
private var dismissState = MutableLiveData<Int>(0)
mMobupInterStitialAd?.interstitialAdListener = object : MoPubInterstitial.InterstitialAdListener {
override fun onInterstitialDismissed() {
dismissState.value=1
}
}
override fun onViewCreated() {
dismissState.observe(viewLifecycleOwner) {
if(it == 1) {
findNavController.navigate(R.id.fragmentAtoFragmentB)
}
}
}
This is how I solved the problem.
I have a fragment stack, where I use replace and add together. The code (in my activity) to add or replace my fragment as below
private fun addFragment(fragment: Fragment, name: String) {
supportFragmentManager.beginTransaction().add(R.id.container, fragment)
.addToBackStack(name).commit()
}
private fun replaceFragment(fragment: Fragment, name: String) {
supportFragmentManager.beginTransaction().replace(R.id.container, fragment)
.addToBackStack(name).commit()
}
In my fragment, I do have a toolbar with a back-icon for home menu. Upon clicking, it should help pop up my fragment to the previous stack.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar_actionbar.setNavigationIcon(R.drawable.ic_arrow_back_black_24dp)
setHasOptionsMenu(true)
(activity as AppCompatActivity).setSupportActionBar(toolbar_actionbar)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == android.R.id.home) {
activity?.onBackPressed()
true
} else {
super.onOptionsItemSelected(item)
}
}
Just to be clear, I show activity onBackPressed is coded as below
override fun onBackPressed() {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStackImmediate()
} else {
super.onBackPressed()
}
}
Now if I add fragment1, add fragment2, add fragment3, add fragment4, and then press back, back, back, back... all works fine.
Similarly, if I replace with fragment1, replace with fragment2, replace with fragment3, replace with fragment4, and then back, back, back, back, all works fine.
However if I do, replace with fragment 1, add fragment 2, and replace with fragment 3, then press back, back, back.... The third time press back no longer works. Why??
To illustrate this better, I have put my code in github as below
https://github.com/elye/issue_android_fragment_replace_add_replace
And recorded into a gif below (63 seconds gif) that show, the 4 adds works, 4 replaces works, but the mix of replace and add, will cause the toolbar back button not functioning after few backs.
Note the add fragment shows overlap number as the background is transparent. This is purposely done to easily distinguish add vs replace.
I suspect it's a google bug, but thought should share in case I miss anything important.
After investigation, it does seems like the issue are as describe below.
As each fragment would have it's own action bar set (in this stackoverflow, I simplify it to only R.id.home click for easier illustration of the issue). Each will call the below code when the fragment is added/removed.
(activity as AppCompatActivity).setSupportActionBar(toolbar_actionbar)
When we do add-add-replace (or replace-add-replace).... We have 3 fragments in backstack, but only one visible. Because the last replace, will remove the earlier 2 fragment.
When we click back button, the last replaced fragment will be pop out, and the system then restore the first 2 fragments.
During restoration of the first 2 fragments, the below code get called for both fragments in a short interval
(activity as AppCompatActivity).setSupportActionBar(toolbar_actionbar)
Some how I suspected this cause some behavior where the toolbar of the first fragment was not set fully (the Android SDK bug?).
In order to workaround the issue, when we pop the 2 fragment again, we have to force the 1st fragment to call
(activity as AppCompatActivity).setSupportActionBar(toolbar_actionbar)
With that my workaround for the above issue is to have that code above in onResume().
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).setSupportActionBar(toolbar_actionbar)
}
And whenever a fragment backstack change, I will call the top fragment's onResume
supportFragmentManager.addOnBackStackChangedListener {
val currentFragment = supportFragmentManager.findFragmentById(R.id.container)
currentFragment?.onResume()
}
This now help to ensure regardless of replace-add mixture of stack, the toolbar menu will continue to work.
I have the sample workaround code in
https://github.com/elye/issue_android_fragment_replace_add_replace_workaround
This is really a workaround than a solution. I hope some better answer posted, or if this is really a Google Bug, a fix could be done to it in later SDK release.
Try with this
if (supportFragmentManager.backStackEntryCount > 1) {
supportFragmentManager.popBackStackImmediate()
} else {
super.onBackPressed()
}
Hi put your toolbar code and it's xml part into MainActivity and it's relevent layout and remove from all fargment and it's layout.
And put code like below:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
toolbar_actionbar.setNavigationIcon(R.drawable.ic_arrow_back_black_24dp)
setSupportActionBar(toolbar_actionbar)
}
And below code into MainActivity to solved it.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == android.R.id.home) {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStackImmediate()
} else {
super.onBackPressed()
}
true
} else {
super.onOptionsItemSelected(item)
}
}
I already tested and working perfectly. hope it helps you.
if you have any doubt please comment/message into below i will explain you and solved your problem.