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)
Some suggestions:
Use separate directory entry lists to store the data backing the two ListView
s.
List<HashMap<String,String>> directoryEntriesLocal = new ArrayList<>();
List<HashMap<String,String>> directoryEntriesRemote = new ArrayList<>();
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.
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.Handler
and Thread
classes. Use AsyncTask instead. It will make your life as an Android Programmer much easier.