Short version: I would like to know how i can recreate a fragment state (e.g. after screen-rotation) when this fragment contains a reference to an object that cannot be serialized or duplicated and needs to stay in memory.
Long version:
In my app i use a ViewPager connected to a custom FragmentPagerAdapter that instantiates a number of fragments which display data (a schedule) contained in a ListView. All this is contained inside a parent fragment. When instantiated by the adapter, each page fragments is passed a reference to one single object (the "ScheduleManager") that does several things:
Contains the data to be displayed
Holds a reference to a Context object (in order to access SharedPreferences)
Holds a reference to the parent fragments LoaderManager so it can reload the data
Implements OnClick- and ActionMode callback listeners (to be able to create and handle an action mode that works across all pages; Page Fragments add the object as a listener to their ListViews)
Defines a callback interface to notify listeners of state changes or when data is reloaded (Page Fragments register themselves as listeners).
Essentially, the ScheduleManager holds everything together and implements the main logic of this ("schedule") part of my app, i.e. loading and providing the data, and the means to modify and relaod it via an ActionMode. I don't know whether this is good design...
My question is how am I supposed restore the Fragments instance state under these circumstances? I cannot serialize the ScheduleManager to a bundle, since it would loose its references to the Context and the LoaderManager (otherwide, of course I would use setArguments / getArguments). Also, all the page fragments must have a reference to the same instance of the ScheduleManager, otherwise the shared action mode won't work. Aside from that I don't want to duplicate the entire schedule data each time a fragment is restored. I want to keep this object in memory and let the page fragments reclaim a reference to it when they are restored.
I guess I could let the containing activity hold the ScheduleManager and have the PageFragment query it for a reference. But I would prefer to keep the everything inside the parent fragment self-contained and modular if this is possible (there can be different schedules for different items). While writing this though I get the feeling there will be no way around this.
Of course, when the parent fragment is recreated it can also recreate the ScheduleManager and feed to it the references it needs (Context + LoaderManager). The problem is there can be no more than one instance of ScheduleManager for each instance of the parent fragment, so how to make the nested page fragments reconnect to it.
Here is what I ended up doing:
public void onCreate(Bundle savedInstanceState)
{
Fragment fragment = this.getActivity().getFragmentManager().findFragmentByTag("SCHEDULE");
if (fragment instanceof ScheduleMasterFragment)
{
ScheduleMasterFragment master = (ScheduleMasterFragment) fragment;
this.scheduleManager = master.getScheduleManager();
}
else
{
throw new RuntimeException("SchedulePageFragment must be " +
"the child of a ScheduleMasterFragment with Tag set to 'schedule'");
}
super.onCreate(savedInstanceState);
}
The ScheduleManager is the reference I wanted to keep. At least this way I keep all the code inside the fragment itself, except for getScheduleManager in ScheduleMasterFragment which makes sense because the ScheduleMasterFragment somehow is the owner of the ScheduleManager instance.
Any better solutions are still welcome...
Related
I have a view pager that is a fragment. Inside this, I have three fragments each one show a list of items that have the same model but filtered by one state (property in my model), for example like whatsapp that I have one list for all my conversations, other for conversations that I haven't read and finally other for that I've read.
The problem is that I have to refresh all the data when I'm inside the first fragment (all my conversations). So, I've tried to get all my data in the container fragment(view pager) but I cant see my first list with data(all my conversations) because when the container view is created, the first and second fragments is created too.
How can I get the data in the container (view pager) and immediately show the respective list in each child fragment?
The solution is not to store data in any Fragment.
It is a very common use case that if a data is needed in Fragment (or Activity) X, then it will also be needed in Fragment Y. Sharing any state between Fragments is not easy and error prone, therefore I always advice to keep application's data in "application global scope".
What you need is some object, let's call it DataRepository, that lives in the scope of Application object and is injected into all Fragments. Since it lives in Application scope, the data is not tied to any specific Fragment and all Fragments can access and filter the data however you find appropriate.
The most convenient way to define such a "global" object is to use dependency injection framework (e.g. Dagger) and designate DataRepository as scoped injection in ApplicationComponent.
My activity uses a FragmentPagerAdapter that creates a new instance of my Fragment from the FragmentPagerAdapter.getItem() method.
Whenever I select the page containing my fragment of interest, I want to update its views because they may have been invalidated in the meantime when this page wasn't on screen.
What is the correct moment/method to do this? I've tried a few things:
MyFragment.onCreateView: this is only called the first time
PagerAdapter.onPageSelected: since the PagerAdapter creates new instances all the time, field references to Views obtained with findViewById in onCreateView are unavailable (unless they're static members I guess).
The other lifecycle events including onResume, onAttach
I found that if I pass the Activity reference to the Fragment from the onPageSelected event, and use findViewById on that to obtain new references to my Views it works, but I can't imagine there isn't a more intended way of doing this.
OverRide this method, setUserVisibleHint Set a hint to the system about whether this fragment's UI is currently visible to the user. This hint defaults to true and is persistent across fragment instance state save and restore. Whenever it is true then that means the fragment is being viewed, either from a view pager or something triggering the validation of its views.
An app may set this to false to indicate that the fragment's UI is scrolled out of visibility or is otherwise not directly visible to the user.
More info:
For some reason my understanding was that a headless Fragment lives for the duration of your application. With this understanding, in my attempt to persist an object between startActivityForResult() I put the object in a Headless Fragment like this
private HeadlessFragment modelFragment;
modelFragment = (HeadlessFragment)
getSupportFragmentManager().findFragmentByTag(Constants.HEADLESS_FRAGMENT_TAG);
if (modelFragment == null){
modelFragment = new HeadlessFragment();
}
modelFragment.setInvoice(invoice);
I can confirm that the custom object was set, however when I go to the next activity and try to get the same object by calling findFragmentByTag with same tag the object is null.
Does a Headless Fragment survive between two Activities life cycle? I did set setRetainInstance(true) on that Headless Fragment. I was hoping that I will not have to implement Parceable on my custom object.
For some reason my understanding was that a headless Fragment lives for the duration of your application.
No. Fragments are owned by activities and are not application-wide constructs.
I can confirm that the custom object was set, however when I go to the next activity and try to get the same object by calling findFragmentByTag with same tag the object is null.
There are at least two reasons for this:
First, at least in the code that you are showing, you never add the fragment to the FragmentManager via a FragmentTransaction. As such, the activity that created the fragment will not be able to find the fragment via findFragmentByTag(), because the FragmentManager does not know about it.
Second, each activity has its own FragmentManager, and fragments from one activity are not accessible in another activity.
I was hoping that I will not have to implement Parceable on my custom object.
Then don't pass the object. Pass the information (e.g., a key or ID) by which the other activity can retrieve the object (from a singleton POJO cache, by querying the database, etc.).
Or, do not make them separate activities, but have them as separate (regular) fragments in one activity.
Or, implement Serializable, though Parcelable executes more quickly.
I have an app that has one main Activity that swaps out numerous Fragment's. Well it doesn't matter what Fragment you are on, after low memory kills the Activity and you try to return to the app, it boots you back to the "start" Fragment that the Activity first calls. (Note: Almost all of these are actually ListFragment's)
So here are my questions:
Should I be using onSaveInstanceState() in EACH Fragment? And if so, am I saving the Data in the Fragment OR the Fragment itself? Or do you use onSaveInstanceState() only once in the Main Activity. (If this is even the course to take)
Note: I have setRetainInstance(true) but I don't think I am handling that correctly, if that is the solution. These are all put as the last line of onActivityCreated().
The answer depends a lot on how you are managing fragments.
I'll assume you are not using the Fragment backstack, and that you have called setRetainInstance(true) on EACH fragment.
You need to use a tag when you attach the fragments.
In Activity#onSaveInstanceState() you need to remember which fragments are visible.
In Activity#onCreate you need to find the existing Fragments by tag for each fragment, then create new instances of any Fragments you can't find. Now you can use the information from the saved instance state to make the appropriate Fragments visible (show or add or replace as necessary depending on how your code manages the fragments.)
Edit in response to questions/comments:
activty.getFragmentManager().findFragmentByTag(tag); finds an existing fragment
in a Fragment transaction: add(fragment, tag), replace(id, fragment, tag), etc. lets you specify the tag. You can also put it in a layout file using the attribute
class=".myFrag$tag"
The actual fragment object including its contents still exist when you use setRetainInstance.
Note: If you don't want to use tags, you may also use the fragment manager's putFragment/getFragment methods to put the fragment into the instance state bundle.
Finally you can simply let the fragment save itself by calling FragmentManager's saveFragmentInstanceState but I've had trouble using this correctly.
My setup is as follows.
I have a FragmentPagerAdapter called from my Activity which loads two fragments. This is setup within onCreate.
In onResume I call an ASyncTask which loads data from a database, and then calls a callback in my activity onLoadComplete via a load data listener.
#Override
public void onLoadComplete(JSONArray data) {
// TODO Auto-generated method stub
LocalFragment fragmentB = (LocalFragment)getSupportFragmentManager().findFragmentByTag(ListTag);
fragmentB.setList(data);
LMapFragment fragmentA = (LMapFragment)getSupportFragmentManager().findFragmentByTag(MapTag);
GoogleMap our_map = fragmentA.getMap();
fragmentA.plotP(myLocation,data);
}
The fragments are initialized by the Pager, and within each fragments code I set the respective tag e.g in LocalFragment
#Override
public void onAttach(Activity activity) {
// TODO Auto-generated method stub
super.onAttach(activity);
String myTag = getTag();
((PagerTest) activity).setListTag(myTag);
Log.d("what",myTag);
}
This allows me to access the fragment, call a function within it which populates a list or populates a map. It works.
What I am now trying to do is account for screen orientation changes.. If while the ASyncTask is running the orientation is changed, the app crashes.
As suggested here: Hidden Fragments I have been trying to implement a hidden fragment which saves the state of my ASyncTask. So what I have done is set it up so in onResume of my Activity i call a function
static LoadDataFromURL the_data = null;
static JSONArray pub_data = null;
private static final String TAG = "RetainFragment";
public RetainFragment() {}
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit(); // add this
}
return fragment;
}
which essentially saves my data.
Basically what this means is that if i rotate my screen i dont call my ASyncTask again.. the screen just rotates.. it works perfectly.
If however I go back to the main menu and then click on the activity again the screen returns blank (but does not crash). My understanding is that the data is retained as an object in the fragment, but on reloading the activity afresh the data needs to be set again.. I.E onLoadComplete needs to be called to populate the list/map..
So i concluded that if initially after the ASyncTask completes i save the returned data in my hidden fragment onRetainInstance, then i could simply call onLoadComplete and pass it..
The problem is, in this situation seemingly the fragment has not been called yet, as such the tags are null, and calling the callbacks within onLoadComplete crashes the app.
I have been banging my head over this for ages.
My ASyncTask is in a seperate class: LoadDataFromURL
What i want to achieve is as follows - a fragmentviewpager whereby on screen rotate the ASyncTask is retained on rotate/attached to the new activity, and if it has completed before it shouldn't be run again..
Could anyone advise.
Many Thanks
EDIT
Having changed the variables in my secret fragment to public variables, everything has seemingly come together.. BUT because im not 100% sure how/when things are called, I dont fully understand WHY it works..
So.. I call findOrCreateRetainFragment and it either creates a new 'secret' fragment or returns the current instance.
If it is returning a current instance, i dont call my async task again. If it is not, I call my asynctask and load the data.
With this setup, when i load the activity and rotate the screen, it rotates as expected woop.
Now, when i go back to the main menu and then click the activity again, it calls the async task.
My understanding is that on rotate the async task is not called again, and the viewpager is somehow saving the fragments.
On the other hand, when i go back my activity is destroyed, as is my secret fragment, and as such when i click on it again it loads the data. THis is essentially what i want..
Have i understood this correctly?
Thanks
There are a few issues here that you're experiencing (I think).
First of all, the reason your callbacks crash is because they're attached to an old Activity that no longer "exists" after a screen orientation and/or Activity push. If you use onAttach() to attach a callback to your fragment, you must use onDetach() to detach that callback when the Fragment is removed from the Activity. Then, whenever you call the callback, check for a null so you don't send data to a dead object.
Basically, the logic you're trying to use here is:
Start Activity.
Check if your Fragment exists. If it does, grab it. Else, create it.
Retrieve the data if it exists. If not, wait for the callback.
Because of the nature of callbacks (depending on your implementation), you will not receive data until the event fires. However, if the Activity is gone and the event has already fired, the callback won't execute. Thus, you have to retrieve the data manually. When using setRetainInstance(), it's helpful to think of it as this entity detatched from your Activity. It will exist as long as you don't pop the current Activity or push a new Activity. However, your current Activity will be destroyed upon screen orientation changes while the Fragment won't. As such, the Fragment shouldn't rely on the existence of the Activity.
A much more elegant solution to the problem that you may want to look in to is implementing the Android Loader API. Loaders are handy tools that are handled by the system that work is roughly the same way but are more in-tune with asynchronously retrieving data. They work effectively the same way. You simply start your loader and the system with either create one if it doesn't exist or re-use one that already exists. It will remain in the system by the LoaderManager upon configuration changes.
EDIT:
To answer your edit, I guess I'll explain what's happening. It's convoluted, so just tell me if anything needs clarification.
Fragments aren't technically speaking part of your currently running Activity. When you create an instance of the Fragment, you have to call beginTransation() and commit() on the FragmentManager. The FragmentManager is a singleton that exists within the realm of your application. The FragmentManager holds on to the instance of the Fragment for you. The FragmentManager then attaches the Fragment to your Activity (see onAttach()). The Fragment then exists within the FragmentManager which is why you never really have to hold a reference to it within your application. You can just call findFragmentByTag/Id() to retrieve it.
Under normal circumstances, when your Activity is being destroyed, the FragmentManager will detach the instance of your Fragment (see onDetach()) and just let it go. The Java garbage collect will detect that no reference to your Fragment exists and will clean it up.
When you call setRetainInstace(), you're telling the FragmentManager to hold on to it. Thus, when your Activity is being destroyed on a configuration change, the FragmentManager will hold on to the reference of your Fragment. Thus when your Activity is rebuilt, you can call findFragmentByTag/Id() to retrieve the last instance. So long as it didn't keep any context of the last Activity, there shouldn't be any problems.
Traditionally, one would use it to keep references to long standing data (as you are) or to keep connection sockets open so a phone flip doesn't delete it.
Your ViewPager has nothing to do with this. How it retrieves the Fragments is completely dependent on how you implement that Adapter that it's attached to. Usually, retained Fragments don't have Views themselves because Views hold Context data of the Activity they were created in. You would just basically want to make it a data bucket to hold on to the data for the Views to pull from when they're being inflated.