javaandroidnullpointerexceptionexpandablelistviewexpandablelistadapter

ExpandableListAdapter getChildrenCount null pointer exception when activity restored


this exception is driving me nuts. When ever my main activity's process gets killed and then restored my app force closes due to a null pointer exception in my expandableListView. This is my first android app and first time writing java so I am a bit of a n00b. It is a timetable app and it is using a Cursor Loader to load in the data to the list from a content provider. What am i doing wrong here? Thanks in advance.

Logcat:

12-03 00:21:42.235: E/AndroidRuntime(2341): FATAL EXCEPTION: main
12-03 00:21:42.235: E/AndroidRuntime(2341): Process: com.crevitus.timetable, PID: 2341
12-03 00:21:42.235: E/AndroidRuntime(2341): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.crevitus.timetable/com.crevitus.timetable.MainActivity}: java.lang.NullPointerException: Attempt to invoke interface method 'int java.util.List.size()' on a null object reference
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2298)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2360)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.app.ActivityThread.access$800(ActivityThread.java:144)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1278)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.os.Handler.dispatchMessage(Handler.java:102)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.os.Looper.loop(Looper.java:135)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.app.ActivityThread.main(ActivityThread.java:5221)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at java.lang.reflect.Method.invoke(Native Method)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at java.lang.reflect.Method.invoke(Method.java:372)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
12-03 00:21:42.235: E/AndroidRuntime(2341): Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'int java.util.List.size()' on a null object reference
12-03 00:21:42.235: E/AndroidRuntime(2341):     at com.crevitus.timetable.adapter.ExpandableListAdapter.getChildrenCount(ExpandableListAdapter.java:67)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.widget.ExpandableListConnector.refreshExpGroupMetadataList(ExpandableListConnector.java:563)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.widget.ExpandableListConnector.setExpandedGroupMetadataList(ExpandableListConnector.java:758)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.widget.ExpandableListView.onRestoreInstanceState(ExpandableListView.java:1340)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.view.View.dispatchRestoreInstanceState(View.java:13621)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.view.ViewGroup.dispatchThawSelfOnly(ViewGroup.java:2907)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.widget.AdapterView.dispatchRestoreInstanceState(AdapterView.java:795)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:2893)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:2893)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.view.View.restoreHierarchyState(View.java:13599)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at com.android.internal.policy.impl.PhoneWindow.restoreHierarchyState(PhoneWindow.java:1980)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.app.Activity.onRestoreInstanceState(Activity.java:1022)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.app.Activity.performRestoreInstanceState(Activity.java:977)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.app.Instrumentation.callActivityOnRestoreInstanceState(Instrumentation.java:1161)
12-03 00:21:42.235: E/AndroidRuntime(2341):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2271)
12-03 00:21:42.235: E/AndroidRuntime(2341):     ... 10 more

MainActivity Code:

package com.crevitus.timetable;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;

import android.app.AlertDialog;
import android.app.LoaderManager;
import android.content.ContentResolver;
import android.content.CursorLoader;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.Loader;
import android.database.Cursor;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ExpandableListView.OnGroupExpandListener;
import android.widget.Toast;

import com.crevitus.timetable.adapter.ExpandableListAdapter;
import com.crevitus.timetable.provider.TimetableContentProvider;
import com.crevitus.timetable.service.ReminderHandler;

public class MainActivity extends BaseActivity implements
    LoaderManager.LoaderCallbacks<Cursor> {

    private ExpandableListAdapter listAdapter;
    private ExpandableListView expListView;
    private List<String> listDataHeader;
    private HashMap<String, List<List<String>>> listDataChild;
    private int lastExpandedPosition = -1;
    private int gPosition, cPosition;
    private static final int MONDAY = 0, TUESDAY = 1, WEDNESDAY = 2, THURSDAY = 3, FRIDAY = 4;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        PreferenceManager.setDefaultValues(this, R.xml.preferences, false);

        //get data from content provider
        getLoaderManager().initLoader(MONDAY, null, this);
        getLoaderManager().initLoader(TUESDAY, null, this);
        getLoaderManager().initLoader(WEDNESDAY, null, this);
        getLoaderManager().initLoader(THURSDAY, null, this);
        getLoaderManager().initLoader(FRIDAY, null, this);

        // get the listview
        expListView = (ExpandableListView) findViewById(R.id.lvExp);
        expListView.setLongClickable(true);

        //initialise data collections
        listDataHeader = new ArrayList<String>();
        listDataChild = new HashMap<String, List<List<String>>>();

        // Add header data
        listDataHeader.add("Monday");
        listDataHeader.add("Tuesday");
        listDataHeader.add("Wednesday");
        listDataHeader.add("Thursday");
        listDataHeader.add("Friday");

        //get adapter
        listAdapter = new ExpandableListAdapter(this, listDataHeader, listDataChild);   

        // set list adapter
        expListView.setAdapter(listAdapter);

        // Listview on child click listener
        expListView.setOnChildClickListener(new OnChildClickListener() {

            @Override
            public boolean onChildClick(ExpandableListView parent, View v,
                    int groupPosition, int childPosition, long id) {
                //get data by position
                String group = (String) listAdapter.getGroup(groupPosition);
                List<String> childList = listDataChild.get(group).get(childPosition);

                //launch view activity and pass data
                Intent view = new Intent(getApplicationContext(), ViewActivity.class);
                Bundle bundle = new Bundle();
                bundle.putStringArray("Class", new String[] {childList.get(0), childList.get(1), childList.get(2), childList.get(3), childList.get(4), childList.get(5)});
                view.putExtras(bundle);
                startActivity(view);
                return false;
            }
        });

        expListView.setOnItemLongClickListener(new OnItemLongClickListener() {

            @Override
            public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                if (ExpandableListView.getPackedPositionType(id) == ExpandableListView.PACKED_POSITION_TYPE_CHILD) 
                 {
                    int groupPosition = ExpandableListView.getPackedPositionGroup(id);
                    int childPosition = ExpandableListView.getPackedPositionChild(id);
                    longClick( expListView, groupPosition, childPosition);

                    return true;
                }
                return false;
            }

            private void longClick(ExpandableListView expListView, int groupPosition, int childPosition) {
                gPosition = groupPosition;
                cPosition = childPosition;
                AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(MainActivity.this);
                // set title
                alertDialogBuilder.setTitle("Delete Class?");

                // set dialog message
                alertDialogBuilder
                    .setMessage("Are you sure you want to delete the class?")
                    .setCancelable(false)
                    .setPositiveButton("Yes",new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog,int id) {
                            String group = (String) listAdapter.getGroup(gPosition);
                            List<String> childList = listDataChild.get(group).get(cPosition);
                            ContentResolver cr = getContentResolver();
                            cr.delete(TimetableContentProvider.CONTENT_URI, TimetableContentProvider.KEY_ID + " = ?", new String[] {childList.get(0)});
                            listAdapter.deleteChild(gPosition, cPosition);
                            listAdapter.notifyDataSetChanged();
                            ReminderHandler.cancelAlarm(Integer.parseInt(childList.get(0)), getApplicationContext());
                            Toast.makeText(getApplicationContext(), "Class Deleted", Toast.LENGTH_SHORT).show();
                        }
                      })
                    .setNegativeButton("No",new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog,int id) {
                            // if this button is clicked, just close
                            // the dialog box and do nothing
                            dialog.cancel();
                        }
                    });

                    // create alert dialog
                    AlertDialog alertDialog = alertDialogBuilder.create();

                    // show it
                    alertDialog.show();
                    }
                });//end longclick listener


        expListView.setOnGroupExpandListener(new OnGroupExpandListener() {
            @Override
            public void onGroupExpand(int groupPosition) {
                    if (lastExpandedPosition != -1 && groupPosition != lastExpandedPosition) 
                    {
                        expListView.collapseGroup(lastExpandedPosition);
                    }
                    lastExpandedPosition = groupPosition;
            }
        });

    }
    @Override
    public void onResume()
    {
        super.onResume();
        getLoaderManager().restartLoader(MONDAY, null, this);
        getLoaderManager().restartLoader(TUESDAY, null, this);
        getLoaderManager().restartLoader(WEDNESDAY, null, this);
        getLoaderManager().restartLoader(THURSDAY, null, this);
        getLoaderManager().restartLoader(FRIDAY, null, this);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        CursorLoader loader = null;
        switch(id)
        {
            case MONDAY:
                loader = new CursorLoader(this,
                        TimetableContentProvider.CONTENT_URI,
                        null,
                        TimetableContentProvider.KEY_DAY_COLUMN + "=?", new String[] {"Monday"},
                        null);
                break;
            case TUESDAY:
                loader = new CursorLoader(this,
                        TimetableContentProvider.CONTENT_URI,
                        null,
                        TimetableContentProvider.KEY_DAY_COLUMN + "=?", new String[] {"Tuesday"},
                        null);              
                break;
            case WEDNESDAY:
                loader = new CursorLoader(this,
                        TimetableContentProvider.CONTENT_URI,
                        null,
                        TimetableContentProvider.KEY_DAY_COLUMN + "=?", new String[] {"Wednesday"},
                        null);              
                break;
            case THURSDAY:
                loader = new CursorLoader(this,
                        TimetableContentProvider.CONTENT_URI,
                        null,
                        TimetableContentProvider.KEY_DAY_COLUMN + "=?", new String[] {"Thursday"},
                        null);              
                break;
            case FRIDAY:
                loader = new CursorLoader(this,
                        TimetableContentProvider.CONTENT_URI,
                        null,
                        TimetableContentProvider.KEY_DAY_COLUMN + "=?", new String[] {"Friday"},
                        null);              
                break;  
        }
        return loader;
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {

        List<String> splitMon;
        List<String> splitTues;
        List<String> splitWed;
        List<String> splitThurs;
        List<String> splitFri;

        List<List<String>> monday = new ArrayList<List<String>>();
        List<List<String>> tuesday = new ArrayList<List<String>>();
        List<List<String>> wednesday = new ArrayList<List<String>>();
        List<List<String>> thursday = new ArrayList<List<String>>();
        List<List<String>> friday = new ArrayList<List<String>>();

        switch(loader.getId())
        {
            case MONDAY:
                while(cursor.moveToNext())
                {
                    splitMon = new ArrayList<String>();
                    splitMon.add(cursor.getString(0));
                    splitMon.add(cursor.getString(1));
                    splitMon.add(cursor.getString(2));
                    splitMon.add(cursor.getString(3));
                    splitMon.add(cursor.getString(4));
                    splitMon.add(cursor.getString(5));
                    monday.add(splitMon);
                }
                listDataChild.put(listDataHeader.get(0), monday);
                break;
            case TUESDAY:
                while(cursor.moveToNext())
                {
                    splitTues = new ArrayList<String>();
                    splitTues.add(cursor.getString(0));
                    splitTues.add(cursor.getString(1));
                    splitTues.add(cursor.getString(2));
                    splitTues.add(cursor.getString(3));
                    splitTues.add(cursor.getString(4));
                    splitTues.add(cursor.getString(5));
                    tuesday.add(splitTues);
                }
                listDataChild.put(listDataHeader.get(1), tuesday);
                break;
            case WEDNESDAY:
                 while(cursor.moveToNext())
                    {
                        splitWed = new ArrayList<String>();
                        splitWed.add(cursor.getString(0));
                        splitWed.add(cursor.getString(1));
                        splitWed.add(cursor.getString(2));
                        splitWed.add(cursor.getString(3));
                        splitWed.add(cursor.getString(4));
                        splitWed.add(cursor.getString(5));
                        wednesday.add(splitWed);
                    }
                 listDataChild.put(listDataHeader.get(2), wednesday);
                break;
            case THURSDAY:
                while(cursor.moveToNext())
                {
                    splitThurs = new ArrayList<String>();
                    splitThurs.add(cursor.getString(0));
                    splitThurs.add(cursor.getString(1));
                    splitThurs.add(cursor.getString(2));
                    splitThurs.add(cursor.getString(3));
                    splitThurs.add(cursor.getString(4));
                    splitThurs.add(cursor.getString(5));
                    thursday.add(splitThurs);
                }
                listDataChild.put(listDataHeader.get(3), thursday); 
                break;
            case FRIDAY:
                 while(cursor.moveToNext())
                    {
                        splitFri = new ArrayList<String>();
                        splitFri.add(cursor.getString(0));
                        splitFri.add(cursor.getString(1));
                        splitFri.add(cursor.getString(2));
                        splitFri.add(cursor.getString(3));
                        splitFri.add(cursor.getString(4));
                        splitFri.add(cursor.getString(5));
                        friday.add(splitFri);

                    }
                    listDataChild.put(listDataHeader.get(4), friday);
                break;
        }
        listAdapter.notifyDataSetChanged();
        expListView.post( new Runnable() {
            @Override
            public void run() {
                 switch(Calendar.getInstance().get(Calendar.DAY_OF_WEEK)){
                case Calendar.MONDAY:
                    expListView.expandGroup(0);
                    break;
                case Calendar.TUESDAY:
                    expListView.expandGroup(1);
                    break;
                case Calendar.WEDNESDAY:
                    expListView.expandGroup(2);
                    break;
                case Calendar.THURSDAY:
                    expListView.expandGroup(3);
                    break;
                case Calendar.FRIDAY:
                    expListView.expandGroup(4);
                    break;
                case Calendar.SATURDAY:
                    expListView.expandGroup(0);
                    break;
                case Calendar.SUNDAY:
                    expListView.expandGroup(0);
                    break;
              }
            }
          });


    }
    @Override
    public void onLoaderReset(Loader<Cursor> arg0) {
        // TODO Auto-generated method stub

    }  
}

ExpandableListAdapter:

package com.crevitus.timetable.adapter;

import java.util.HashMap;
import java.util.List;

import android.content.Context;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;

import com.crevitus.timetable.R;

public class ExpandableListAdapter extends BaseExpandableListAdapter {

    private Context _context;
    private List<String> listGroupData;
    private HashMap<String, List<List<String>>> listItemData;

    public ExpandableListAdapter(Context context, List<String> listGroupData,
            HashMap<String, List<List<String>>> listDataChild) {
        this._context = context;
        this.listGroupData = listGroupData;
        this.listItemData = listDataChild;
    }

    @Override
    public Object getChild(int groupPosition, int childPosition) {
        return listItemData
                .get(listGroupData
                        .get(groupPosition))
                        .get(childPosition);
    }

    @Override
    public long getChildId(int groupPosition, int childPosition) {
        return childPosition;
    }

    @Override
    public View getChildView(int groupPosition, final int childPosition,
            boolean isLastChild, View convertView, ViewGroup parent) {

        List<String> childText = (List<String>) getChild(groupPosition, childPosition);

        if (convertView == null) {
            LayoutInflater infalInflater = (LayoutInflater) this._context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = infalInflater.inflate(R.layout.exp_list_item, null);
        }

        TextView sTime = (TextView) convertView.findViewById(R.id.sTime);
        TextView modName = (TextView) convertView.findViewById(R.id.modName);

        if(childText != null)
        {
            modName.setText(childText.get(1) + " - Room: " + childText.get(2));
            sTime.setText(childText.get(4));
        }
        return convertView;
    }

    @Override
    public int getChildrenCount(int groupPosition) {
        return this.listItemData.get(this.listGroupData.get(groupPosition))
                .size();
    }

    @Override
    public Object getGroup(int groupPosition) {
        return this.listGroupData.get(groupPosition);
    }

    @Override
    public int getGroupCount() {
        return this.listGroupData.size();
    }

    @Override
    public long getGroupId(int groupPosition) {
        return groupPosition;
    }

    @Override
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
        String headerTitle = (String) getGroup(groupPosition);
        if (convertView == null) {
            LayoutInflater infalInflater = (LayoutInflater) this._context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = infalInflater.inflate(R.layout.list_group, null);
        }

        TextView lblListHeader = (TextView) convertView.findViewById(R.id.listGroup);
        lblListHeader.setTypeface(null, Typeface.BOLD);
        lblListHeader.setText(headerTitle);

        return convertView;
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }

    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return true;
    }

    public void deleteChild(int groupPosition, int childPosition)
    {
        listItemData.get(listGroupData.get(groupPosition)).remove(childPosition);
    }
}

Solution

  • So I'm not too familiar with the LoaderManager but I'd say the onResume() method which restarts the loaders is fishy. I'm thinking you can just remove that completely. I highly suggest reading this SO answer which discusses the difference between initLoader and restartLoader and when to use them when an Activity gets recreated. A small excerpt from there:

    The Activity/Fragment life cycle has nothing to do with the decision to use one or the other method...This decision is made solely based on the "need" for a new loader. If we want to run the same query we use initLoader, if we want to run a different query we use restartLoader. We could always use restartLoader but that would be inefficient. After a screen rotation or if the user navigates away from the app and returns later to the same Activity we usually want to show the same query result and so the restartLoader would unnecessarily re-create the loader and dismiss the underlying (potentially expensive) query result.

    This may add to why you are crashing as it looks like a timing issue really. You base your group count on listGroupData which is populated in onCreate(). However you retrieve your child count from listItemData which is not loaded until the cursor finishes. That means between onCreate() and the onLoadFinished() your listItemData will not contain any data but your adapter thinks there's items available and hence crash with an NPE.

    When writing your adapter its absolutely critical that listGroupData reflects listItemData. Modifying one means you need to modify the other. Also if you guarantee the List value in the map is never null, you won't need to worry about doing null checks all over the place. Example, In onCreate do this instead:

        listDataChild = new HashMap<String, List<List<String>>>();
        listDataChild.put("Monday", new ArrayList<List<String>>());
        listDataChild.put("Tuesday", new ArrayList<List<String>>());
        listDataChild.put("Wednesday", new ArrayList<List<String>>());
        listDataChild.put("Thursday", new ArrayList<List<String>>());
        listDataChild.put("Friday", new ArrayList<List<String>>());
    
        listDataHeader = new ArrayList<String>(listDataChild.keySet());
    

    This ensures every item in listDataHeader is found in listDataChild and that every value in listDataChild will return an empty list until the loader finishes it and actually populates with data.