问题
I am using an Activity that extends SherlockFragmentActivity, that has 3 tabs. The 3 tabs are ListFragments that implement LoaderManager.LoaderCallbacks. The OnCreate method for the Activity loads the tabs like so
bar.addTab(bar.newTab()
.setTag("venues_list")
.setText(getString(R.string.list_venues_header))
.setTabListener(new TabListener<VenueListFragment>(
this, getString(R.string.list_invites_header), VenueListFragment.class, null)));
// I do the EXACT same thing for the other two tabs, using their respective ListFragments
if (savedInstanceState != null) {
bar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
}
The layout that I'm loading the tabs into is very simple:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
</LinearLayout>
Each of the tabs have a class that is identical to this, just called something different:
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.support.v4.widget.SimpleCursorAdapter;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockListFragment;
import com.lateral.oursvp.R;
import com.lateral.oursvp.database.SimpleCursorLoader;
import com.lateral.oursvp.database.VenuesDataSource;
/**
* @author rabbott
*
*/
public class VenueListFragment extends SherlockListFragment implements LoaderManager.LoaderCallbacks<Cursor> {
SharedPreferences appPreferences;
private CursorAdapter cursorAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// bind the columns of the cursor to the list
String[] from = new String[] { VenuesDataSource.KEY_NAME, VenuesDataSource.KEY_DESCRIPTION };
int[] to = new int[] { R.id.list_item_title, R.id.list_item_subtitle };
cursorAdapter = new SimpleCursorAdapter( getActivity(), R.layout.list_item, null, from, to, 0);
// retrieve the listview to populate
ListView lv = (ListView) getActivity().findViewById(android.R.id.list);
// set the adapter on the listview
lv.setAdapter(cursorAdapter);
// click event for each row of the list
lv.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> arg0, View view,
int position, long id) {
Cursor cursor = cursorAdapter.getCursor();
cursor.moveToPosition(position);
Toast.makeText(getActivity(), "Tapped row " + position + "!", Toast.LENGTH_SHORT).show();
}
});
// Start out with a progress indicator.
setListShown(false);
// load the data
getActivity().getSupportLoaderManager().initLoader(0, null, this);
}
public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) {
return new VenueCursorLoader(getActivity());
}
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
cursorAdapter.swapCursor(cursor);
// the list should now be shown
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
public void onLoaderReset(Loader<Cursor> loader) {
cursorAdapter.swapCursor(null);
}
public static final class VenueCursorLoader extends SimpleCursorLoader {
Context mContext;
public VenueCursorLoader(Context context) {
super(context);
mContext = context;
}
@Override
public Cursor loadInBackground() {
VenuesDataSource datasource = new VenuesDataSource(mContext);
return datasource.getAllVenues(((EventActivity) mContext).getEventId());
}
}
}
which uses the SimpleCursorLoaded defined here:
import android.content.Context;
import android.database.Cursor;
import android.support.v4.content.AsyncTaskLoader;
public abstract class SimpleCursorLoader extends AsyncTaskLoader<Cursor> {
private Cursor mCursor;
public SimpleCursorLoader(Context context) {
super(context);
}
/* Runs on a worker thread */
@Override
public abstract Cursor loadInBackground();
/* Runs on the UI thread */
@Override
public void deliverResult(Cursor cursor) {
if (isReset()) {
// An async query came in while the loader is stopped
if (cursor != null) {
cursor.close();
}
return;
}
Cursor oldCursor = mCursor;
mCursor = cursor;
if (isStarted()) {
super.deliverResult(cursor);
}
if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
oldCursor.close();
}
}
/**
* Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
* will be called on the UI thread. If a previous load has been completed and is still valid
* the result may be passed to the callbacks immediately.
* <p/>
* Must be called from the UI thread
*/
@Override
protected void onStartLoading() {
if (mCursor != null) {
deliverResult(mCursor);
}
if (takeContentChanged() || mCursor == null) {
forceLoad();
}
}
/**
* Must be called from the UI thread
*/
@Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
@Override
public void onCanceled(Cursor cursor) {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
if (mCursor != null && !mCursor.isClosed()) {
mCursor.close();
}
mCursor = null;
}
}
The issue I'm having is that when I select say.. the second tab (third tab does the same) it attempts to load the cursor, but when it tried to populate the list_items, I get an error saying
E/AndroidRuntime(2055): java.lang.IllegalArgumentException: column 'GIVEN_VARIABLE' does not exist
After setting a few break points I have discovered that the cursor its attempting to use is the cursor from the first tab, which loaded fine on inflation, but apparently isn't getting closed, and is apparently trying to be used again even though I (think) am sending it a different curser from getAllVenues()
EDIT: This is the last place the stack trace sends me When I set some breakpoints I can see that the cursor being provided here, is the cursor from the first tab, not that of the Venue tab..
EDIT: VenuesDataSource code to show the getAllVenues method
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.util.Log;
/**
* @author rabbott
*
*/
public class VenuesDataSource extends appSQLiteHelper {
public static final String TABLE_NAME = "venues";
// venue columns
public static final String KEY_NAME = "name";
public static final String KEY_DESCRIPTION = "description";
public static final String KEY_START_TIME = "start_time";
public static final String KEY_ADDRESS = "address";
public static final String KEY_CITY = "city";
public static final String KEY_STATE = "state";
public static final String KEY_ZIP = "postal_code";
public static final String KEY_LNG = "lng";
public static final String KEY_LAT = "lat";
public static final String KEY_PHONE = "phone";
public static String COLUMNS_VENUES[] = {
DatabaseConstants.KEY_ROWID,
DatabaseConstants.KEY_EVENT_ID,
KEY_NAME,
KEY_DESCRIPTION,
KEY_START_TIME,
KEY_ADDRESS,
KEY_CITY,
KEY_STATE,
KEY_ZIP,
KEY_LNG,
KEY_LAT,
KEY_PHONE,
DatabaseConstants.KEY_CREATED_AT
};
public static final String CREATE_STATEMENT = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "event_id INTEGER NOT NULL,"
+ "name TEXT NOT NULL,"
+ "description TEXT,"
+ "start_time TEXT,"
+ "address TEXT NOT NULL,"
+ "city TEXT NOT NULL,"
+ "state TEXT NOT NULL,"
+ "postal_code TEXT,"
+ "lat TEXT,"
+ "lng TEXT,"
+ "phone TEXT,"
+ "created_at TEXT);";
public VenuesDataSource(Context context) {
super(context);
Log.i("VenueDataSource", "Constructor");
}
// create a new contact locally
public long createVenue(Integer venue_id, Integer event_id, String name, String description, String start_time, String address, String city, String state, String postal_code, String lat, String lng, String phone, String created_at) {
ContentValues initialValues = new ContentValues();
initialValues.put(DatabaseConstants.KEY_ROWID, event_id);
initialValues.put(DatabaseConstants.KEY_EVENT_ID, event_id);
initialValues.put(KEY_NAME, name);
initialValues.put(KEY_DESCRIPTION, description);
initialValues.put(KEY_START_TIME, start_time);
initialValues.put(KEY_ADDRESS, address);
initialValues.put(KEY_CITY, city);
initialValues.put(KEY_STATE, state);
initialValues.put(KEY_ZIP, postal_code);
initialValues.put(KEY_PHONE, phone);
initialValues.put(KEY_LAT, lat);
initialValues.put(KEY_LNG, lng);
initialValues.put(DatabaseConstants.KEY_CREATED_AT, created_at);
return getWritableDatabase().insert(TABLE_NAME, null, initialValues);
}
// retrieve a venue from the local database
public Cursor getVenue(long rowId) throws SQLException {
Cursor mCursor = getWritableDatabase().query(true, TABLE_NAME, COLUMNS_VENUES, DatabaseConstants.KEY_ROWID + "=" + rowId, null, null, null, null, null);
if (mCursor != null) {
mCursor.moveToFirst();
}
return mCursor;
}
// update a local venue
public long updateVenue(Integer venue_id, Integer event_id, String name, String description, String start_time, String address, String city, String state, String postal_code, String lat, String lng, String phone) {
ContentValues initialValues = new ContentValues();
initialValues.put(DatabaseConstants.KEY_EVENT_ID, event_id);
initialValues.put(KEY_NAME, name);
initialValues.put(KEY_DESCRIPTION, description);
initialValues.put(KEY_START_TIME, start_time);
initialValues.put(KEY_ADDRESS, address);
initialValues.put(KEY_CITY, city);
initialValues.put(KEY_STATE, state);
initialValues.put(KEY_ZIP, postal_code);
initialValues.put(KEY_PHONE, phone);
initialValues.put(KEY_LAT, lat);
initialValues.put(KEY_LNG, lng);
return getWritableDatabase().update(TABLE_NAME, initialValues, "_id=?", new String[] { Long.toString(venue_id) });
}
// delete a local venue
public boolean deleteVenue(long rowId) {
return (getWritableDatabase().delete(TABLE_NAME, DatabaseConstants.KEY_ROWID + "=" + rowId, null) > 0);
}
// retrieve all local venues
public Cursor getAllVenues(long event_id) {
Cursor mCursor = getWritableDatabase().query(true, TABLE_NAME, COLUMNS_VENUES, DatabaseConstants.KEY_EVENT_ID + "=" + event_id,
null, null, null, null, null);
return mCursor;
}
public boolean venueExists(int venue_id) {
Cursor mCursor = getWritableDatabase().query(true, TABLE_NAME, COLUMNS_VENUES, DatabaseConstants.KEY_ROWID + "=" + venue_id,
null, null, null, null, null);
if (mCursor.getCount() == 0) {
mCursor.close();
return false;
} else {
mCursor.close();
return true;
}
}
public void parseVenue(JSONObject venueJson) throws JSONException {
boolean venue_exists = false;
int event_id, venue_id;
String venue_name, venue_description, start_time, venue_address, venue_city, venue_state, venue_postal_code, lat, lng, venue_phone, created_at;
venue_id = venueJson.getInt(DatabaseConstants.KEY_REMOTE_ID);
event_id = venueJson.getInt(DatabaseConstants.KEY_EVENT_ID);
venue_name = venueJson.getString(KEY_NAME);
venue_description = venueJson.getString(KEY_DESCRIPTION);
start_time = venueJson.getString(KEY_START_TIME);
venue_address = venueJson.getString(KEY_ADDRESS);
venue_city = venueJson.getString(KEY_CITY);
venue_state = venueJson.getString(KEY_STATE);
venue_postal_code = venueJson.getString(KEY_ZIP);
venue_phone = venueJson.getString(KEY_PHONE);
lat = venueJson.getString(KEY_LAT);
lng = venueJson.getString(KEY_LNG);
created_at = venueJson.getString(DatabaseConstants.KEY_CREATED_AT);
// check to see if this venue already exists
venue_exists = this.venueExists(venue_id);
if (venue_exists == true) {
this.updateVenue(
venue_id,
event_id,
venue_name,
venue_description,
start_time,
venue_address,
venue_city,
venue_state,
venue_postal_code,
lat,
lng,
venue_phone);
} else {
this.createVenue(
venue_id,
event_id,
venue_name,
venue_description,
start_time,
venue_address,
venue_city,
venue_state,
venue_postal_code,
lat,
lng,
venue_phone,
created_at);
}
}
public void parseVenues(JSONArray venuesArray) throws JSONException {
JSONObject venueJson;
for (int i = 0; i < venuesArray.length(); i++) {
// Iterate through each venue
venueJson = venuesArray.getJSONObject(i);
this.parseVenue(venueJson);
}
}
}
回答1:
The issue is that the first parameter of the initLoader method is a way to uniquely identify each item to be loaded, all of mine were set to the same (0) value -- changing them to a unique value fixed the issue.
回答2:
The problem is that your Fragment
s are directly manipulating the Activity
's LoaderManager
, and as a result the first Loader
with ID 0
is always being reused.
Each Activity and Fragment in your app has its own LoaderManager instance, so instead of manipulating the Activity's LoaderManager directly from the Fragment, you should have your Fragment make use of its own LoaderManager. That is, instead of
// initialize a new Loader that will be managed by the Activity's LoaderManager
getActivity().getSupportLoaderManager().initLoader(0, null, this);
you should have each individual Fragment implement the LoaderCallbacks, and then in onActivityCreated, have each one call,
// initialize a new Loader that will be managed by the Fragment's LoaderManager
getSupportLoaderManager().initLoader(0, null, this);
In the documentation on Fragments, one of the things they stress the most is that Fragments should be designed for reuse and shouldn't be specific to a particular Activity. Thus, your "hack" (i.e. supplying different Loader ids for each Fragment) is technically "bad practice" because your Fragments will only work correctly when attached to that specific Activity. You want your Fragments to work with any Activity, so you usually want to pack your Fragments with as much control over its own UI as it can.
回答3:
For me in the class which is
public abstract class AbstractListFragment extends SherlockFragment implements
LoaderCallbacks<Cursor>
the problem with not loading content was fixed by commenting one 1st line of code and adding the second one:
// getActivity().getSupportLoaderManager().initLoader(0, null, this);
getLoaderManager().initLoader(0, null, this);
来源:https://stackoverflow.com/questions/11166032/sherlockfragmentactivity-with-multiple-listfragments-and-sqlite-cursor-battles