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
Which method is generally better / safer for reading string resources while in a Fragment?
Here I read getResources().getString() directly:
public class SomeFragment extends Fragment {
public static SomeFragment newInstance() {
Bundle args = new Bundle();
SomeFragment fragment = new SomeFragment();
fragment.setArguments(args);
return fragment;
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String someString = getResources().getString(R.string.my_string_id);
}
}
Or is it better to do it this way, by setting a Context field first and then reading the resources from that:
public class SomeFragment extends Fragment {
private Context context;
public static SomeFragment newInstance() {
Bundle args = new Bundle();
SomeFragment fragment = new SomeFragment();
fragment.setArguments(args);
return fragment;
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
this.context = context;
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String someString = context.getResources().getString(R.string.my_string_id);
}
}
Any tradeoffs/drawbacks to either of these methods?
The reason I ask is because sometimes I've run into nullpointer issues with the first approach that were resolved by using the second approach; so I wasn't sure if it was problematic to use the first one.
The second approach is more complicated and adds no value over the first approach. Use the first approach.
Both the approaches are better. No issues in this. You faced the null pointer exception because.
When a fragment transaction happens the lifecycle of the fragment starts and only when it gets attached to the activity and when reaches the onstart it will be ready.
Some times if we try to access the public methods of the fragment immediately after the fragmentManager's commit. Then you will run into null pointer exception while accessing the context object. As it is not still completed the process.
So its developers logic to write the code understanding the life cycle to avoid those.
When Fragments is onCreate() method, it already has the Activity's context, so it is not necessary set the context previously. Unless you need to do an specific (and inusual) implementation of Context, like parsing variables/values from portrait screen mode to landscape mode I guess...
However you're getting an string resource from last context, wich is already set up, so well the parsed variables/values indeed.
I have an Activity called BaseActivity, hosting multiple fragments.
The BaseActivity has a public field Object owhich is accessed by the fragments by calling Object o = ((BaseActivity) getActivity()).o; This is initialized in the fragments onCreate.
This works but I have problems with runtime configuration changes. It seems that the Fragments onCreate is called before the BaseActivitys onCreate, so I cant retain the Object from the Bundle i saved in onSaveInstanceState.
Is there a way I can make sure the acitivty can retain its object from the saved Bundle before the Fragment tries to access it?
Try this ..
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Object o = ((BaseActivity) getActivity()).o;
}
This makes sure that the activity's onCreate has completed executing.
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.
Unable to instantiate fragment make sure class name exists, is public,
and has an empty constructor that is public
Is it because my Fragment is not a static class?
Is it because my Fragment is an inner class?
If I make my Fragment a static class, all my references to findViewById fail, which means a LOT of refactoring.
How can I solve this without turning my inner Fragment into a static class?
is it because my Fragment is an inner class
If your fragment is an inner class, it must be a static inner class. Ideally, it's a standalone public Java class.
if I make my Fragment a static class, all my references to findViewById fail, which means a LOT of refactoring
You needed to do that refactoring anyway. Widgets are now owned by the fragment, not the activity. Fragments should know as little as possible about what activity contains them, so they can be shuffled between different activities as needed to support phones, tablets, TV, etc.
How can I solve this without turning my inner Fragment into a static class??
You make it a standalone public Java class.
Your Fragment shouldn't have constructors (see this documentation and its examples).
You should have a newInstance() static method defined and pass any parameters via arguments (bundle)
For example:
public static final MyFragment newInstance(int title, String message)
{
MyFragment fragment = new MyFragment();
Bundle bundle = new Bundle(2);
bundle.putInt(EXTRA_TITLE, title);
bundle.putString(EXTRA_MESSAGE, message);
fragment.setArguments(bundle);
return fragment ;
}
And read these arguments at onCreate:
#Override
public Dialog onCreate(Bundle savedInstanceState)
{
title = getArguments().getInt(EXTRA_TITLE);
message = getArguments().getString(EXTRA_MESSAGE);
//...
//etc
//...
}
This way if detached and re-attached the object state can be stored through the arguments, much like bundles attached to Intents.
As CommonsWare said make it static or standalone, additionally don't know why you need a shedload of refactoring for getting findViewById to work. Suggestions:
Using the view inflated in onCreateView,
inflatedView.findViewById(.....)
or calling it in onActivityCreated(.....)
getActivity().findViewById(......)
But even if you still need a load of refactoring then that might just be the way it is, converting an app to use fragments doesn't come for free having just finished a project doing so.
I had this problem as well - turns out it was getting confused because my custom Fragment had a constructor.
I renamed the constructor method and called the new method instead upon instantiation, and it worked!
public static class MyDialogFragment extends DialogFragment {
public MyDialogFragment(){
}
public Dialog onCreateDialog(Bundle savedInstanceState) {
LinearLayout main = new LinearLayout(getActivity());
main.setOrientation(LinearLayout.VERTICAL);
return (new AlertDialog.Builder(getActivity()).setTitle(
getText("Title")).setView(main).create());
}
}
In my case, I was missing the constructor, the post from #eoghanm above helped me
public static class MyDialogFragment extends DialogFragment {
public MyDialogFragment(){
}
...
}
Using setRetainInstance(true) worked for us. Our inner classes now look like this:
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
Fragment fragment = new MySectionFragment();
Bundle args = new Bundle();
args.putInt(MySectionFragment.ARG_SECTION_NUMBER, position + 1);
fragment.setArguments(args);
fragment.setRetainInstance(true);
return fragment;
}
// ...
}
public class MySectionFragment extends Fragment {
public static final String ARG_SECTION_NUMBER = "section_number";
#SuppressLint("ValidFragment")
public MySectionFragment() {
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//...
}
// ...
}
PS. Here's an interesting one about setRetainInstance(boolean): Understanding Fragment's setRetainInstance(boolean)
if you don't want to make the inner class static, try to override the method onPause of the dialog fragment like this:
public void onPause()
{
super.onPause();
dismiss();
}
so the fragment should be destroyed when the app goes on pause and there is no exception. i tried it and works.
Hahah my hilarious issue was I had a call to getString() as a member level variable in my fragment which is a big no no because it's too early I guess. I wish the error was more descriptive!
Make sure the Fragment isn't abstract. Copy&paste makes this kind of things happen :(
The inner class constructor must be pass in an instance of the outer class. so it is said the compiler cannot find the constructor which has no parameter. so it should be put into static of other java file.
i have meet this problem
you need use full class name :
eg:
Fragment.instantiate(MainActivity.this, com.XX.yourFragmentName);
must full class name
It is also worth trying to check that your default Fragment constructor is public, not package-private, which Android Studio might propose. This was the cause in my case.