Android: BottomNavigationView first fragment switch is super delayed - android

Attempting to use a bottom nav view, the standard view provided by android. The first time I click on any fragment, it's a SUPER delayed UI reaction time (about 2 seconds until the ripple, item selection update, and new fragment show)
It's only the first time I switch to any fragment, after that, it behaves as expected.
I found a similar question already on here, but there were zero suggestions or answers. See that post here
Find below the logic I user for switching the fragments.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bottom_nav_bar.inflateMenu(R.menu.bottom_nav_bar_menu)
bottom_nav_bar.selectedItemId = R.id.nav_summary
bottom_nav_bar.setOnNavigationItemSelectedListener { menuItem ->
when (menuItem.itemId) {
R.id.nav_1-> startFragment1()
R.id.nav_2 -> startFragment2()
else -> startFragment3()
}
true
}
}
fun startFragment1() = replaceFragment(Fragment1(), "TAG1")
fun startFragment2() = replaceFragment(Fragment2(), "TAG2")
fun startFragment3() = replaceFragment(Fragment3(), "TAG3")
private fun replaceFragment(fragment: Fragment, fragmentTag: String) {
fragmentManager.beginTransaction()
.setCustomAnimations(R.animator.fade_in, R.animator.fade_out)
.replace(R.id.fragment_container, fragment, fragmentTag)
.commit()
}

Use
supportfragmentmanager
instead of just normal FragmentManager. It’s smoother. Of course you have to change to imports to v4 Fragment but after that it should work better. I was having all kinds of weird stuff happen till I made that switch.

Related

PagingDataAdapter stops loading after fragment is removed and added back

I am presenting a PagingSource returned by Room ORM on a PagingDataAdapter.
The RecyclerView is present on a Fragment -- I have two such fragments. When they are switched, they stop loading the items on next page and only placehodlers are shown on scrolling.
Please view these screen captures if it isn't clear what I mean--
When I scroll without switching fragments, all the items are loaded
When I switch Fragments before scrolling all the way down, the adapter stops loading new items
Relevant pieces of code (please ask if you would like to see some other part/file) -
The Fragment:
private lateinit var recyclerView: RecyclerView
private val recyclerAdapter = CustomersAdapter(this)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.recycler_view)
recyclerView.adapter = recyclerAdapter
recyclerView.layoutManager = LinearLayoutManager(context)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.customersFlow.collectLatest { pagingData ->
recyclerAdapter.submitData(pagingData)
}
}
}
View model-
class CustomersListViewModel(application: Application, private val debtOnly: Boolean): ViewModel() {
private val db = AppDatabase.instance(application)
private val customersDao = db.customersDao()
val customersFlow = Pager(PagingConfig(20)) {
if (debtOnly)
customersDao.getAllDebt()
else
customersDao.getAll()
}.flow.cachedIn(viewModelScope)
}
After I went through your code, I found the problem FragmentTransaction.replace function and flow.cachedIn(viewModelScope)
When the activity calls the replace fragment function, the CustomerFragment will be destroyed and its ViewModel will also be destroyed (the viewModel.onCleared() is triggered) so this time cachedIn(viewModelScope) is also invalid.
I have 3 solutions for you
Solution 1: Remove .cachedIn(viewModelScope)
Note that this is only a temporary solution and is not recommended.
Because of this, instances of fragments still exist on the activity but the fragments had destroyed (memory is still leaking).
Solution 2: Instead of using the FragmentTransaction.replace function in the Main activity, use the FragmentTransaction.add function:
It does not leak memory and can still use the cachedIn function. Should be used when the activity has few fragments and the fragment's view is not too complicated.
private fun switchNavigationFragment(navId: Int) {
when (navId) {
R.id.nav_customers -> {
switchFragment(allCustomersFragment, "Customer")
}
R.id.nav_debt -> {
switchFragment(debtCustomersFragment, "DebtCustomer")
}
}
}
private fun switchFragment(fragment: Fragment, tag: String) {
val existingFragment = supportFragmentManager.findFragmentByTag(tag)
supportFragmentManager.commit {
supportFragmentManager.fragments.forEach {
if (it.isVisible && it != fragment) {
hide(it)
}
}
if (existingFragment != fragment) {
add(R.id.fragment_container, fragment, tag)
.disallowAddToBackStack()
} else {
show(fragment)
}
}
}
Solution 3: Using with Navigation Component Jetpack
This is the safest solution.
It can be created using Android Studio's template or some of the following articles.
Navigation UI
A safer way to collect flows
I tried solution 1 and 2 and here is the result:

Move between fragments without recreation- Kotlin

I'm trying to move between two fragments without recreation of them so the data in the previous fragment won't disappear.
I tried to look over the internet for answers and tried for hours but without success. I looked at those links:
link 1
link 2
link 3- android developer site
After show() and hide() I also tried the AddToBackStack() but yet no success
link 4
class MainActivity : AppCompatActivity(){
private val onNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
#Override
when (item.itemId) {
R.id.navigation_home -> {
//replaceFragment(SignInFragment())
supportFragmentManager.beginTransaction().hide(AllEventsFragment()).commit()
supportFragmentManager.beginTransaction().show(SignInFragment()).commit()
return#OnNavigationItemSelectedListener true
}
R.id.navigation_events -> {
//replaceFragment(AllEventsFragment())
supportFragmentManager.beginTransaction().hide(SignInFragment()).commit()
supportFragmentManager.beginTransaction().show(AllEventsFragment()).commit()
if (currentUser.isNotEmpty()) {
updateRecyclerView()
sign_in_error?.visibility = View.INVISIBLE
}
return#OnNavigationItemSelectedListener true
}
}
return#OnNavigationItemSelectedListener false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().add(R.id.activity_main, AllEventsFragment(), "2").commit()
supportFragmentManager.beginTransaction().add(R.id.activity_main, SignInFragment(), "1").commit()
val navView: BottomNavigationView = findViewById(R.id.nav_view)
navView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener)
personInEvent = false
}
The result is overlapping fragments without an option to really navigate between them. I really tried everything I know there are some answers over the internet but none of them helped me fix my issue. I would really appreciate some help with this frustrating issue.
Before navigation:
After navigation:
supportFragmentManager.beginTransaction().hide(AllEventsFragment()).commit()
your recreating your fragments every time!
calling AllEventsFragment() is equivelant to new AllEventsFragment()
you need to instantiate them first
for example,
your code needs to be like this,
val fragment1: Fragment = SignInFragment()
val fragment2: Fragment = AllEventsFragment()
var active = fragment1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().add(R.id.activity_main,fragment2 , "2").commit()
supportFragmentManager.beginTransaction().add(R.id.activity_main, fragment1, "1").commit()
val navView: BottomNavigationView = findViewById(R.id.nav_view)
navView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener)
personInEvent = false
}
inside your listener
R.id.navigation_home -> {
supportFragmentManager.beginTransaction().beginTransaction().hide(active).show(fragment1).commit();
active = fragment1;
return#OnNavigationItemSelectedListener true
}
R.id.navigation_events -> {
//replaceFragment(AllEventsFragment())
supportFragmentManager.beginTransaction().beginTransaction().hide(active).show(fragment2).commit();
active = fragment2
)
//handle rest of the cases
Take a look at architicture components, yuo can also achieve that in the old way Android - save/restore fragment state
When a fragment isnt viewable its paused or might even be destroyed use bundle to perserve data.
What you are trying to acheive could be done using two containers but you really shouldn't

Fragment replace() operating differently to remove() + add()

According to the Android docs:
replace() is essentially the same as calling remove(Fragment) for all
currently added fragments that were added with the same
containerViewId and then add(int, Fragment, String) with the same
arguments given here.
However my code says otherwise.
My app has 1 activity and multiple fragments. It also has a BottomNavigationView with 3 tabs (Options, Game, Leaderboards).
When the MainActivity is initialised, 3 fragments are added to the container FrameLayout MainActivity. Immediately after being added, 2 fragments are hidden which leaves 1 shown on the screen (the opening fragment).
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
val transaction = supportFragmentManager.beginTransaction()
transaction.add(R.id.fragment_layout, firstFragment, "Opening")
transaction.add(R.id.fragment_layout, OptionsFragment(), "Options")
transaction.add(R.id.fragment_layout,LeaderboardsFragment(), "Leaderboards")
transaction.commitNow()
val transaction2 = supportFragmentManager.beginTransaction()
val options: androidx.fragment.app.Fragment = supportFragmentManager.findFragmentByTag("Options")!!
val leaderboards: androidx.fragment.app.Fragment = supportFragmentManager.findFragmentByTag("Leaderboards")!!
transaction2.hide(options)
transaction2.hide(leaderboards)
transaction2.commitNow()
}
From here on out, each fragment that isn't OptionsFragment() or LeaderboardsFragment() is swapped out via replace().
OpeningFragment.kt
transaction?.replace(R.id.fragment_layout, playerDetailsFragment, "Add Player")
transaction?.commit()
gameString = "Add Player"
OptionsFragment() and LeaderboardsFragment() are put into view by being selected from the bottom nav, which then hides every other fragment except for the one selected (e.g if Options is selected from bottom nav, then every fragment is hidden except for OptionsFragment()).
MainActivity.kt
override fun onNavigationItemSelected(item: MenuItem): Boolean {
val fragmentTags = arrayListOf("Opening", "Leaderboards", "Options", "Add Player", "Question", "Location", "Game Over")
val transaction = supportFragmentManager.beginTransaction()
val selectedFragmentTag = when (item.itemId) {
R.id.action_leaderboards -> "Leaderboards"
R.id.action_options -> "Options"
else -> {
gameString
}
}
// Selected Fragment
val selectedFragment: androidx.fragment.app.Fragment? = supportFragmentManager.findFragmentByTag(selectedFragmentTag)
for (fragment_tag in fragmentTags){
// Hide every Fragment that has been added and isn't the selected Fragment
val fragment = supportFragmentManager.findFragmentByTag(fragment_tag)
if (fragment != null && fragment != selectedFragment) {
transaction.hide(fragment)
}
}
transaction.show(selectedFragment!!)
transaction.commit()
return true
}
This method doesn't work. However - if I use remove() and add() instead of replace(), then it works fine:
OpeningFragment.kt
val opening = fragmentManager?.findFragmentByTag("Opening")
transaction?.remove(opening!!)
transaction?.add(R.id.fragment_layout, playerDetailsFragment, "Add Player")
transaction?.commit()
gameString = "Add Player"
Any idea why this is?
For me replace works exactly as docs says. It removes all fragments in given container id and then adds new fragment with specified tag.
Your optional code removes only one fragment that you desire, not all of them, therefore it's not identical to replace function.

Why does hiding a Fragment cause a null pointer exception?

Update 1:
I saw this answer which solved the issue but I don't understand why and feel like this guy.
Both these return a non-null InfoboxFragments but the transaction fails with the first one.
childFragmentManager.findFragmentById(R.id.infobox_fragment)
childFragmentManager.findFragmentByTag("plz")
Excuse me?!
Update 2:
childFragmentManager does not play nice with nested fragments in xml.
I've been trying to hide a Fragment inside of another Fragment for a while. I've tried a few approaches and reducing the child fragments height to 1dp seems the most reliable, but it leaves a bit of the fragment visible.
So, I tried a recommended approach of using the childFragmentManager with transactions to hide the fragment like below.
The Android docs says this about the hide(fragment) method:
Hides an existing fragment. This is only relevant for fragments whose
views have been added to a container, as this will cause the view to
be hidden
So, I've contained the fragment inside a LinearLayout.
The showInfoBox() works fine but the hideInfoBox() crashes with a NPE:
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.ViewGroup.startViewTransition(android.view.View)' on a null object reference
at android.support.v4.app.FragmentManagerImpl.completeShowHideFragment(FragmentManager.java:1681)
at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1778)
The only difference seems to be the hide and show transactions.
private fun showInfoBox() {
val infoboxFragment = childFragmentManager.findFragmentById(R.id.infobox_fragment) as InfoboxFragment
childFragmentManager.beginTransaction()
.setCustomAnimations(android.R.animator.fade_in,
android.R.animator.fade_out)
.show(infoboxFragment)
.commit()
TransitionManager.beginDelayedTransition(constraintLayout)
applyConstraintSet.applyTo(constraintLayout)
}
and
private fun hideInfoBox() { // this crashes
val infoboxFragment = childFragmentManager.findFragmentById(R.id.infobox_fragment) as InfoboxFragment
childFragmentManager.beginTransaction()
.setCustomAnimations(android.R.animator.fade_in,
android.R.animator.fade_out)
.hide(infoboxFragment) // with only this difference
.commit()
}
The fragment is added like so:
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val infoboxFragment = InfoboxFragment()
with(childFragmentManager) {
beginTransaction().add(R.id.infobox_fragment_container, infoboxFragment).commit()
//beginTransaction().hide(infoboxFragment).commit()
}
resetConstraintSet.clone(constraintLayout)
applyConstraintSet.clone(constraintLayout)
//applyConstraintSet.setVisibility(R.id.infobox_fragment_container, View.VISIBLE)
applyConstraintSet.constrainHeight(R.id.infobox_fragment_container, 250.toDP())
}

Fragment instance is retained but child fragment is not re-attached

Update: accepted answer points to explanation (bug) with a work-around, but also see my Kotlin based work-around attached as an answer below.
This code is in Kotlin, but I think it is a basic android fragment life-cycle issue.
I have a Fragment that holds a reference to an other "subfragment"
Here is basically what I am doing:
I have a main fragment that has retainInstance set to true
I have a field in the main fragment that will hold a reference to the subfragment, initially this field is null
In the main fragment's onCreateView, I check to see if the subfragment field is null, if so, I create an instance of the subFragment and assign it to the field
Finally I add the subfragment to a container in the layout of the main fragment.
If the field is not null, ie we are in onCreateView due to a configuration change, I don't re-create the subfragment, I just try to added it to the containter.
When the device is rotated, I do observe the onPaused() and onDestroyView() methods of the subfragment being called, but I don't see any lifecyle methods being called on the subfragment during the process of adding the retained reference to the subfragment, to the child_container when the main fragments view is re-created.
The net affect is that I don't see the subfragment view in the main fragment. If I comment out the if (subfragment == null) and just create a new subfragment everytime, i do see the subfragment in the view.
Update
The answer below does point out a bug, in which the childFragmentManager is not retained on configuration changes. This will ultimately break my intended usage, which was to preserve the backstack after rotation, however I think what I am seeing is something different.
I added code to the activities onWindowFocusChanged method and I see something like this when the app is first launched:
activity is in view
fm = FragmentManager{b13b9b18 in Tab1Fragment{b13b2b98}}
tab 1 fragments = [DefaultSubfragment{b13bb610 #0 id=0x7f0c0078}]
and then after rotation:
activity is in view
fm = FragmentManager{b13f9c30 in Tab1Fragment{b13b2b98}}
tab 1 fragments = null
here fm is the childFragmentManager, and as you can see, we still have the same instance of Tab1Fragment, but it has a new childFragmentManager, which I think is unwanted and due to the bug reported in the answer below.
The thing is that I did add the subfragment to this new childFragmentManger.
So it seems like the transaction never executes with the reference to the fragment that was retained, but does complete if I create a brand new fragment. (I did try calling executePendingTransactions on the new childFragmentManager)
class Tab1Fragment: Fragment() {
var subfragment: DefaultSubfragment? = null
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val rootView = inflater!!.inflate(R.layout.fragment_main, container, false)
if (subfragment == null ) {
subfragment = DefaultSubfragment()
subfragment!!.sectionLabel = "label 1"
subfragment!!.buttonText = "button 1"
}
addRootContentToContainer(R.id.child_container, content = subfragment!!)
return rootView
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
inline fun Fragment.addRootContentToContainer(containerId: Int, content: Fragment) {
val transaction = childFragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.commit()
}
Your problem looks similar to the issue described here:
https://code.google.com/p/android/issues/detail?id=74222
unfortunately this issue will probably not be fixed by google.
Using retained fragments for UI or nested fragments is not a good idea - they are recomended to be used in place of onRetainNonConfigurationInstance, so ie. for large collections/data structures. Also you could find Loaders better than retained fragments, they also are retained during config changes.
btw. I find retained fragments more of a hack - like using android:configChanges to "fix" problems caused by screen rotations. It all works until user presses home screen and android decides to kill your app process. Once user will like to go back to your app - your retained fragments will be destroyed - and you will still have to recreate it. So its always better to code everything like if your resources could be destroyed any time.
The accepted answer to my question above points out a reported bug in the support library v4 in which nested fragments (and child fragment managers) are no longer retained on configuration changes.
One of the posts provides a work-around (which seems to work well).
The work around involves creating a subclass of Fragment and uses reflection.
Since my original question used Kotlin code, I thought I would share my Kotlin version of the work around here in case anyone else hits this. In the end, I am not sure I will stick with this solution, since it is still somewhat of a hack, it still manipulates private fields, however if the field name is changed, the error will be found at compile time rather than runtime.
The way this works is this:
In your fragment that will contain child fragments you create a field retainedChildFragmentManager, that will hold the childFragmentManager that will be lost during the configuration change
In the onCreate callback for the same fragment, you set retainInstance to true
In the onAttach callback for the same fragment, you check to see if retainedChildFragmentManger is non-null, if so you call a Fragment extension function that re-attaches the retainedChildFragmentManager, otherwise you set the retainedChildFragmentManager to the current childFragmentManager.
Finally you need to fix the child fragments to point back to the newly created hosting activity (the bug leaves them referencing the old activity, which I think results in a memory leak).
Here is an example:
Kotlin Fragment extensions
// some convenience functions
inline fun Fragment.pushContentIntoContainer(containerId: Int, content: Fragment) {
val transaction = fragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.addToBackStack("tag")
transaction.commit()
}
inline fun Fragment.addRootContentToContainer(containerId: Int, content: Fragment) {
val transaction = childFragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.commit()
}
// here we address the bug
inline fun Fragment.reattachRetainedChildFragmentManager(childFragmentManager: FragmentManager) {
setChildFragmentManager(childFragmentManager)
updateChildFragmentsHost()
}
fun Fragment.setChildFragmentManager(childFragmentManager: FragmentManager) {
if (childFragmentManager is FragmentManagerImpl) {
mChildFragmentManager = childFragmentManager // mChildFragmentManager is private to Fragment, but the extension can touch it
}
}
fun Fragment.updateChildFragmentsHost() {
mChildFragmentManager.fragments.forEach { fragment -> // fragments is hidden in Fragment
fragment?.mHost = mHost // mHost is private also
}
}
The Fragment Hosting the child Fragments
class Tab1Fragment : Fragment() , TabRootFragment {
var subfragment: DefaultSubfragment? = null
var retainedChildFragmentManager: FragmentManager? = null
override val title = "Tab 1"
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val rootView = inflater!!.inflate(R.layout.fragment_main, container, false)
if (subfragment == null ) {
subfragment = DefaultSubfragment()
subfragment!!.sectionLable = "label 1x"
subfragment!!.buttonText = "button 1"
addRootContentToContainer(R.id.child_container, content = subfragment!!)
}
return rootView
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (retainedChildFragmentManager != null) {
reattachRetainedChildFragmentManager(retainedChildFragmentManager!!)
} else {
retainedChildFragmentManager = childFragmentManager
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
}

Categories

Resources