I'm in the progress of developing an Android app for displaying a number of RSS feeds (yes, I know there are many apps like this already). The data to be displayed is backed by a content provider, and I want to be backward compatible with API level 4.
I'm using an ExpandableListView
to display the content of three different RSS feeds. The ExpandableListView
's adapter is implemented as a sub-class of SimpleCursorTreeAdapter
:
private class RssFeedLatestListAdapter extends SimpleCursorTreeAdapter {
public static final String FEED_NAME_COLUMN = "feedName";
public RssFeedLatestListAdapter(Context ctx, Cursor groupCursor, int groupLayout,
String[] groupFrom, int[] groupTo, int childLayout, String[] childFrom,
int[] childTo) {
super(ctx, groupCursor, groupLayout, groupLayout, groupFrom, groupTo, childLayout, childFrom, childTo);
}
@Override
protected Cursor getChildrenCursor(final Cursor groupCursor) {
final String feedName = groupCursor.getString(groupCursor.getColumnIndex(FEED_NAME_COLUMN));
return managedQuery(LATEST_URI, null,
FeedItemColumns.CATEGORY + " = ? AND " + FeedItemColumns.FEED + " = ?",
new String[] { mCategory.getId(), feedName }, null);
}
@Override
protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
super.bindGroupView(view, context, cursor, isExpanded);
// Bind group view (impl details not important) ...
}
@Override
protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
super.bindChildView(view, context, cursor, isLastChild);
// Bind child view (impl details not important) ...
}
}
With this setup, all content is loaded as expected. However, the UI hangs/stutters randomly during the loading of the list content. Usually not enough to get an ANR, but still noticeable and very annoying!
In order to debug this issue, I enabled android.os.StrictMode (great new feature/tool btw!), and started up the emulator (on Android 2.3). When the list content is loaded, I get the following StrictMode output:
D/StrictMode( 395): StrictMode policy violation; ~duration=1522 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=23 violation=2 D/StrictMode( 395): at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:745) D/StrictMode( 395): at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:1345) D/StrictMode( 395): at android.database.sqlite.SQLiteQueryBuilder.query(SQLiteQueryBuilder.java:330) D/StrictMode( 395): at com.mycompany.myapp.provider.RSSFeedProvider.query(RSSFeedProvider.java:128) D/StrictMode( 395): at android.content.ContentProvider$Transport.query(ContentProvider.java:187) D/StrictMode( 395): at android.content.ContentResolver.query(ContentResolver.java:262) D/StrictMode( 395): at android.app.Activity.managedQuery(Activity.java:1550) D/StrictMode( 395): at com.mycompany.myapp.activity.MultipleFeedsActivity$RssFeedLatestListAdapter.getChildrenCursor(MultipleFeedsActivity.java:388) D/StrictMode( 395): at android.widget.CursorTreeAdapter.getChildrenCursorHelper(CursorTreeAdapter.java:106) D/StrictMode( 395): at android.widget.CursorTreeAdapter.getChildrenCount(CursorTreeAdapter.java:178) D/StrictMode( 395): at android.widget.ExpandableListConnector.refreshExpGroupMetadataList(ExpandableListConnector.java:561) D/StrictMode( 395): at android.widget.ExpandableListConnector.expandGroup(ExpandableListConnector.java:682) D/StrictMode( 395): at android.widget.ExpandableListConnector.expandGroup(ExpandableListConnector.java:636) D/StrictMode( 395): at android.widget.ExpandableListView.expandGroup(ExpandableListView.java:608) D/StrictMode( 395): at com.mycompany.myapp.activity.MultipleFeedsActivity.onReceiveResult(MultipleFeedsActivity.java:335) D/StrictMode( 395): at com.mycompany.myapp.service.FeedResultReceiver.onReceiveResult(FeedResultReceiver.java:40) D/StrictMode( 395): at android.os.ResultReceiver$MyRunnable.run(ResultReceiver.java:43) D/StrictMode( 395): at android.os.Handler.handleCallback(Handler.java:587) D/StrictMode( 395): at android.os.Handler.dispatchMessage(Handler.java:92) D/StrictMode( 395): at android.os.Looper.loop(Looper.java:123) D/StrictMode( 395): at android.app.ActivityThread.main(ActivityThread.java:3647) D/StrictMode( 395): at java.lang.reflect.Method.invokeNative(Native Method) D/StrictMode( 395): at java.lang.reflect.Method.invoke(Method.java:507) D/StrictMode( 395): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839) D/StrictMode( 395): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597) D/StrictMode( 395): at dalvik.system.NativeStart.main(Native Method)
This seems reasonable! In SimpleCursorTreeAdapter.getChildrenCursor()
, the content provider is queried on the UI thread, which of course is a bad idea that will most definitely hang the UI!
I had a look at the JavaDoc (API level 4) of CursorTreeAdapter
(a super-class of SimpleCursorTreeAdapter
) and for getChildrenCursor()
it says:
"If you want to asynchronously query a provider to prevent blocking the UI, it is possible to return null and at a later time call setChildrenCursor(int, Cursor).".
Great! Let's do that! I change my implementation of getChildrenCursor()
to start a new task responsible for performing the query and setting the children cursor on the adapter. After having started the task, I just return null in getChildrenCursor()
:
@Override
protected Cursor getChildrenCursor(final Cursor groupCursor) {
final String feedName = groupCursor.getString(groupCursor.getColumnIndex(FEED_NAME_COLUMN));
new RefreshChildrenCursorTask(groupCursor.getPosition()).execute(feedName);
return null;
}
, where the RefreshChildrenCursorTask
is implemented as:
private class RefreshChildrenCursorTask extends AsyncTask<String, Void, Cursor> {
private int mGroupPosition;
public RefreshChildrenCursorTask(int groupPosition) {
this.mGroupPosition = groupPosition;
}
@Override
protected Cursor doInBackground(String... params) {
String feedName = params[0];
return managedQuery(LATEST_URI, null,
FeedItemColumns.CATEGORY + " = ? AND " + FeedItemColumns.FEED + " = ?",
new String[] { mCategory.getId(), feedName }, null);
}
@Override
protected void onPostExecute(Cursor childrenCursor) {
mLatestListAdapter.setChildrenCursor(mGroupPosition, childrenCursor);
}
}
}
I reinstalled the app on the 2.3 emulator and started it up. It worked like a charm, bye bye non-responsive UI! But the joy didn't last long ... The next thing to do was to run the same code on the target device (in this case a Samsung Galaxy S running Android 2.2). I then got the following Exception
during the loading of the list feed content:
E/AndroidRuntime(23191): FATAL EXCEPTION: main E/AndroidRuntime(23191): java.lang.NullPointerException E/AndroidRuntime(23191): at android.widget.SimpleCursorTreeAdapter.initFromColumns(SimpleCursorTreeAdapter.java:194) E/AndroidRuntime(23191): at android.widget.SimpleCursorTreeAdapter.initChildrenFromColumns(SimpleCursorTreeAdapter.java:205) E/AndroidRuntime(23191): at android.widget.SimpleCursorTreeAdapter.init(SimpleCursorTreeAdapter.java:186) E/AndroidRuntime(23191): at android.widget.SimpleCursorTreeAdapter.(SimpleCursorTreeAdapter.java:136) E/AndroidRuntime(23191): at com.mycompany.myapp.activity.MultipleFeedsActivity$RssFeedLatestListAdapter.(MultipleFeedsActivity.java:378) E/AndroidRuntime(23191): at com.mycompany.myapp.activity.MultipleFeedsActivity$2.run(MultipleFeedsActivity.java:150) E/AndroidRuntime(23191): at android.os.Handler.handleCallback(Handler.java:587) E/AndroidRuntime(23191): at android.os.Handler.dispatchMessage(Handler.java:92) E/AndroidRuntime(23191): at android.os.Looper.loop(Looper.java:123) E/AndroidRuntime(23191): at android.app.ActivityThread.main(ActivityThread.java:4627) E/AndroidRuntime(23191): at java.lang.reflect.Method.invokeNative(Native Method) E/AndroidRuntime(23191): at java.lang.reflect.Method.invoke(Method.java:521) E/AndroidRuntime(23191): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:871) E/AndroidRuntime(23191): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629) E/AndroidRuntime(23191): at dalvik.system.NativeStart.main(Native Method)
A quick look at the source code of SimpleCursorTreeAdapter
tells me that there is no way that it can cope with null being returned asynchronously from getChildrenCursor()
. They seem to have solved this problem in Android 2.3, but how are you supposed to get around it in earlier versions of Android? Do I really have to write my own implementation of CursorTreeAdapter
to be able to refresh the children cursor asynchronously?
Has anybody had the same problem and perhaps even found a (feasible) workaround? If not, could anybody give me a hint on how to proceed!? I would really appreciate any help I can get!
Thanks in advance!
Regards, Jacob