Search code examples
androidlistviewarraylistandroid-arrayadapternotifydatasetchanged

How to use ListView, ArrayList, ArrayAdapter and notifyDataSetChanged together with a runnable, and a message Handler in the main thread


I worked thru 6 other SO question threads plus some blog posts with the same error mesage, all to no avail.

I am loading a files directory into an ArrayList and then into a ListView using an ArrayAdapter. The code actually works - the directory displays and responds to click events - but the program then ends with this error, usually after I display the directory one, a few or several times and then run another part of the program which does a loadUrl into a WebView:

The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. Make sure your adapter calls notifyDataSetChanged() when its content changes.

None of the simple examples that i have seen use notifyDataSetChanged or a separate thread. I am wondering if i need to use these and how to use them correctly. I am confused between the ArrayAdapter and the ArrayList regarding which needs to be modified only in the main thread. The data in the ArrayList is changed only when a new directory list is requested, then it is cleared and loaded with the new data.

My questions are: do i need the notifyDataSetChanged in this case? [i am quite certain that i do not], and how do i arrange the various parts between the main and background/runnable thread?

I make the program crash by alternately running the directoryList and displayFtpHelp [uses WebView/loadUrl()] sections - but I must run the pair several to many times to get the crash.

The LogCat does not include the last statement executed in the program, so it is difficult to pinpoint the actual trigger; rather it is this:

android.widget.ListView.layoutChildren(ListView.java:1555)

The displayFtpHelp section loadUrl() into a WebView. I wonder about how this eventually triggers the error. It seems that the program is not actually operating on the ArrayList/ArrayAdapter when the error occurs. It seems like a program structure error rather than a technical or logic error.

What i am doing is, first define two ListViews and the ArrayList. One ListView is for the local device files, the other for a remote Ftp site.

static ListView filesListLocal, filesListRemote;
static ArrayList<HashMap<String,String>> directoryEntries = new ArrayList<>();

I'll show code for the Ftp directory. The code for the local directory is essentially the same except for using file.getAbsolutePath() instead of ftp.list(), and technical changes to lookForFilesAndDirs(), which is recursive for the local directory processing.

Then i read the Ftp directory into an array using a Runnable. I am omitting error checking and other details here; this is using the Ftp4j library:

Runnable r = new Runnable() {
public void run() {
    Message msg = mHandler.obtainMessage();
    try {
        FTPFile[] fileAndDirs = ftp.list();
        dirName = ftp.currentDirectory();
    } catch (Various Exceptions e) {
        result = "The operation failed\n" + e.toString();
    }
    Bundle bundle = new Bundle();
    bundle.putString("mickKey", result);
    msg.setData(bundle);
    mHandler.sendMessage(msg);
}
};
 Thread t = new Thread(r);
 t.start();

In the message handler i copy the data from the array to the ArrayList and use the Adapter to display it in the ListView. I originally had this code in the Runnable, then moved it to the message handler in the main thread when i first received the error message, but that did not help. I put in the notifyDataSetChanged calls on a guess but it also made no difference:

dirName = ftp.currentDirectory();
sa.notifyDataSetChanged();
directoryEntries.clear();
lookForFilesAndDirs(fileAndDirs);    // load ArrayList
SimpleAdapter saFtp = new SimpleAdapter(myContext, directoryEntries,
    R.layout.my_two_lines, new String[] {"path", "filename"},
    new int[] {R.id.path, R.id.filename});
filesListRemote.setAdapter(saFtp);
sa.notifyDataSetChanged();

... leaving out more details here

public static void lookForFilesAndDirs(FTPFile[] fileAndDirs) {
for (FTPFile fileOrDir : fileAndDirs) {
    String fileOrDirName = fileOrDir.getName();
    int entryType = fileOrDir.getType();
    if (entryType == 1) {               // entry is a directory
        HashMap<String,String> listEntry = new HashMap<>();
        listEntry.put("path", fileOrDirName);
        listEntry.put("filename", null);
        directoryEntries.add(listEntry);
    } else {                             // entry is a file
        HashMap<String,String> listEntry = new HashMap<>();
        listEntry.put("path", dirName);
        listEntry.put("filename", fileOrDirName);
        directoryEntries.add(listEntry);
    }
}

The LogCat:

10-14 19:47:29.403 3006-3006/com.webs.mdawdy.htmlspyii I/Mick: onMenuItemClick
10-14 19:47:29.403 3006-3006/com.webs.mdawdy.htmlspyii I/Mick: ftpPrintFilesList
10-14 19:47:30.235 3006-3006/com.webs.mdawdy.htmlspyii I/Mick: handleMessage
10-14 19:47:32.339 3006-3006/com.webs.mdawdy.htmlspyii W/EGL_genymotion: eglSurfaceAttrib not implemented
10-14 19:47:34.627 3006-3006/com.webs.mdawdy.htmlspyii I/Mick: onMenuItemClick
10-14 19:47:34.627 3006-3006/com.webs.mdawdy.htmlspyii I/Mick: displayFtpHelp Begin
10-14 19:47:34.627 3006-3006/com.webs.mdawdy.htmlspyii I/Mick: displayFtpHelp end
10-14 19:47:34.819 3006-3006/com.webs.mdawdy.htmlspyii D/AndroidRuntime: Shutting down VM
10-14 19:47:34.819 3006-3006/com.webs.mdawdy.htmlspyii W/dalvikvm: threadid=1:
    thread exiting with uncaught exception (group=0xa4d2db20)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime: FATAL EXCEPTION: main
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime: Process: com.webs.mdawdy.htmlspyii, PID: 3006
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:
    java.lang.IllegalStateException: The content of the adapter has changed but
    ListView did not receive a notification. Make sure the content of your
    adapter is not modified from a background thread, but only from the UI thread.
    Make sure your adapter calls notifyDataSetChanged() when its content changes.
    [in ListView(2131427419, class android.widget.ListView) with
    Adapter(class android.widget.SimpleAdapter)]
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at android.widget.ListView.layoutChildren(ListView.java:1555)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at android.widget.ListView.setSelectionInt(ListView.java:1980)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at android.widget.AbsListView.resurrectSelection(AbsListView.java:5376)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at android.widget.AbsListView.onWindowFocusChanged(AbsListView.java:2822)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at android.view.View.dispatchWindowFocusChanged(View.java:7900)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at android.view.ViewGroup.dispatchWindowFocusChanged(ViewGroup.java:968)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at android.view.ViewGroup.dispatchWindowFocusChanged(ViewGroup.java:972)
[6 more lines identical to the one above]
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:3133)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at android.os.Handler.dispatchMessage(Handler.java:102)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at android.os.Looper.loop(Looper.java:136)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at android.app.ActivityThread.main(ActivityThread.java:5001)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at java.lang.reflect.Method.invokeNative(Native Method)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at java.lang.reflect.Method.invoke(Method.java:515)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)
10-14 19:47:34.823 3006-3006/com.webs.mdawdy.htmlspyii E/AndroidRuntime:     at dalvik.system.NativeStart.main(Native Method)

Solution

  • Some suggestions:

    1. Use separate directory entry lists to store the data backing the two ListViews.

      • List<HashMap<String,String>> directoryEntriesLocal = new ArrayList<>();
      • List<HashMap<String,String>> directoryEntriesRemote = new ArrayList<>();
    2. Set the adapters sa and saFtp to the respective listviews in the onCreate() method of the activity. At this stage both directoryEntriesLocal and directoryEntriesRemote can be empty.

    3. In the Handler for the main thread, just alter the data of the directory entry list that needs to be updated and then call notifyDataSetChanged() of the corresponding adapter. It will be better if you declared two Handlers, one for updating the remote list and the other for the local list. Or you can do both in a single Handler. That choice is left to you. No need to instantiate and assign new adapters here.
    4. Please do away with the direct use of Handler and Thread classes. Use AsyncTask instead. It will make your life as an Android Programmer much easier.