The question pretty much sums it up, I am trying to save my fragments so when I rotate the screen the application does not crash but I am not sure where or how to save my fragments, I have tried using a fragment manager and setting retainstate to true as well as checking the saved instance state.
This is my code :
EventsActivty - Hosts the fragments
#Override
public void onCreate(Bundle savedInstanceState) {
new AsyncLogin().execute(username, password);
super.onCreate(savedInstanceState);
username = getIntent().getStringExtra("username");
password = getIntent().getStringExtra("password");
}
private List<Fragment> getFragments(){
List<Fragment> fList = new ArrayList<Fragment>();
EventListFragment eventListFragment = (EventListFragment)
EventListFragment.instantiate(this, EventListFragment.class.getName());
EventGridFragment eventGridFragment = (EventGridFragment)
EventGridFragment.instantiate(this, EventGridFragment.class.getName());
fList.add(eventListFragment);
fList.add(eventGridFragment);
return fList;
}
The getFragments is called here, on the OnPostExecute of the AsyncTask
protected void onPostExecute(JSONObject jsonObject) {
try {
getEvents(jsonObject);
setContentView(R.layout.eventlist);
List<Fragment> fragments = getFragments();
pageAdapter = new MyPageAdapter(getSupportFragmentManager(), fragments);
ViewPager pager = (ViewPager)findViewById(R.id.viewpager);
pager.setAdapter(pageAdapter);
}
Fragment 1 : OnCreateView
#Override
public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
eventObjects = ((EventsActivity)getActivity()).getEventObjects();
setRetainInstance(true);
View view = inflater.inflate(R.layout.eventlist ,container,false);
final ListView listView = (ListView) view.findViewById(R.id.listView);
listView.setAdapter(new MyCustomBaseAdapter(getActivity(), eventObjects));
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> a, View v, int position, long id) {
Object o = listView.getItemAtPosition(position);
EventObject fullObject = (EventObject)o;
System.out.println("asd");
}
});
return view;
}
}
Fragment 2:
#Override
public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
setRetainInstance(true);
View view = inflater.inflate(R.layout.eventgrid ,container,false);
GridView gridView = (GridView) view.findViewById(R.id.eventgrid);
gridView.setAdapter(new ImageAdapter(view.getContext())); // uses the view to get the context instead of getActivity().
return view;
}
Actually, a FragmentActivity will automatically restore your fragments in onCreate().
Weird thing is, you call AsyncTask first, prior to calling through to super.onCreate() and retrieving username and password from the Intent.
Even having set that aside, that approach will make your activity spawn a new login task every time it's rotated.
Better way is to check savedInstanceState for null:
if (savedInstanceState == null) {
// first launch
new AsyncLogin().execute(username, password);
}
...
That way it's only going to run when Activity is created for the first time.
Second, you need to completely unbind login info from your activity. Make the AsyncTask return whatever login result you get to the Application and store it there. And your activity and fragments to retrieve that info from the Application - that way you get full control over you login procedure: you check whether there's an active session, if there isn't you check whether the AsyncTask is already running, if it isn't - you need to launch it. If it is - you need to wait for it to finish (show progress bar or something).
You should understand that retaining the fragments won't prevent the activity from being distroyed.
Now take a look here:
#Override
public void onCreate(Bundle savedInstanceState) {
new AsyncLogin().execute(username, password);
// .....
}
When a screen orientation change occurs, your activity is still destroyed. As a result a new AsyncTask is started. But when the old AsyncTask completes the job, it tries to deliver the result to the old activity, that was destroyed already. As a result this will cause a crash.
The solution would be to put the AsyncTask in the fragment itself. So that the instance to not be destroyed on orientation change.
See if this article can help you.
Related
I've been searching for hours and tried numerous methods but cannot seem to grasp my head around the idea / figure out how to retain/restore data in a ViewPager Fragment when it is destroyed and then recreated.
Here is what I have -
An activity where I setup the ViewPager and PageAdapter
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.view_pager);
//Setup pager and adapter
mPager = (ViewPager) findViewById(R.id.viewpager);
mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
mPager.setAdapter(mPagerAdapter);
}
My PageAdapter where I setup a fragment with a bundle using .newInstance()
#Override
public Fragment getItem(int position) {
ScreenSlidePageFragment fragment = new ScreenSlidePageFragment();
return fragment.newInstance(position);
}
My Fragment that has a layout that includes a TextView that shows the user a question, a picture, and two True/False buttons. New instance is returned back to the Adapter.
public static ScreenSlidePageFragment newInstance(int position) {
ScreenSlidePageFragment fragment = new ScreenSlidePageFragment();
Bundle args = new Bundle();
args.putInt("page_position", position + 1);
fragment.setArguments(args);
return fragment;
}
//the fragment is newly created for the first time or recreated when exiting the view
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_main, parent, false);
//Handle a question being displayed on each fragment
count = getArguments().getInt("page_position") - 1;
mQuestionText = (TextView)v.findViewById(R.id.questionText);
mQuestionText.setText(bank.get(count).getQuestion());
//change the image depending on correct / incorrect answer
mPhoto = (ImageView)v.findViewById(R.id.imageView);
trueButton = (Button)v.findViewById(R.id.true_button);
falseButton = (Button)v.findViewById(R.id.false_button);
//True Button is pressed
trueButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if(bank.get(count).getAnswer()) {
mPhoto.setImageResource(R.drawable.right);
clickable = false;
}
else {
mPhoto.setImageResource(R.drawable.wrong);
clickable = false;
}
trueButton.setClickable(clickable);
falseButton.setClickable(clickable);
}
});
What I cannot figure out for the life of me, is how to retain/save that fact that the user has pressed a button and which picture to display when the fragment is restored. I have tried a number of options using onResume(), getArguments(), onSaveInstanceState(), onActivityCreated() etc but none of them seem to work.
I can fix the problem by keeping all my ViewPager pages alive using setOffscreenPageLimit(total pages) but have read this is a bad idea since it takes up a large amount of memory.
Here is an example that I use that can help you where an ArrayList of urls for pictures is saved for later when the view is recreated, you need to override onSavedInstanceState and save your variables in a bundle, then retrieve the values from the bundle when the view is created again, hope this can help
ArrayList<String> picutersUrl = new ArrayList<String>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState!=null)
{
picutersUrl = savedInstanceState.getStringArrayList("My Pictures");
}
#Override
protected void onSaveInstanceState(Bundle bundle)
{
super.onSaveInstanceState(bundle);
bundle.putStringArrayList("My Pictures", picutersUrl );
}
I too have used PagerAdapter to support few static tab widgets. I think you need to add code to ScreenSlidePagerAdapter. Google webpage at PagerAdapter.
Note and read the details on override method instantiateItem. This is where you populate the UI.
Code example for ScreenSlidePagerAdapterfrom (PagerAdapter subclass), using your posted code:
#Override
public Object instantiateItem(ViewGroup container, int position) {
View v = inflater.inflate(R.layout.fragment_main, parent, false);
...
trueButton = (Button)v.findViewById(R.id.true_button);
}
In reality with Views in this framework, you're responsible on saving the state of UI elements. In my ListView, I am caching and saving up to 100 rows of data and refreshes it in an Adapter, for example. For caching, I created custom class containing the UI data.
We have a web service which serves up an XML file via a HTTP Post.
I am downloading and parsing this xml file into an object to populate some views inside a couple of fragments held in a FragmentPagerAdapter. I get this XML file via an AsyncTask and it tells my fragments the process has finished via a listener interface.
From there, I populate the view inside the fragment with data returned from the web service. This is all fine until the orientation changes. From what I understand, the ViewPager's adapter is supposed to retain the fragments it's created, which is fine, and which I want to happen, and I know the fragment's onCreateView method is still called to return the view. I've spent the last day or so hunting through posts here and the Google docs etc and I can't find a concrete method that lets me do what I want to do: retain the fragment, and it's already populated view so that I can simply restore it when the orientation changes and avoid unneccesary calls to the web service.
Some code snippets:
In the main activities onCreate:
mViewPager = (ViewPager) findViewById(R.id.viewpager);
if (mViewPager != null) {
mViewPager.setAdapter(new PagerAdapter(getSupportFragmentManager()));
}
if (savedInstanceState == null) {
if (CheckCredentials()) {
Refresh(0,0);
} else {
ShowCredentialsDialog(false);
}
}
Refresh method in main activity...
public void Refresh(Integer month, Integer year) {
if (mUpdater == null) {
mUpdater = new UsageUpdater(this);
// mUpdater.setDataListener(this);
}
if (isConnected()) {
mUpdater.Refresh(month, year);
usingCache = false;
mProgress.show();
} else {
mUpdater.RefreshFromCache();
usingCache = true;
}
}
This is the entire Fragment in question, minus some of the UI populating code as it's not important to show the setting of text in textviews etc...
public class SummaryFragment extends Fragment implements Listeners.GetDataListener {
private static final String KEY_UPDATER = "usageupdater";
private UsageUpdater mUpdater;
private Context ctx;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
this.ctx = activity;
}
private View findViewById(int id) {
return ((Activity)ctx).findViewById(id);
}
public void onGetData() {
// AsyncTask interface method, will be called from onPostExecute.
// Populate view from here
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_usagesummary, container, false);
mUpdater = (UsageUpdater) getArguments().getSerializable(KEY_UPDATER);
mUpdater.setDataListener(this);
return view;
}
}
If I understand any of this 'issue' it's that I'm returning an empty view in onCreateView but I don't know how to retain the fragment, return it's view prepopulated with data and manage all web service calling from the main activity.
In case you can't tell, Android is not a primary language for me and this probably looks a shambles. Any help is appreciated I'm getting rather frustrated.
If you're not using any alternative resources when the Activity is re-created, you could try handling the rotation event yourself by using configChange flags in your AndroidManifest:
<activity
...
android:configChanges="orientation|screenSize"
... />
There is no way to keep the same, pre-populated Views if your Activity is re-created since this would cause a Context leak:
http://www.curious-creature.org/2008/12/18/avoid-memory-leaks-on-android/
Im having some problems when it comes to porting my app from the normal activity style to the fragment style. Im beginning to notice that when a fragment gets recreated, or popped from the backstack it loses its views. When I say that Im talking about a listview in particular. What im doing is im loading items into the listview, then rotating the screen. When it goes back through, it gets a nullpointerexception. I debug it and sure enough the listview is null. Here is the relevant code to the fragment
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstanceState) {
return inflater.inflate(R.layout.sg_question_frag, viewGroup, false);
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
list = (ListView)getActivity().findViewById(R.id.sgQuestionsList);
if (savedInstanceState != null) {
catId = savedInstanceState.getInt("catId");
catTitle = savedInstanceState.getString("catTitle");
}
populateList(catId, catTitle);
}
And here is how it is called (keep in mind there are a few other fragments that im working with as well)
#Override
public void onTopicSelected(int id, String catTitle) {
// TODO Auto-generated method stub
FragmentManager fm = this.getSupportFragmentManager();
SGQuestionFragment sgQuestFrag = (SGQuestionFragment) fm.findFragmentByTag("SgQuestionList");
FragmentTransaction ft = fm.beginTransaction();
//If the fragment isnt instantiated
if (sgQuestFrag == null) {
sgQuestFrag = new SGQuestionFragment();
sgQuestFrag.catId = id;
sgQuestFrag.catTitle = catTitle;
//Fragment isnt there, so we have to put it there
if (mDualPane) {
//TO-DO
//If we are not in dual pane view, then add the fragment to the second container
ft.add(R.id.sgQuestionContainer, sgQuestFrag,"SgQuestionList").commit();
} else {
ft.replace(R.id.singlePaneStudyGuide, sgQuestFrag, "SqQuestionList").addToBackStack(null).commit();
}
} else if (sgQuestFrag != null) {
if (sgQuestFrag.isVisible()) {
sgQuestFrag.updateList(id, catTitle);
} else {
sgQuestFrag.catId = id;
sgQuestFrag.catTitle = catTitle;
ft.replace(R.id.sgQuestionContainer, sgQuestFrag, "SgQuestionList");
ft.addToBackStack(null);
ft.commit();
sgQuestFrag.updateList(id, catTitle);
}
}
fm.executePendingTransactions();
}
What I would ultimately want it to do is to completely recreate the activity, forget the fragments and everything and just act like the activity was started in landscape mode or portrait mode. I dont really need the fragments there, I can recreate them progmatically with some saved variables
If you want to get a reference to a view from within a Fragment always look for that View in the View returned by the getView() method. In your case, at the time you look for the ListView the Fragment's view probably isn't yet attached to the activity so the reference will be null. So you use:
list = (ListView) getView().findViewById(R.id.sgQuestionsList);
in my app I'm using one activity and two fragments. The app uses a layout with a container so the fragments are added via transactions. The first fragment contains a listview and the other fragment a detail view for the listview items.
Both fragments use setRetainInstance(true). The fragments are added via a replace transaction and addToBackStack(null) is set. The listfragment contains an instance variable which holds some infos for the list. Now I'm changing to detail and press back and the instance variable is null. I read about setRetainInstance and addToBackStack and removed addToBackStack, but even then the instance variable is null.
Does anyone know what I might be doing wrong?
regards,
Thomas
setRetainInstance(true) will tell the FragmentManager to keep the fragment around when the containing Activity is killed and rebuilt for some reason. It doesn't guarantee that the Fragment instance will stick around after a transaction to add or replace. It sounds like your adapter is being garbage collected and you're not creating a new one.
A more generally easy solution would be to make a viewless Fragment to retain your ListAdapter. The way you do this is to create the Fragment, set the retain instance to true, and return null in the method onCreateView(). To add it, just called addFragment(Fragment, String) via the FragmentTransaction. You never remove or replace it, so it will always stay in memory for the length of the app. Screen rotations won't kill it.
Whenever your ListFragment is created, in onCreateView() get the FragmentManager and use either the method findFragmentById() or FindFragmentByTag() to retrieve your retained fragment from memory. Then get the adapter from that fragment and set it as your adapter for the list.
public class ViewlessFragment extends Fragment {
public final static string TAG = "ViewlessFragment";
private ListAdapter mAdapter;
#Override
public ViewlessFragment() {
mAdapter = createAdater();
setRetainInstance(true);
}
#Override
public void onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return null;
}
public ListAdapter getAdapter() {
return mAdapter;
}
}
public class MyListFragment extends ListFragment {
final public static String TAG = "MyListFragment";
#Override
public void onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View returnView = getMyView();
final ViewlessFragment adapterFragment = (ViewlessFragment) getFragmentManager().findFragmentByTag(ViewlessFragment.TAG);
setListAdapter(ViewlessFragment.getAdapter());
return returnView;
}
}
public class MainActivity extends FragmentActivity {
#Override
public void onCreate(Bundle icicle) {
// ... setup code...
final FragmentManager fm = getSupportFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
ViewlessFragment adapterFragment = fm.findFragmentByTag(ViewlessFragment.TAG);
if(adapterFragment == null) {
ft.add(new ViewlessFragment(), ViewlessFragment.TAG);
}
ft.add(R.id.fragmentContainer, new MyListFragment(), MyListFragment.TAG);
ft.commit();
}
}
I have a ViewPager with two Fragments which I instantiate in onCreate of my FragmentActivity.
private List<Fragment> fragments = new Vector<Fragment>();
fragments.add(Fragment.instantiate(this,Frag_1.class.getName()));
fragments.add(Fragment.instantiate(this,Frag_2.class.getName()));
this.vPagerAdapter = new Adapt(super.getSupportFragmentManager(),fragments);
vPager = (ViewPager) super.findViewById(R.id.pager);
vPager.setAdapter(vPagerAdapter);
My second Fragment has a method inside that I call to update my ListView - refreshList():
public class Frag_2 extends Fragment {
private ListView list;
private ArrayList<data> data;
private boolean firstCreation=true;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setRetainInstance(false);
}
public void onAttach(Activity activity) {
// TODO Auto-generated method stub
super.onAttach(activity);
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.layout, container, false);
list = (ListView) view.findViewById(R.id.lst);
//this.setRetainInstance(true);
return view;
}
public void refreshList(ArrayList <data> data){
if(data!=null){
ArrayAdapter<data> adapter = new Item_data_adapter(getActivity(),data);
list.setAdapter(adapter);}
}
}
Called from my FragmentActivity
//Something
Frag_2 fr = (Frag_2) vPagerAdapter.getItem(1);
if (fr.getView() != null) {
fr.refreshList(data);
}
It works fine until I change the orientation of the screen. Correct me if I'm wrong, but I was searching for hours and I didn't find a solution or a good explanation, the FragmentActivity is created only one time and the Fragments are attached to it but the Fragments recreate on configuration changes.
Now, when the orientation changes I don't get the View from onCreateso when I try to get the View from the Fragment it returns null and my refreshList() method isn't called. How can I fix this?
I fixed the problem this way:
In the onCreate of the FragmentActivity
if(savedInstanceState!=null){
frag1 = (frag_1) getSupportFragmentManager().getFragment(savedInstanceState, frag_1.class.getName());
frag2 = (frag_2) getSupportFragmentManager().getFragment(savedInstanceState, frag_2.class.getName());
}
else{
frag1 = (frag_1) Fragment.instantiate(this,frag_1.class.getName());
frag2 = (frag_2) Fragment.instantiate(this,frag_2.class.getName());
}
fragments.add(frag1);
fragments.add(frag2);
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
getSupportFragmentManager().putFragment(outState, frag_1.class.getName(), frag1);
getSupportFragmentManager().putFragment(outState, frag_2.class.getName(), frag2);
}
Maybe it's not the best solution in the universe, but it looks like it works...
When u want to refresh the List do something like this :
public void setView() {
Frag_2 fr = (Frag_2) vPagerAdapter.getItem(1);
fragmentManager.beginTransaction().detach(fr).commit();
fragmentManager.beginTransaction().attach(fr).commit();
}
If you are using a dynamic fragment, you need to test first to prevent creating a second instance of a fragment.
To test whether the system is re-creating the activity, check whether the Bundle argument passed to your activity’s
onCreate() is null.
If it is non-null, the system is re-creating the activity. In this case, the activity automatically re-instantiates existing
fragments.
If it's null you can safely instantiate your dynamic fragment. For example:
public void onCreate(Bundle savedInstanceState) {
// ...
if (savedInstanceState != null) {
FragmentManager fragmentManager = getFragmentManager()
// Or: FragmentManager fragmentManager = getSupportFragmentManager()
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
}
}
The Fragment class supports the onSaveInstanceState(Bundle) method (but not the onRestoreInstanceState() method) in much the same way as the Activity class.
The default implementation saves the state of all the fragment’s views that have IDs.
You can override this method to store additional fragment state information.
If the system is re-creating the fragment from a previous saved state, it provides a reference to the Bundle containing that state to the onCreate(), onCreateView(), and onActivityCreated() methods; otherwise, the
argument is set to null.
If you want a detailed info, here's a good talk by Ken Jones of Marakana