Search code examples
androidandroid-listfragmentsavestate

How to save custom ListFragment state with orientation change


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:

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;
}
}

Solution

  • 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);
    }