Fragment Transaction Issue in Android - android

I have a Fragment A in My app.
From the Fragment A, I am moving to Fragment B.
Note that I am adding the Fragment B on Fragment A (not replacing the Fragment.)
Now, When I coming back to Fragment A from Fragment B, I have to call a method of Fragment A and that's Why I am calling that method in life cycle method of Fragment A : in onResume(), onViewCreated()
But What I noticed by putting log in both the method that these methods are not calling means I am not getting Log in logcat and the method which I have called in these two methods is not executing.
What might be the issue? Thanks.
I am doing as below for your reference:
override fun onResume() {
super.onResume()
Log.e("onResume 1","onResume 1")
(activity as HomeActivity).updateToolbar(false)
setLanguageSpecificData()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.e("onViewCreated","onViewCreated")
setLanguageSpecificData()
}

Problem:
The way you mentioned in your question that:
I am adding the Fragment B on Fragment A (not replacing the Fragment.)
So, There is a difference between what lifecycle methods gets called based on replace & add.
If you take a look at this answer :https://stackoverflow.com/a/21684520/9715339
In terms of fragment's life cycle events onPause, onResume,
onCreateView and other life cycle events will be invoked in case of
replace but they wont be invoked in case of add
Also, It's tied to activity lifecycle as well.
Solution:
Looking at your code you want to update something when current visible fragment changes or in other words backstack changes.
For that you can do this in your activity:
supportFragmentManager.addOnBackStackChangedListener {
//when backstack changes get the current fragment & do something with that info
val frg = supportFragmentManager.findFragmentById(R.id.root_container)
if (frg is AFragment){
// do something like updateToolbar(false)
} else if (frg is BFragment){
//something else
}
Log.d("FrgTest-",frg.toString())
}
In fragment you can do requireActivity().supportFragmentManager & rest will be fine.
Read:
This is just an example for backstack change. If you want to communicate between fragments you can use other ways like setFragmentResultListener as well.
This may help: https://developer.android.com/guide/fragments/communicate#kotlin

Related

Problem going back from current Fragment back to the replaced Fragment when using NavHostFragment

I have three fragments A, B and C. And I'm using navHostFragment container in MainActivity. So the application goes from A -> B using kotlin extension function findNavController().navigate... and then go from B to C using same function. All works fine till here.
Now in Fragment C, I'm replacing different elements on fragment C using
activity?.supportFragmentManager
?.beginTransaction()
?.replace(R.id.list_container, someFragment)
?.addToBackStack("some_frag_id")
?.commit()
The list_container is replaced with someFragment. After this when I press physical back button Fragment C pops out and my app goes to Fragment B while what I expect it to restore replaced list_container i.e. whatever was there before replacement.
I'm also overiding this in my MainActivity
override fun onBackPressed() {
val count = supportFragmentManager.backStackEntryCount
if (count == 0) {
super.onBackPressed()
//additional code
}
else {
supportFragmentManager.popBackStack()
}
}
I'm not sure what is missing here. I have read a lot of solutions on stackoverflow but none worked to my satisfaction. Please guide.
If you are adding a Fragment to a View within a Fragment, you must always use the childFragmentManager - using activity?.supportFragmentManager is always the wrong FragmentManager to use in that case.
Besides fixing cases with restoring state (which would not work when using the wrong FragmentManager), this also ensures that the default behavior for dispatching onBackPressed() down the FragmentManager hierarchy will work out the box - you should not need any logic at all in onBackPressed() to have the pop work correctly.
If you need to intercept the back button in Fragment C, you should follow the providing custom back documentation to register an OnBackPressedDispatcher - you should not override onBackPressed() even in those cases.

The Fragment refreshes on back- Android Navigation

I am making an application using the Android Navigation component. But I ran into a very fundamental problem which can cause problems in the whole development of my application.
The Scenario
I have this Fragment where in onViewCreated I am observing a field from my viewmodel.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(EventDetailsViewModel::class.java)
viewModel.init(context!!,eventId)
viewModel.onEventDetailsUpdated().observe(this, Observer {
setEventDetails(it)
})
}
And in the setEventDetails method, I set recyclerviews with the data.
The PROBLEM
This fragment is a long fragment with a scroll. Suppose I scroll long way down to a section and click on a button which takes me to another fragment.
But when I come back to this fragment, it again takes me to the top and does everything that it did on first load.
That can be troubling. It is kind of recreating the whole fragment instead of keeping its old state.
What I tried
I searched a lot of questions. And went through This Github Query, This SO question, Another Git... But I could not solve my problem.
Please help, Thanks in advance.
Yes, Fragment's view will get destroyed whenever you navigate forward to another fragment.
RecyclerView's scroll position should be automatically restored, even when new instance of RecyclerView is created and new Adapter instance is set, as long as you setup everything with the same dataset as before. Also, you need to do it before the first layout pass.
This means that you need your old data and you need to have it ready immediately (no async loads!).
ViewModelProvider should return the same ViewModel instance. That ViewModel holds the data you should be able to synchronously get and display on the UI. Make sure to refactor your viewModel.init method - you don't want to make API call if data is already there in case when going back. A simple boolean isInitialized can work here, or you can even check if LiveData is empty or not.
Also, you have a subtle bug when calling observe on LiveData. onViewCreated can be called many times for the same fragment (each time you navigate forward and back!) - so observe will be called each time. Your Fragment will be subscribed many times to the same LiveData. This means you will get events multiple times (once for each subscription). This can cause issues with RecyclerView state restoration too. Your subscription is tied to Lifecycle owner you passed. You passed Fragment's Lifecycle owner which is tied to Fragment's lifecycle. What you want to do is pass Fragment view's lifecycle owner, so whenever the view is destroyed the subscription gets cleared, and you only have 1 subscription ever and only while the Fragment's view is alive. For this, you can use getViewLifecycleOwner instead of this.
You need to rely on ViewModel to restore the fragment state because ViewModel doesn't get destroyed on fragment change.
In your viewModel, create a variable listState
class HomeViewModel : ViewModel() {
var listState: Parcelable? = null
}
Then in your fragment use below code
class HomeFragment : Fragment() {
private val viewModel by navGraphViewModels<HomeViewModel>(R.id.mobile_navigation)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (viewModel.listState != null) {
list.layoutManager?.onRestoreInstanceState(viewModel.listState)
viewModel.listState = null
}else{
//load data normally
}
override fun onDestroyView() {
super.onDestroyView()
viewModel.listState = list.layoutManager?.onSaveInstanceState()
}
}
You don't have to initialize the view model each time. Just check for null before initializing. Don't know kotlin, still it will be something like:
if(viewModel == null){
viewModel = ViewModelProviders.of(this).get(EventDetailsViewModel::class.java)
viewModel.init(context!!,eventId)
}
try putting this code where you first call your fragment.
ft = fm.beginTransaction();
ft.replace(R.id.main_fragment, yourSearchFragment, "searchFragment");
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.commit();
and this when going back to the fragment
ft = fm.beginTransaction();
ft.hide(getFragmentManager().findFragmentByTag("searchFragment"));
ft.add(R.id.main_fragment, yourDetailfragment);
ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.commit();

Managing Fragments on Android Foldable

I am creating a new Android Project and soon android foldable devices will be launched. I have an Activity which has fragment called first fragment.
First Fragment has a button called first button which open second fragment which has a button called second and on click of second, third fragment opens.
Suppose user is in third fragment and user decides to unfold his device, will the user go back to fragment one or will he stay in fragment three. As far as I have understood from the Developer Summit, the activity will be destroyed and recreated when user unfolds his device so technically user goes backs to first fragment leading to poor user experience.
So my question is should I consider even using fragments?, If yes how to manage state so that user goes to the same fragment he was when he folds or unfolds his device.
Following is my code if I am changing fragments
private fun displayView(fragment: Fragment?, title: String) {
if (fragment != null) {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.beginTransaction()
.replace(R.id.framelayout_activity_main, fragment, title).commit()
}
}
In onCreate(), you only want to execute a FragmentTransaction if this activity is being
newly created, instead of being recreated from a configuration change. Or, more accurately,
you only want to execute a FragmentTransaction if you do not already have fragments in the state that you want them.
So, a typical approach is to see if you already have a fragment in your container:
override fun onCreate(state: Bundle) {
super.onCreate(state)
if (supportFragmentManager.findFragmentById(R.id.framelayout_activity_main) == null) {
// do something to show your fragment
}
// other good stuff goes here
}
On the first onCreate() invocation, findFragmentById() will return null, so you execute your code to display your first fragment. On a subsequent onCreate() invocation after a configuration change, Android will have already set up your fragment(s) for you by the time onCreate() is called. So, in that case, findFragmentById() will return something other than null, so you know that you already have a fragment in your container and do not need to do anything more.

Fragment lifecycle overlap on navigate

I have a single Activity application with multiple Fragments that are being switched by using Navigation components. When I switch between two fragments their onCreate() and onDestroy() methods seem to overlap. Thus making it difficult for me to write initialization and clean up code for fragments when they access the same global objects.
Navigating from Framgent_A to Fragment_B has the following order of methods:
Fragment_B.onCreate()
Fragment_A.onDestroy()
In Fragment_A.onDestroy() I reverse the operations I do in Fragment_A.onCreate(). And in Fragment_B I expect things to be in a neutral state when onCreate() is called. However that is not the case since Fragment_A.onDestroy() has not yet been called.
Is the overlap normal on Android or did I configure something wrong in my Navigation components? Is there another way I could achieve what I am trying to do? I know I could couple both Fragments and make it work, but I don't want either Fragment to know about each other. To me it seems weird that Framgnet_A is still alive when Fragment_B is created, when Fragment_B is supposed to replace Fragment_A.
Any help is greatly appreciated!
Edit:
After groing through the source code while debugging I have found out that in FragmentNavigator.navigate() FragmentTransaction.setReorderingAllowed() is called, which allows reordering of operations, even allowing onCreate() of a new fragment to be called before onDestroy() of the previous. The question still remains, how can I solve my problem of correctly cleaning up global state in one Fragment before initializing the same global state in the next Fragment.
The Android Fragment life-cycle is not really an appropriate callback host for your needs. The navigation controller will replace the two fragments with animation, so both are somehow visible the same time and eventually even onPause() of the exiting fragment is called after onResume() of the entering one.
Solution 1: Use OnDestinationChangedListener
The onDestinationChanged() callback is called before any of the life-cycle events. As a very simplified approach (look out for leaks) you could do the following:
findNavController().addOnDestinationChangedListener { _, destination, _ ->
if(shouldCleanupFor(destination)) cleanup()
}
Solution 2: Abstract the global changes away
Instead of having single navigation points change the global state, have a single point of truth for it. This could be another fragment independent of the navigation hierarchy. This then observes the navigation as before:
findNavController(R.id.nav_graph).addOnDestinationChangedListener { _, destination, _ ->
resetAll()
when(distination.id) {
R.id.fragment_a -> prepareForA()
R.id.fragment_b -> prepareForB()
else -> prepareDefault()
}
}
As an additional advantage you could implement the state changes idempotently as well.
Since you have an activity that controls the inflation of your Fragments you can manually control the lifecycles of the fragment that are being inflated. By calling into below methods you can control which fragment is ready to use global data. You will at this point have to, some how pass data back to Mainactivity to establish which fragment is active since your asking about how to inflate 2 fragment simultaneously which will share an object. Better approach would be to have the MainActivity implement FragmentA and FragmentB-detail with specific classes to do Stuff this way you have to treat your app like Tablet and determine 2 pane mode and which point you can use appropriate classes out of those fragments controlled by your Activity. The included link matches what your trying to accomplish
private void addCenterFragments(Fragment fragment) {
try {
removeActiveCenterFragments();
fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.content_fragment, fragment);
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
activeCenterFragments.add(fragment);
fragmentTransaction.commit();
}catch (Exception e){
Crashlytics.logException(e);
}
}
private void removeActiveCenterFragments() {
if (activeCenterFragments.size() > 0) {
fragmentTransaction = fragmentManager.beginTransaction();
for (Fragment activeFragment : activeCenterFragments) {
fragmentTransaction.remove(activeFragment);
}
activeCenterFragments.clear();
fragmentTransaction.commit();
}
}
Perhaps you could move some the code related to initialization where you assume a neutral state to that fragments onStart() or onCreateView() method. According to the developer documentation this is where initialization should take place.
Another option available is using an Observer /Observable pattern, where you could notify your Activity once onDestroy() in Fragment A is completed. The Activity would then notify Fragment B that it is safe to assume a cleaned up state and begin initialization.
My case was a little bit different, and I would like to share it in case anyone faced the same issue.
I wanted to do an action in onPause() of the current fragment, but not execute that code when one navigates from a fragment to another. What I had to do was to call isRemoving() method to check if the current fragment is being removed or not. It is set to true when NavController.navigate(...) method is called.
override fun onPause() {
super.onPause()
if (!isRemoving()) {
// Write your code here
}
}
Per Google's Fragment.isRemoving() documentation:
Return true if this fragment is currently being removed from its activity. This is not whether its activity is finishing, but rather whether it is in the process of being removed from its activity.

Difference between creating a new fragment and getting an existing one from the manager on rotation

It seems whether I set retainInstance to true or not, when I rotate the device, I get an existing fragment. The difference is that if I set it to true, I get "test = yes!", otherwise I get "test = no!" after rotating the device after clicking the test button to change test. That is, the member variable is kept, if I retain the instance.
But as I have said, even if I do not retain it, I get an existing fragment from the manager, anyway (always get "Reusing existing" on rotation). In that case, if all member variables are lost and the views of the fragment are recreated, what are kept? What is the point of getting an existing instance of the fragment?
In the activity's onCreate,
var frag = supportFragmentManager.findFragmentById(R.id.frame)
if(frag == null)
{
frag = Fragment1.newInstance("", "");
}
else
{
Log.d("sss", "Reusing exsiting");
}
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.frame, frag)
transaction.commit()
In the fragment,
var test = "no!";
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
{
super.onViewCreated(view, savedInstanceState)
Log.d("sss", "test = " + test);
testButton.setOnClickListener {
test = "yes!";
}
}
I have spent some hours trying to recreate your situation with different scenarios. First of all, I should point out that the life cycle of Fragments are in fact so complicated that during Google I/O 2018, one of the lead developers asked the audiences if onCreate() method of the Activity is invoked first or that of Fragment's. And the answer was that, it depends on the SDK version. But they are focusing more and more on Compatibility Libraries and advice developers to use these to have a universal experience across different devices, as well as enjoying the new APIs that will do the job of dealing with Fragments, so much easier for us.
While looking at the documentation of AppCompatActivity class, I realized that this behavior is their way of dealing with fragments.
Protected methods
void onCreate(Bundle savedInstanceState)
Perform initialization of all fragments.
void onDestroy()
Destroy all fragments.
void onPostCreate(Bundle savedInstanceState)
void onPostResume()
Dispatch onResume() to fragments.
void onSaveInstanceState(Bundle outState)
Save all appropriate fragment state.
void onStart()
Dispatch onStart() to all fragments.
void onStop()
Dispatch onStop() to all fragments.
As you can see they "save all appropriate fragment state" in onSaveInstanceState(); meaning that the states will be restored later on, after the Activity gets destroyed and recreated. So in onDestroy() all fragments get destroyed and when the Activity is created again, they get recreated as well. To make sure, you could override these methods inside both Fragment and Activity and check the result. If you do not check FragmentManager for already attached fragments, onCreate() method of the Fragment will be called twice, once directly by you -adding as a new Fragment- and once by the AppCompatActivity itself.
About the retainInstance = true, the documentation says that it keeps the member variables during configuration changes and will cause a slight difference in the life cycle of the Fragment.
setRetainInstance(true)
Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change). This can only be used with fragments not in the back stack. If set, the fragment lifecycle will be slightly different when an activity is recreated:
onDestroy() will not be called (but onDetach() still will be, because the fragment is being detached from its current activity).
onCreate(Bundle) will not be called since the fragment is not being re-created.
onAttach(Activity) and onActivityCreated(Bundle) will still be called.

Categories

Resources