I am being more thorough with hope the question will actually be easier to understand.
Activity purpose: allow users to select images from gallery; display thumbnail of image in ListFragment along with title user gave the image; when user is finished save each image's uri and title, and the name user gave this collection of images.
Problem: When device is rotated the FragmentList is losing all the images and titles the user already chose, ie, all the rows of the list are missing.
Attempted problem solving:
Implemented the RetainedFragment to save the List collection on device rotation. Previously I had not done this and figured "Ah, the adapter is fed a blank List collection on creation. I'll save the state of the List and then when Activity's onCreate is called I can feed the retained List to the Adapter constructor and it'll work." But it didn't.
Then I thought, "Of course it is not working, you haven't notified the adapter of the change!" So I put the adapter.notifyDataSetChanged()
in the onCreate. This didn't work.
Then I moved the adapter.notifyDataSetChanged()
to onStart
thinking I might need to notify the adapter later in the activity's lifecycle. Didn't work.
Note: I have another activity in this same app that use this same custom ListViewFragment and the state of the ListFragment is being preserved with device orientation changes. That activity has two principle differences: the fragment is hard coded into the .xml (I don't think that would make a difference, other than perhaps maybe Android's native saving of .xml fragments is different than programmatically added ones); and that activity uses a Loader and LoaderManager and gets its data from a Provider that I built (which gathers data from my SQLite database). Looking at the differences between these two activities is what caused me to think "you're not handling the data feeding the adapter appropriately somehow" and inspired me to use the RetainedFragment to save the List collection when the device is rotated.
...which is prompting me to think about figuring out how to, as Android says on their Loader page about LoaderManager:
"An abstract class associated with an Activity or Fragment for managing one or more Loader instances. This helps an application manage longer-running operations in conjunction with the Activity or Fragment lifecycle; the most common use of this is with a CursorLoader, however applications are free to write their own loaders for loading other types of data."
It is the "loading other types of data" part that has me thinking "Could I use a LoaderManager to load the List data? Two reasons I shy from this: 1) what I have already, at least conceptually, ought to work; 2) what I'm doing currently isn't really a "longer-running operation" at all, I don't think.
Research:
StackOverflow Fool proof way to handle Fragment on orientation change
Once for all, how to correctly save instance state of Fragments in back stack?
Save backstack fragments.
Not shown in my code pasted below, but my activity dynamically creates three other fragments and I use the following if savedInstanceState !=null
and those fragments' states are saved without doing any work in onSaveInstanceState()
(this is partly why it feels like my problem isn't with doing something in onSaveInstanceState
because Android handles the saving my other fragments state so shouldn't it do it, too, with the ListFragment? Seems like it should).
if(savedInstanceState.containsKey(AddActivity_Frag1.F1_TAG)){
frag1 = (AddActivity_Frag1)getFragmentManager().getFragment(savedInstanceState, AddActivity_Frag1.F1_TAG);
}
Many of the StackOverflow questions surrounding my query seem to be mostly about how to save the scroll position of the ListFragment with orientation change but I don't need to do that (though I did read them looking for tips that might help).
Activity - with many, hopefully unrelated things, removed:
public class AddActivity extends Activity{
// data collection
List<ImageBean> beanList;
// adapter
AddCollectionAdapter adapter;
// ListViewFragment tag
private static final String LVF_TAG = "list fragment tag";
// fragment handles
ListViewFragment listFrag;
// Handles images; LruCache for bitmapes
ImageHandler imageHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add2);
// Create ImageHandler that holds LruCache
imageHandler = new ImageHandler(this, getFragmentManager());
// Obtain retained List<ImageBean> or create new List<ImageBean>.
RetainedFragment retainFragment = RetainedFragment.findOrCreateRetainFragment(getFragmentManager());
beanList = retainFragment.list;
if(beanList == null){
beanList = new ArrayList<ImageBean>();
retainFragment.list = beanList;
}
// create fragments
if(savedInstanceState == null){
listFrag = new ListViewFragment();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(R.id.add_fragFrame, listFrag, LVF_TAG);
ft.commit();
}else{
listFrag = (ListViewFragment)getFragmentManager().findFragmentByTag(LVF_TAG);
}
// create adapter
adapter = new AddCollectionAdapter(this, beanList);
// set list fragment adapter
listFrag.setListAdapter(adapter);
}
@Override
protected void onStart() {
// TESTING: If device orientation has changed List<ImageBean> was saved
// with a RetainedFragment. Seed the adapter with the retained
// List.
adapter.notifyDataSetChanged();
super.onStart();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// Android automatically saves visible fragments here. (?)
super.onSaveInstanceState(outState);
}
/*
* ImageBean.
*/
public static class ImageBean{
private String collectionName; // Title of image collection
private String imageUri; // Image URI as a string
private String imageTitle; // Title given to image
public ImageBean(String name, String uri, String title){
collectionName = name;
imageUri = uri;
imageTitle = title;
}
public String getCollectionName() {
return collectionName;
}
public String getImageUri() {
return imageUri;
}
public String getImageTitle() {
return imageTitle;
}
}
/*
* Called when user is finished selecting images.
*
* Performs a bulk insert to the Provider.
*/
private void saveToDatabase() {
int arraySize = beanList.size();
final ContentValues[] valuesArray = new ContentValues[arraySize];
ContentValues values;
String imageuri;
String title;
int counter = 0;
for(ImageBean image : beanList){
imageuri = image.getImageUri();
title = image.getImageTitle();
values = new ContentValues();
values.put(CollectionsTable.COL_NAME, nameOfCollection);
values.put(CollectionsTable.COL_IMAGEURI, imageuri);
values.put(CollectionsTable.COL_TITLE, title);
values.put(CollectionsTable.COL_SEQ, counter +1);
valuesArray[counter] = values;
counter++;
}
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... arg0) {
getContentResolver().bulkInsert(CollectionsContentProvider.COLLECTIONS_URI, valuesArray);
return null;
}
@Override
protected void onPostExecute(Void result) {
// End this activity.
finish();
}
};
task.execute();
}
public ImageHandler getImageHandler(){
return imageHandler;
}
}
class RetainedFragment extends Fragment{
private static final String TAG = "RetainedFragment";
// data to retain
public List<AddActivity.ImageBean> list;
public static RetainedFragment findOrCreateRetainFragment(FragmentManager fm){
RetainedFragment fragment = (RetainedFragment)fm.findFragmentByTag(TAG);
if(fragment == null){
fragment = new RetainedFragment();
fm.beginTransaction().add(fragment, TAG);
}
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}
ListFragment:
public class ListViewFragment extends ListFragment {
ListFragListener listener;
public interface ListFragListener{
public void listFragListener(Cursor cursor);
}
@Override
public void onCreate(Bundle savedInstanceState) {
// Retain this fragment across configuration change
setRetainInstance(true);
super.onCreate(savedInstanceState);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Set listener
if(activity instanceof ListFragListener){
listener = (ListFragListener)activity;
}else{
//Instantiating activity does not implement ListFragListener.
}
}
@Override
public void onListItemClick(ListView listView, View v, int position, long id) {
// no action necessary
}
}
Adapter:
public class AddCollectionAdapter extends BaseAdapter {
// data collection
List<ImageBean> beanList;
// layout inflator
private LayoutInflater inflater;
// context
Context context;
public AddCollectionAdapter(Context context, List<ImageBean> beanList){
this.context = context;
this.beanList = beanList;
inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public int getCount() {
return beanList.size();
}
@Override
public Object getItem(int position) {
return beanList.get(position);
}
@Override
public long getItemId(int arg0) {
// collection not from database nor is going directly to database; this is useless.
return 0;
}
// holder pattern
private class ViewHolder{
ImageView imageView;
TextView titleView;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
View xmlTemplate = convertView;
if(xmlTemplate == null){
//inflate xml
xmlTemplate = inflater.inflate(R.layout.frag_listview_row, null);
// initilaize ViewHolder
holder = new ViewHolder();
// get views that are inside the xml
holder.imageView = (ImageView)xmlTemplate.findViewById(R.id.add_lvrow_image);
holder.titleView = (TextView)xmlTemplate.findViewById(R.id.add_lvrow_title);
// set tag
xmlTemplate.setTag(holder);
}else{
holder = (ViewHolder)xmlTemplate.getTag();
}
// Get image details from List<ImageBean>
ImageBean bean = beanList.get(position);
String imageUri = bean.getImageUri();
String title = bean.getImageTitle();
// Set Holder ImageView bitmap; Use parent activity's ImageHandler to load image into Holder's ImageView.
((AddActivity)context).getImageHandler().loadBitmap(imageUri, holder.imageView, Constants.LISTVIEW_XML_WIDTH, Constants.LISTVIEW_XML_HEIGHT);
// Set Holder's TextView.
holder.titleView.setText(title);
// return view
return xmlTemplate;
}
}
Solved. After putting log statements in strategic places I discovered the RetainedFragment's list was always null. After some head scratching noticed this in RetainedFragment:
fm.beginTransaction().add(fragment, TAG);
I'm missing the commit()
!
After I added that the state is being preserved now with configuration changes.
More information related to saving ListFragment state that I discovered during my trials and tribulations:
If you add a fragment via:
if(savedInstanceState == null){
listFrag = new ListViewFragment();
// programmatically add fragment to ViewGroup
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(R.id.add_fragFrame, listFrag, LVF_TAG);
}
Then either of these will work in the else
:
1) This one works because Android takes care of saving the Fragment:
listFrag = (ListViewFragment)getFragmentManager().findFragmentByTag(LVF_TAG);
2) This one works because the fragment was specifically saved into bundle in
onSaveInstanceState:
listFrag = (ListViewFragment)getFragmentManager().getFragment(savedInstanceState, LVF_TAG);
For number 2 to work, this happens in onSaveInstanceState():
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
getFragmentManager().putFragment(outState, LVF_TAG, listFrag);
}