Android: Fragment's fields get cleared while entering Multi-Window mode - android

I have a Fragment that is used in a ViewPager. Fragment instances are constructed via factory method like this:
public static MyFragment newInstance(int sectionNumber, List<String> aList) {
MyFragment fragment = new MyFragment();
fragment.list = aList;
return fragment;
}
The list is passed from the activity's onCreate() method to a SectionsPagerAdapter instance where I call newInstance() for my fragment. The list can never be null (there are checks in the code).
Problem: The application fails with NPE in onStart() method when I try to enter
Multi-Window view. The list is null.
What I noticed is that the fields that I set in newInstance() call are now nulls. Only those that are initialized in onCreateView() are assigned with objects. I guess the system initializes my Fragment in a different way bypassing my newInstance() method.
Question: Why fields that are set in my factory method are set to null? Why it happens only in Multi-Window mode? How to prevent this?
Probably I misuse something, thought creating a fragment via factory method is what Astroid Studio offers by default.

You should never be setting data in your fragment like that because of this exact issue.
When your app goes into multi window mode your fragment/activity get recreated so any data you passed in like that is lost.
What you should be doing that does handles configuration changes is pass your data in a bundle to your fragment via setArguments. When your fragment loads you then get the bundle via getArguments then grab the information from the bundle and proceede as normal.
get/setArguments holds the bundle information through configuration changes
it would look something like this
myFragment = new MyFragment();
Bundle b = new Bundle();
b.putLong("list",aList);
myFragment.setArguments(b);

Multi-window is a form of Configuration change. I bet if you rotate the device the same issue will occur.
In your Activity's AndroidManifest block, try adding the following (as a property of <activity>:
android:configChanges="keyboard|screenLayout|screenSize|orientation"
This will tell Android that you want to handle Configuration changes yourself, and will call your Activity's (and Fragment's) onConfigurationChanged() method instead of handling itself. Leave that method alone and you should be good.

Related

Unknown fragment after rotation

In my application I create a fragment with the keyword new and set it by FragmentTransaction.
Upon rotation a stumbled upon a NullPointerException in the method onActivityCreated() indicating a missing injection, that I do after the call to new. I suspected the fragment was not created by my code und proved this by logging the hashCode(). It looks like a fragment is created automatically by the system upon rotation.
Where does it come from?
Is it created by the fragment manager?
How am I supposed to use it correctly?
How can I access it, to set the missing value?
For now I ignore it by testing for the null value, in which case onActivityCreated() does nothing. Instead use the fragment I create with new. However, this does not feel very satisfying, to throw away an object, that was already created.
Where does it come from? Is it created by the fragment manager?
On Activity recreation, Android will restore the fragments which already exist in activity's fragments manager
How am I supposed to use it correctly?
public void onCreate(Bundle savedInstanceState){
if(savedInstanceState == null){
//activity is created for first time
//commit the fragment
}else{
//Activity is recreated(by means of rotation or something else)
//Dont commit the fragment, fragmet will be restored by the system
}
}
How can I access it, to set the missing value?
Normally, you have to handle this inside the fragment using onSaveInstanceState method. You can get the fragment instance by using, getSupportFragmentManager.findFragmentById(R.id.container) or getSupportFragmentManager.findFragmentByTag(tagName)

Passing a listener through a chain of Fragments?

I want to be able to define a listener (an Activity, Fragment, etc) and be able to pass it through any number of nested Fragments before I decide to finally invoke the callback. That way it can call callback.someFunction() and it doesn't need to know what Activity or Fragment that callback is attached to.
Right now it seems, though, that there is no good way to send a listener through a bunch of Fragments. I initially considered passing it through the constructors, but then the listener reference would be nulled out on a configuration change like a screen rotation.
I then considered the onAttach() methods, but these only give you access to the context of the base Activity which doesn't necessarily do what I want, either.
I also considered passing the listeners in through newInstance() (which is normally how you save arguments passed into Fragments because the contents of getArguments() survives configuration changes via the Bundle), but I could not see any good way to save the listener in the argument Bundle.
What can I do?
What you should do is recreate everything when a configuration change happens or when the activity is re-created and its instance state restored...which is the same for that matter...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.acitivyt_layout);
if(savedInstanceState != null){
//1. restore instance state here
savedInstanceState.getString("whatever");
//2. try to find the fragment
Fragment f = getSupportFragmentManager().findFragmentByTag("FRAGMENT TAG");
//3. make sure the fragment is correctly set up if already loaded
if(f != null){
((ConcreteFragmentType)f).setListener(your_listener);
//bring the fragment to the front or show it using the fragment manager
}
//4. or else add a brand-new instance
else{
ConcreteFragmentType c = ConcreteFragmentType.newInstance();
c.setListener(your_listener);
getSupportFragmentManager().beginTransaction()
.add(R.id.fragmentContainer, c)
.commit();
}
}
}

How to set data to fragments (setArgumets() vs setters method)

Is that compulsory to set data using bundle and set argument?
What is wrong Here?
MyFragment frag = new MyFragment ()
frag.setData(mSchoolData);
//add to back stack stuff.
by using the setArgs() you will ensure that this Fragment can be recreated due to lifecycle event...while by passing arguments with your own setters it may not work properly under certain circustances. That is why it is absolutely recommended to either uset Args OR use Intent extras, these will always be automatically provided by the system if the fragment gets recreated.

Fragments, Parameters, and ActionBar 'Up' Navigation

So I am trying to get some experience with Fragments, but I'm finding some roadblocks.
My current situation is as follows.
I have an activity that displays a List whose content is determined by Extra Intent parameters sent from the 'calling' activity.
This List activity uses ListFragment declared in the XML like so:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" android:background="#color/black">
<fragment class="com.pixlworks.NLC.DirectoryBrowse$ListingFragment"
android:id="#+id/listing"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Currently I get the parameter that indicates the type of content directly in the Fragment by accessing the Extra data of the Activity Intent (or saved Bundle if available):
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
mListingType = savedInstanceState.getString(Utils.DIRECTORY_TYPE_STORE_KEY);
else
mListingType = getActivity().getIntent().getStringExtra(Utils.DIRECTORY_TYPE_STORE_KEY);
// get content by type, create and set the adapter
}
Now part of my problem is that I am not sure this is the right way to 'pass' that parameter from the Activity to the Fragment.
On top of that, I am getting issues with this setup when using the Action Bar's UP Navigation. When I click on an item in this List Activity it goes to another activity showing the details of the selected item. From this detail activity:
If I use the back button, the List Activity is brought back from the stack as usual and everything works fine.
If I use the ActionBar's UP (despite following steps here), it would seem that a new instance is created instead of using the one in the stack and this new instance obviously is not getting the Extra parameter in the Intent. Since I am expecting the value to exist in the saved Bundle or in the Intent, my app crashes in this situation.
So to boil things down, I am not sure which of these to follow and how to make them work properly with 'UP' navigation:
A) Hold the 'type' parameter in a field in the Activity and save it in the Activity's Bundle onSaveInstanceState. In which case I am not sure how to then pass the value to the Fragment. In this case I would just need to make sure that UP calls the existing instance of the Activity List
B) Continue with my current setup of saving the value in the Fragment instead of the Activity, but again, how to handle the UP navigation correctly?
I know it is kind of multiple things I am asking here at the same time, but they are all connected, so I hope that I can get some help on this.
Thanks for any help in advance!
The UP navigation makes more sense to be used within the same activity level. That is the intention of the codes that you followed in the developers page. Because you started a new activity, if you want to return to previous activity like the back button you will need to call finish() to destroy the details activity first.
As for passing data from activity to fragment, when you create a new instance of fragment, you can pass the data to it as bundle, for example:
// in fragment class
public static MyFragment newInstance(Bundle arg) {
MyFragment f = new MyFragment();
f.setArguments(arg);
return f;
}
When you create a new fragment, you can call:
// in activity
Bundle arg = new Bundle();
int info = ...;
arg.putInt("INFO",info);
...
MyFragment mFragment = MyFragment.newInstance(arg);
Finally, to get the data in fragment:
int info = getArguments().getInt("INFO");
...
Instead of directly calling MyFragment mFragment = new MyFragment() to instantiate the fragment, you should use a static method to instantiate it. This is to prevent some crashes which might happen if you rotate the screen and the framework complains that it couldn't find a public empty constructor.
UPDATE
To answer your questions:
1) Say you start from activity A -> activity B. Then in activity B you press the up button. By logic of use, the up button will not bring you back to activity A, because its intention is to navigate one level up,but still inside, activity B. To return to activity A, you need to call finish() to destroy activity B first.
2) If your fragment is created in xml, you still can set arguments. In your xml, you set an id for the fragment android:id="#+id/fragment_id", then
// in activity
FragmentManager fm = getSupportFragmentManager(); // or getFragmentManager() if you don't have backward compatibility
MyFragment mFragment = fm.findFragmentById(R.id.fragment_id);
Bundle arg = new Bundle();
// put data blah blah
mFragment.setArguments(arg);
Just make sure you set the arguments before you use the fragment.
Simply said, intent is used when you pass data between calling activities; bundle is used when you want to pass data from activity to fragment.

Proper way to give initial data to fragments?

So I've learned that I need an empty constructor in order for my fragments not to crash on reinitialization. My problem is that I use lists of data for my fragments when they are initialized (at least some of them). So what would be a good way to start new fragments with a list of data. Should I in the OnCreate() make a getData method which gets the data from some other source or what would be a proper approach?
Feeding the bundle with data really wouldn't be a very good approach as I have a lot of data.
So let's take a case (I understand it tons better that way).
When a user clicks on a button the fragment is started. What I used to do was creating a new fragment this way:
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.center_container, new DetailFragment(item));
fragmentTransaction.addToBackStack(DETAIL_TAG);
fragmentTransaction.commit();
Then in my fragment:
public DetailFragment(EventItem item) {
mItem = item;
mPlaces = Database.getContainerPlaces(getActivity()).getValidItems();
}
I can't give all the data to a bundle, so that wouldn't work. So what should I do?
A: Should I initialize the fragment with the empty constructor and then from my activity use setters to set the data directly in the fragment? However, won't I be missing data if the user presses home, Android closes the fragment and the user later returns?
B: Should I initialize the fragment with factory pattern and call setRetainInstance(true), give the fragment a key for identifying the data, and then letting the fragment fetch the data needed in onCreateView from some third source?
C: Should I just make an empty constructor and then in onCreate() fetch the data needed for the fragment?
It should be noted that the app is locked in portrait so the issue is primarily with maintaining the objects when Android closes and the user restarts.
So what would be a good way to start new fragments with a list of data.
Use the factory pattern and the "arguments" Bundle, such as:
package com.commonsware.empublite;
import android.os.Bundle;
public class SimpleContentFragment extends AbstractContentFragment {
private static final String KEY_FILE="file";
protected static SimpleContentFragment newInstance(String file) {
SimpleContentFragment f=new SimpleContentFragment();
Bundle args=new Bundle();
args.putString(KEY_FILE, file);
f.setArguments(args);
return(f);
}
#Override
String getPage() {
return(getArguments().getString(KEY_FILE));
}
}
If you are retaining your fragment instance, you should be able to get away with just using ordinary setters to put stuff in data members. The "arguments" Bundle is retained as part of configuration changes, so for non-retained instances, this is the way to ensure your setup data is retained if the user rotates the screen, etc.

Categories

Resources