I've been using for a while the best practice recommended by Google of having a MyFragment.newInstance() static function. Though thinking about it, why can't we simplify it removing this static function, the call to onCreate to access the arguments, and only using one bundle to always save and retrieve the latest data when recreating the fragment ?
I made a simple test that seems to work just as fine as the slightly heavier current practice.
The state persisted after activity recreation, orientation change, and fragment re-creation in a FragmentStatePagerAdapter.
Am I missing anything?
public class TestFragment extends Fragment {
private String fragmentText;
public TestFragment() { } // Required empty public constructor
#SuppressLint("ValidFragment")
public TestFragment(String fragmentText) {
// add here other init arguments
// don't save them in any bundle yet
this.fragmentText = fragmentText;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (savedInstanceState != null) {
// retrieve all arguments here
fragmentText = savedInstanceState.getString("fragmentText", fragmentText);
}
TextView textView = new TextView(getActivity());
textView.setText(fragmentText);
return textView;
}
#Override
public void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
// save everything here once, only when needed
outState.putString("fragmentText", fragmentText);
}
// Add your setters to interact with the fragment
// those changes will persists after fragment re-creation
public void setFragmentText(String fragmentText) {
this.fragmentText = fragmentText;
}
}
Why isn't savedInstanceState bundle enough?
It is enough. The arguments Bundle is added to the saved instance state Bundle automatically.
I made a simple test that seems to work just as fine as the slightly heavier current practice.
Your approach is roughly the same, in terms of lines of code, as is the factory-method approach.
Why do we need to add an additional bundle with setArguments?
You do not "need" it. It is merely an available and recommended pattern for providing input to the fragment. You are welcome to do something else if you wish. Just remember to have the public zero-argument constructor as well as your custom constructor, since the framework will use the public zero-argument constructor when recreating your fragments.
Because savedInstance doesn't call every time. It will be triggered when device screen will be rotated or when inner system kill application due to low memory and some more scenarios. So if you want to pass some values from activities to fragment or fragment to fragment you must have to pass it through Argument. There are plenty of others ways -> you can make static variable and store the value in it but thats not a perfect value to pass value -> it will consume lot of memory. So passing through argument is standard way
Related
i need to save a custom object that i use in a fragment so it will not be lost when the screen rotates (when the app calls onDestroy and then recalls onCreate)
now the normal way to do so is to implement Parcelable interface and save it to the bundle as a Parcelable object.
that is a very tedious way of doing things.
is there a way to just pass the object along as "putObject" method?
You can save your data in fragment, retained during a configuration change like in example.
Extend the Fragment class and declare references to your stateful
objects.
public class RetainedFragment extends Fragment {
// data object we want to retain
private MyDataObject data;
// this method is only called once for this fragment
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
.. getter and setter
}
Then use FragmentManager to add the fragment to the activity.
public class MyActivity extends Activity {
private RetainedFragment dataFragment;
#Override
public void onCreate(Bundle savedInstanceState) {
..
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (RetainedFragment) fm.findFragmentByTag(“data”);
// create the fragment and data the first time
if (dataFragment == null) {
// add the fragment
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, “data”).commit();
} else {
// available dataFragment.getData()
..
// save data in onDestroy dataFragment.setData(yourData);
The best way is to implement Parcelable (Faster).
Easier (not efficient) way is to implement Serializable and add the object into the bundle as serializable.
well searching i found no official way of doing so, so here are two "hacks" i found around the problem:
1)create a class that extends Application class, in it add an arrayList of objects.
inside onSaveInstanceState call:
getApplication().getObjectArray().add(YourObject);
save the Object index inside the bundle using putInt.
extract it inside the method onReturnestoreInstanceState.
2)my less favorite one:
android automatically saves the states of its views
therefor a way to save an object will be to create a view set its visibility to none so it wont show on the screen and then add each object we want to the view using the methods:
view.setTag(key,Object); or view.setTag(Object);
now inside onReturnestoreInstanceState get the view and extract the tags.
unfortunately i couldn't find a more simple way of saving an object
hope this one helps you out (in my app i ended up using the first method)
I have a simple Activity containing a ViewPager, which displays Fragments.
My Activity should display information about a football league, and each fragment displays information like livescroes/matchdays, tables, etc.
The Intent with which I start the Activity, contains the league id.
And each Fragment needs this league id to load the correct data.
So my FragmentPagerAdapter looks like this
public class LeaguePagerAdapter extends FragmentPagerAdapter {
private String leagueId;
public LeaguePagerAdapter(FragmentManager fm, String leagueId) {
super(fm);
this.leagueId = leagueId;
}
#Override
public Fragment getItem(int pos) {
if (pos == 0){
return TableFragment.newInstance(leagueId);
} else {
return MatchdayFragment.newInstance(leagueId);
}
}
}
The TableFragment looks like this ( the matchday fragment looks similar):
public class TableFragment extends PullToRefreshListViewAdFragment {
private String leagueId;
public static TableFragment newInstance(String leagueId) {
TableFragment t = new TableFragment();
t.leagueId = leagueId;
return t;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Setup UI and load data
}
}
Sometimes the leagueId is null. I see the exceptions in the crash logs (crittercism). But Im asking my self why. It seems to me, that the problem is when the activity has been destroyed in the background and reconstructed if (for instance) the user uses the multitasking button to switch to my app.
So as far as I know, the original Intent will be stored internally by Android itself if the Activity has been destoryed. Therefore I have not implemented any onSaveInstanceState() in my activity nor in the fragment. In my activity I read the Intent Extra to retrieve the leagueId. This works fine, also on restoring the activity. I have assumed that by recreating the activity, a new LeaguePagerAdapter will be created and all fragments will also be new created.
Is that correct? Or does the "old" fragment instance will be restored and hence the leagueId is null (because the fragment has not stored the leagueId in Fragments onSaveInstanceState method?).
Is there a way to test such lifecycle things
The reason it is null is because the system restores the Fragment with the default constructor. Here's what the documents say:
Every fragment must have an empty constructor, so it can be instantiated when restoring its activity's state. It is strongly recommended that subclasses do not have other constructors with parameters, since these constructors will not be called when the fragment is re-instantiated; instead, arguments can be supplied by the caller with setArguments(Bundle) and later retrieved by the Fragment with getArguments().
edit: also, take a look at this: Fragment's onSaveInstanceState() is never called
edit: To further add on, you are creating your Fragment with your newInstance(String) method. If your Fragment is killed by Android, it uses the default constructor and so your leagueId variable won't be set. Try using setArguments/getArguments to pass the value into your Fragment instead.
I have a fragment class that looks like this:
public class MessageFragment extends Fragment {
Context ctx;
Button compose;
public MessageFragment(Context ctx){
this.ctx = ctx;
}
...}
The constructor it gives an error that says
This fragment should provide a default constructor
Meanwhile, I have 4 other fragment classes that are formatted this exact way, but they don't give this error. How can I fix this?
When your Activity is recreated due to a configuration change (such as an orientation change), the system will manage recreating the state of your fragments by creating a new instance of your Fragment, and then passing the arguments in using setArguments(Bundle args). It uses the default constructor to recreate your fragment, which is why it is required. You should never rely on logic that happens in a non-default constructor for your fragment, as you'll immediately break on a configuration change.
Also, passing in a Context to your Fragment seems like a memory leak waiting to happen. It might not, but it's not good practice. Wait until one of the Fragment lifecycle events such as onCreate() or onAttach(), and store a reference to getActivity() as your Context. You can then release the reference in onDetach().
EDIT: Basically, anything you need to pass in for your Fragment to function properly should either be stored in its arguments Bundle, or saved in the onSaveInstanceState(Bundle outState) event and restored in onCreate(Bundle state), otherwise you'll lose it on a config change.
This is why there's a common pattern of a static factory method for creating fragments. For example:
public static Fragment newInstance(String arg1, int arg2) {
Fragment result = new MyFragment();
Bundle args = new Bundle();
args.putString("arg1_key", arg1);
args.putInt("arg2_key", arg2);
result.setArguments(args);
return result;
}
And then use that instead of a non-default constructor. From within your Fragment you can then retrieve the data with:
Bundle args = getArguments();
String arg1 = args.getString("arg1_key");
int arg2 = args.getInt("arg2_key");
Just add a default constructor like this
public MessageFragment(){}
And you should be good.
I've got an activity, containing fragment 'list', which upon clicking on one of its items will replace itself to a 'content' fragment. When the user uses the back button, he's brought to the 'list' fragment again.
The problem is that the fragment is in its default state, no matter what I try to persist data.
Facts:
both fragments are created through public static TheFragment newInstance(Bundle args), setArguments(args) and Bundle args = getArguments()
both fragments are on the same level, which is directly inside a FrameLayout from the parent activity (that is, not nested fragments)
I do not want to call setRetainInstance, because my activity is a master/detail flow, which has a 2 pane layout on larger screens. 7" tablets have 1 pane in portrait and 2 panes in landscape. If I retain the 'list' fragment instance, it will (I think) fuck things up with screen rotations
when the users clicks an item in the 'list' fragment, the 'content' fragment is displayed through FragmentTransaction#replace(int, Fragment, String), with the same ID but a different tag
I did override onSaveInstanceState(Bundle), but this is not always called by the framework, as per the doc: "There are many situations where a fragment may be mostly torn down (such as when placed on the back stack with no UI showing), but its state will not be saved until its owning activity actually needs to save its state."
I'm using the support library
From the bullet 5 above, I guess that low-end devices that need to recover memory after a fragment transaction may call Fragment#onSaveInstanceState(Bundle). However, on my testing devices (Galaxy Nexus and Nexus 7), the framework doesn't call that method. So that's not a valid option.
So, how can I retain some fragment data? the bundle passed to Fragment#onCreate, Fragment#onActivityCreated, etc. is always null.
Hence, I can't make a difference from a brand new fragment launch to a back stack restore.
Note: possible related/duplicate question
This doesn't seem right, but here's how I ended up doing:
public class MyActivity extends FragmentActivity {
private Bundle mMainFragmentArgs;
public void saveMainFragmentState(Bundle args) {
mMainFragmentArgs = args;
}
public Bundle getSavedMainFragmentState() {
return mMainFragmentArgs;
}
// ...
}
And in the main fragment:
public class MainFragment extends Fragment {
#Override
public void onActivityCreated(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = ((MyActivity) getActivity()).getSavedMainFragmentState();
if (args != null) {
// Restore from backstack
} else if (savedInstanceState != null) {
// Restore from saved instance state
} else {
// Create from fragment arguments
args = getArguments();
}
// ...
}
// ...
#Override
public void onDestroyView() {
super.onDestroyView();
Bundle args = new Bundle();
saveInstance(args);
((MyActivity) getActivity()).saveMainFragmentState(args);
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
saveInstance(outState);
}
private void saveInstance(Bundle data) {
// put data into bundle
}
}
It works!
if back from backstack, the fragment uses the parameters saved in onDestroyView
if back from another app/process/out of memory, the fragment is restored from the onSaveInstanceState
if created for the first time, the fragment uses the parameters set in setArguments
All events are covered, and the freshest information is always kept.
It's actually more complicated, it's interface-based, the listener is un/registered from onAttach/onDetach. But the principles are the same.
We are using Fragments and we don't need them to be automatically recovered when the Activity is recreated.
But Android every time when Activity::onCreate(Bundle savedInstanceState) -> super.onCreate(savedInstanceState) is called, restores Fragments even if we use setRetainInstance(false) for those Fragments.
Moreover, in those Fragments Fragment.performCreateView() is called directly without going through Fragment::onAttach() and so on. Plus, some of the fields are null inside restored Fragment...
Does anybody know how to prevent Android from restoring fragments?
P.S. We know that in case of recreating Activity for config changes it could be done by adding to manifest android:configChanges="orientation|screenSize|screenLayout. But what about recreating activity in case of automatic memory cleaning?
We finished by adding to activity:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
It suppresses any saved data on create/recreate cycle of an Activity and avoids fragments auto re-creation.
#goRGon 's answer was very useful for me, but such use cause serious problems when there is some more information you needs to forward to your activity after recreate.
Here is improved version that only removes "fragments", but keep every other parameters.
ID that is removed from bundle is part of android.support.v4.app.FragmentActivity class as FRAGMENTS_TAG field. It may of course change over time, but it's not expected.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(createBundleNoFragmentRestore(savedInstanceState));
}
/**
* Improve bundle to prevent restoring of fragments.
* #param bundle bundle container
* #return improved bundle with removed "fragments parcelable"
*/
private static Bundle createBundleNoFragmentRestore(Bundle bundle) {
if (bundle != null) {
bundle.remove("android:support:fragments");
}
return bundle;
}
I was having a problem with TransactionTooLargeException. So thankfully after using tolargetool I founded that the fragments (android:support:fragments) were been in memory, and the transaction became too large. So finally I did this, and it worked great.
#Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("android:support:fragments", null);
}
Edit: I added it to the Activity. In my case I have one single Activity app and Multiple Fragments.
Those who got NPE with ViewPager when use this method described in the accepted answer, please override
ViewPager.onRestoreInstanceState(Parcelable state)
method and call
super.onRestoreInstanceState(null);
instead.
I removed the fragments in Activity's onCreate.
For an app with a ViewPager, I remove the fragments in onCreate(), before their creation.
Based on this thread: Remove all fragments from container, we have:
FragmentManager fm = getSupportFragmentManager();
for (Fragment fragment: fm.getFragments()) {
fm.beginTransaction().remove(fragment).commitNow();
}
Use this one for androidx
override fun onCreate(savedInstanceState: Bundle?) {
preventFragmentRecreation()
super.onCreate(savedInstanceState)
}
private fun preventFragmentRecreation() {
supportFragmentManager.addFragmentOnAttachListener { _, _ ->
savedStateRegistry.unregisterSavedStateProvider("android:support:fragments")
}
}
This worked for me
#Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.remove("androidx.lifecycle.BundlableSavedStateRegistry.key");
}
View hierarchy in not restored automatically. So, in Fragment.onCreateView() or Activity.onCreate(), you have to restore all views (from xml or programmatically). Each ViewGroup that contains a fragment, must have the same ID as when you created it the first time. Once the view hierarchy is created, Android restores all fragments and put theirs views in the right ViewGroup thanks to the ID. Let say that Android remembers the ID of the ViewGroup on which a fragment was. This happens somewhere between onCreateView() and onStart().