Creating a Fragment: constructor vs newInstance() - android

I recently grew tired of constantly having to know String keys to pass arguments into Bundles when creating my Fragments. So I decided to make constructors for my Fragments that would take the parameters I wanted to set, and put those variables into the Bundles with the correct String keys, therefore eliminating the need for other Fragments and Activities needing to know those keys.
public ImageRotatorFragment() {
super();
Log.v(TAG, "ImageRotatorFragment()");
}
public ImageRotatorFragment(int imageResourceId) {
Log.v(TAG, "ImageRotatorFragment(int imageResourceId)");
// Get arguments passed in, if any
Bundle args = getArguments();
if (args == null) {
args = new Bundle();
}
// Add parameters to the argument bundle
args.putInt(KEY_ARG_IMAGE_RES_ID, imageResourceId);
setArguments(args);
}
And then I pull out those arguments like normal.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.v(TAG, "onCreate");
// Set incoming parameters
Bundle args = getArguments();
if (args != null) {
mImageResourceId = args.getInt(KEY_ARG_IMAGE_RES_ID, StaticData.getImageIds()[0]);
}
else {
// Default image resource to the first image
mImageResourceId = StaticData.getImageIds()[0];
}
}
However, Lint took issue with this, saying not to have subclasses of Fragment with constructors with other parameters, requiring me to use #SuppressLint("ValidFragment") to even run the app. The thing is, this code works perfectly fine. I can use ImageRotatorFragment(int imageResourceId) or the old school method ImageRotatorFragment() and call setArguments() manually on it. When Android needs to recreate the Fragment (orientation change or low memory), it calls the ImageRotatorFragment() constructor and then passes the same argument Bundle with my values, which get set correctly.
So I have been searching for the "suggested" approach and see a lot of examples using newInstance() to create Fragments with parameters, which seems to do the same thing my constructor is. So I made my own to test it, and it works just as flawlessly as before, minus Lint whining about it.
public static ImageRotatorFragment newInstance(int imageResourceId) {
Log.v(TAG, "newInstance(int imageResourceId)");
ImageRotatorFragment imageRotatorFragment = new ImageRotatorFragment();
// Get arguments passed in, if any
Bundle args = imageRotatorFragment.getArguments();
if (args == null) {
args = new Bundle();
}
// Add parameters to the argument bundle
args.putInt(KEY_ARG_IMAGE_RES_ID, imageResourceId);
imageRotatorFragment.setArguments(args);
return imageRotatorFragment;
}
I personally find that using constructors is a much more common practice than knowing to use newInstance() and passing parameters. I believe you can use this same constructor technique with Activities and Lint will not complain about it. So basically my question is, why does Google not want you to use constructors with parameters for Fragments?
My only guess is so you don't try to set an instance variable without using the Bundle, which won't get set when the Fragment gets recreated. By using a static newInstance() method, the compiler won't let you access an instance variable.
public ImageRotatorFragment(int imageResourceId) {
Log.v(TAG, "ImageRotatorFragment(int imageResourceId)");
mImageResourceId = imageResourceId;
}
I still don't feel like this is enough reason to disallow the use of parameters in constructors. Anyone else have insight into this?

I personally find that using constructors is a much more common practice than knowing to use newInstance() and passing parameters.
The factory method pattern is used fairly frequently in modern software development.
So basically my question is, why does Google not want you to use constructors with parameters for Fragments?
You answered your own question:
My only guess is so you don't try to set an instance variable without using the Bundle, which won't get set when the Fragment gets recreated.
Correct.
I still don't feel like this is enough reason to disallow the use of parameters in constructors.
You are welcome to your opinion. You are welcome to disable this Lint check, either on a per-constructor or per-workspace fashion.

Android only recreates fragments it kills using default constructor, so any initialization we do in additional constructors will be lost.Hence data will be lost.

Related

Android / Dagger2 - How to add bundle arguments ? Inject fragment or use newInstance?

I'm looking to find a solution on how to inject fragment and pass arguments to it.
And i didn't find any proper solution because injecting the fragment means by the constructor which is not safe for states.
Is there any way to do this, without calling the newInstance pattern ?
Thanks,
Best.
Because Android manages the lifecycle of your Fragment, you should separate the problems of passing state into the Fragment through its bundle and injecting the Fragment with injectable deps. Usually, the best way to separate these is by providing a static factory method, which you might be calling the newInstance pattern.
public class YourFragment extends Fragment {
// Fragments must have public no-arg constructors that Android can call.
// Ideally, do not override the default Fragment constructor, but if you do
// you should definitely not take constructor parameters.
#Inject FieldOne fieldOne;
#Inject FieldTwo fieldTwo;
public static YourFragment newInstance(String arg1, int arg2) {
YourFragment yourFragment = new YourFragment();
Bundle bundle = new Bundle();
bundle.putString("arg1", arg1);
bundle.putInt("arg2", arg2);
yourFragment.setArguments(bundle);
return yourFragment;
}
#Override public void onAttach(Context context) {
// Inject here, now that the Fragment has an Activity.
// This happens automatically if you subclass DaggerFragment.
AndroidSupportInjection.inject(this);
}
#Override public void onCreate(Bundle bundle) {
// Now you can unpack the arguments/state from the Bundle and use them.
String arg1 = bundle.getString("arg1");
String arg2 = bundle.getInt("arg2");
// ...
}
}
Note that this is a different type of injection than you may be used to: Rather than getting a Fragment instance by injecting it, you are telling the Fragment to inject itself later once it has been attached to an Activity. This example uses dagger.android for that injection, which uses subcomponents and members-injection methods to inject #Inject-annotated fields and methods even when Android creates the Fragment instance outside of Dagger's control.
Also note that Bundle is a general key-value store; I've used "arg1" and "arg2" instead of coming up with more creative names, but you can use any String keys you'd like. See Bundle and its superclass BaseBundle to see all of the data types Bundle supports in its get and put methods. This Bundle is also useful for saving Fragment data; if your app is interrupted by a phone call and Android destroys your Activity to save memory, you can use onSaveInstanceState to put form field data into the Bundle and then restore that information in onCreate.
Finally, note that you don't need to create a static factory method like newInstance; you could also have your consumers create a new YourFragment() instance and pass in a particular Bundle design themselves. However, at that point the Bundle structure becomes a part of your API, which you may not want. By creating a static factory method (or Factory object or other structure), you allow the Bundle design to be an implementation detail of your Fragment, and provide a documented and well-kept structure for consumers to create new instances.

What is the difference between 2 ways to set arguments of fragment

I want to pass a data object to Fragment, here are two ways to do this:
public class MyFragment extends Fragment {
private Serializable way1;
private Serializable way2;
public void setDataWay1(Serializable way1) {
this.way1 = way1;
}
public void setDataWay2(Serializable way2) {
Bundle data = new Bundle();
data.putSerializable("data", way2);
setArguments(data);
}
}
So, what is the difference between the 2 ways? Sometimes, way1 may cause NullPointerException,why? If I want to pass a OnClickListener to Fragment, what should I do?
While both methods can set the appropriate data to your fragment for first time initialization. Note that fragments will be recreated and destroyed by the system (for example on screen rotation). When that happens the system will not really call the setter way (method 1) hence, it will be a giant FAIL. Therefore, it is recommended to use the setArguments() way.

Why use serialization to pass info to a fragment?

I have a list like fragment, and currently I am passing in info like so:
Fragment:
public void populate(Map<String, List<Book>> booksGroupedByType)
{
BookListAdapter bookListAdapter = new BookListAdapter(this.getActivity(), booksGroupedByType);
_lstBooks.setAdapter(bookListAdapter);
}
Activity:
private void populateBooksFragment()
{
Map<String, List<Book>> booksGroupedByType = _repository.getAllBooksGroupedByType();
BookListFragment bookListFragment = (BookListFragment) getFragment(R.id.fragBooks);
if (bookListFragment != null)
{
bookListFragment.populate(booksGroupedByType);
}
}
Then I felt it would be better if I could pass this information when creating the fragment, since we have no constructor available I looked up the method and found this:
public static DetailsFragment newInstance(int index) {
DetailsFragment f = new DetailsFragment();
// Supply index input as an argument.
Bundle args = new Bundle();
args.putInt("index", index);
f.setArguments(args);
return f;
}
I tried to implement, but found my Map was not serializable as it was and needed more work. So my question is, why go through this? is there a disadvantage to using my original approach (populate), which would even be faster than serializing?
I thought perhaps my fragment will lose its data when rotated, but no, when rotating (in emulator) the list was kept intact.
Let's say you have some data obtained in time/resource consuming way. If you don't want to download them each time configuration changes (and activity is destroyed), you have to somehow persist them.
First option is to put data into the bundle, so it will be available for the fragment even after it is autorecreated by the system. It may work for simple types, but for arbitrary object it's usually not an option because of performance reasons (serialization/parcelization).
Second option would be retaining the fragment, by setting a flag in fragment's onCreate():
setRetainInstanceState(true)
In that case fragment won't be destroyed after configuration change, but just detached from activity being destroyed, and attached to the new one. Any data you will pass e.g. via setters will be available too.
See also: Understanding Fragment's setRetainInstance(boolean)

Parameterised constructor for a fragment in android

I have the following constructor inside a fragment:-
public PlaceDialogFragment(Place place, DisplayMetrics dm){
super();
this.mPlace = place;
this.mMetrics = dm;
}
I have also tried this:-
public static final DialogFragment newInstance(Place place, DisplayMetrics dm)
{
DialogFragment fragment = new DialogFragment();
Bundle bundle = new Bundle(2);
bundle.putParcelable("Place", place);
bundle.putLong("Metrics", dm);
fragment.setArguments(bundle);
return fragment ;
}
But There is an error on bundle.putLong("Metrics", dm) line
Here Place is a class which implements the Parceable interface
But i get an error saying:-
Avoid non-default constructors in fragments: use a default constructor plus Fragment#setArguments(Bundle) instead
Any suggestions how to resolve this?
The reason you should use default constructors and pass arguments as bundles is because when the system restores your fragment state, it's gonna call the default constructor and restore the bundle. If you get your parameters from the bundle, then you can restore the state correctly.
Using your current method, everything you do in your custom constructor will be lost when the fragment is recreated.
See this answer for an example.
Use setArguments instead, while transacting the fragment, pass the constructor params in bundle, then use them in fragment using getArguments()
How to use setArguments() and getArguments() methods in Fragments?
What's the point of setArguments?
http://developer.android.com/reference/android/app/Fragment.html
You added :
bundle.putLong("Metrics", dm);
Do like this:
bundle.putFloat("Metrics_density", dm.density);
Refer to DisplatMetrics documentation and add arguments separately, or add DisplayMetrics object as static in your Application memory, and use it from anywhere.
DisplayMetrics is not a Long object, not even parcelable, add relevant DisplayMetrics fields in bundle instead.
public static final PlaceDialogFragment newInstance(Place place, DisplayMetrics dm)
{
PlaceDialogFragment fragment = new DialogFragment();
Bundle bundle = new Bundle(2);
bundle.putParcelable("Place", place);
bundle.putFloat("Metrics_density", dm.density);
//bundle.putFloat("Metrics_other", dm.<other fields>);
fragment.setArguments(bundle);
return fragment ;
}
Note : Don't use public constructor with parameters.
The error message is right!
Avoid the use of Parameterized constructors to Fragment/Activity..
You can do "quick-fix" by going into Lint settings and excluding the rule + adding a default constructor. But quick fix is not the way. This will result in problem.
Consider this case, you just rotate the screen, then your fragment gets destroyed and recreated when you call super.onCreate(savedState) of your activity, which will call default constructor => this results in NullPointerException.
So respect the Android Lint, make use of setArguments() to pass the instance of Place. If Place is your model class, make it Parcelable
you can get arguments by calling getArguments() inside your fragment
Ideally, a fragment needs to reconstruct itself using only its arguments. A parameterised constructor does not work well for this as the parameters are lost in the case of a (for example) device orientation change (although you can mitigate this with a call to setRetainInstance).
Use a static method instead of a constructor to create your fragment.
e.g.
public static MyFragment newInstance() {
MyFragment f = new FragStateList_();
Bundle args = new Bundle();
args.putInt("someInt", someInt);
args.putString("someString", someString);
f.setArguments(args);
return f;
}
You should then include a default constructor so the system car re-create your fragment when it needs to.
In your updated question, you are attempting to place a DisplayMetrics objects into the bundle as a Long. The types are not compatible. Do not pass in the DisplayMetrics. Instead, try this in your fragment to get the DisplayMetrics object.
DisplayMetrics metrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
Ideally, a fragment needs to reconstruct itself using only its arguments. A parameterised constructor does not work well for this as the parameters are lost in the case of a (for example) device orientation change (although you can mitigate this with a call to setRetainInstance).
Like Kuffs wrote, the proper way to do this is to have a static method that calls a (default) constructor and after it is initialized, it adds your custom values. You can place arguments in that method, for example:
public static PlaceDialogFragment newInstance(Place place, DisplayMetrics dm) {
PlaceDialogFragment f = new PlaceDialogFragment(); //alternatively new Fragment()
f.mPlace = place;
f.mMetrics = dm;
return f;
}
Then from you Activity, you call it like:
PlaceDialogFragment pdf = PlaceDialogFragment.newInstance(param1, param2);

Why do I want to avoid non-default constructors in fragments?

I am creating an app with Fragments and in one of them, I created a non-default constructor and got this warning:
Avoid non-default constructors in fragments: use a default constructor plus Fragment#setArguments(Bundle) instead
Can someone tell me why this is not a good idea?
Can you also suggest how I would accomplish this:
public static class MenuFragment extends ListFragment {
public ListView listView1;
Categories category;
//this is my "non-default" constructor
public MenuFragment(Categories category){
this.category = category;
}....
Without using the non-default constructor?
It seems like none of the answers actually answer "why use bundle for passing parameters rather than non default constructors"
The reason why you should be passing parameters through bundle is because when the system restores a fragment (e.g on config change), it will automatically restore your bundle.
The callbacks like onCreate or onCreateView should read the parameters from the bundle - this way you are guaranteed to restore the state of the fragment correctly to the same state the fragment was initialised with (note this state can be different from the onSaveInstanceState bundle that is passed to the onCreate/onCreateView)
The recommendation of using the static newInstance() method is just a recommendation. You can use a non default constructor but make sure you populate the initialisation parameters in the bundle inside the body of that constructor. And read those parameters in the onCreate() or onCreateView() methods.
Make a bundle object and insert your data (in this example your Category object). Be careful, you can't pass this object directly into the bundle, unless it's serializable.
I think it's better to build your object in the fragment, and put only an id or something else into bundle. This is the code to create and attach a bundle:
Bundle args = new Bundle();
args.putLong("key", value);
yourFragment.setArguments(args);
After that, in your fragment access data:
Type value = getArguments().getType("key");
That's all.
Your Fragment shouldn't have constructors because of how the FragmentManager instantiates it.
You should have a newInstance() static method defined with the parameters you need, then bundle them and set them as the arguments of the fragment, which you can later access with the Bundle parameter.
For example:
public static 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 void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
title = getArguments().getInt(EXTRA_TITLE);
message = getArguments().getString(EXTRA_MESSAGE);
//...
}
This way, if detached and re-attached, the object state can be stored through the arguments, much like bundles attached to Intents.
If you use parameter for some class. try this
SomeClass mSomeInstance;
public static final MyFragment newInstance(SomeClass someInstance){
MyFragment f = new MyFragment();
f.mSomeInstance = someInstance;
return f;
}
I think, there is no difference between static constructor and two constructors (empty and parametrized one that stores arguments into a Fragment's arguments bundle), most probably, this rule of thumb is created to reduce probability of forgetting to implement no-arg constructor in Java, which is not implicitly generated when overload present.
In my projects I use Kotlin, and implement fragments with a primary no-arg constructor and secondary constructor for arguments which just stores them into a bundle and sets it as Fragment arguments, everything works fine.
If fragment uses non-default constructors after configuration changing the fragment will lose all data.

Categories

Resources