SherlockFragmentActivity with multiple ListFragments and SQLite cursor battles

ε祈祈猫儿з 提交于 2019-12-22 09:24:16

问题


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 Fragments 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!