jrhee17
jrhee17

Reputation: 1152

Relationship between Cursor, CursorAdapter, LoaderManager, Loader

So I've been working with IOS development for the past couple of months and have used Core Data and NSFetchedResultsController extensively.

Core Data + NSFetchedResultsController : Detects changes to the database automatically and updates table elements accordingly.

Now I've switched over to Android development and I've been looking for an equivalent of the above. I've looked at various different classes available and am a little confused.

The different types of classes:

  1. Cursor: Provides access to the result of a database query.
  2. CursorAdapter : Links the (list, recycler) view with the a cursor and displays the objects.
  3. ContentProvider: Provides access to database objects. Required for use of LoaderManager.
  4. LoaderManager: Implemented by an Activity or a Fragment to load data without blocking UI
  5. Loader: Loader which generates cursor objects when content has changed.

I think it's also worth mentioning I am using greendao, and I am using its generated ContentProvider.

Flow of updating

This is where I got a little iffy. This is my assumption.

  1. Content Provider maintains a cursor -- which is used by LoaderManager and possibly also CursorAdapter.
  2. When a change occurs from the database, the Loader has a ForceLoadContentObserver which observes the cursor and calls onLoadFinished when its content is changed.
  3. Normally a cursor adapter would call swap() inside the onLoadFinished(), resulting in a update of the table.

Keeping the above in mind, I created a ContentProvider and implemented LoaderManager but the function onLoadFinished() isn't being called when I persist a new object (using greenDao) -- which led me to start questioning whether I am understanding the process correctly. Here is a snippet of code to show what I have generally coded so far.

Fragment class

public class MissionPageFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {

    // Various initialization methods

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        getLoaderManager().initLoader(0, savedInstanceState, this);
    }

    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        ContentProvider.daoSession = Main.daoSession;
        Uri uri = ContentProvider.CONTENT_URI;

        // Other initializations

        CursorLoader cursorLoader = new CursorLoader(getContext(), uri, projections, null, null, null);
        return cursorLoader;
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        System.out.println("Should be called when a new item is persisted... but not called =(");
    }

    // Other methods
}

If someone could confirm whether I am thinking about the process correctly and/or shed light on what might be going wrong, I would appreciate it a lot.

Edit 1

Here is a snippet of the query() function in the ContentProvider subclass generated by greenDao.

@Override
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {

    SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
    int uriType = sURIMatcher.match(uri);
    switch (uriType) {
        case MISSION_DIR:
            queryBuilder.setTables(TABLENAME);
            break;
        case MISSION_ID:
            queryBuilder.setTables(TABLENAME);
            queryBuilder.appendWhere(PK + "="
                    + uri.getLastPathSegment());
            break;
        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
    }

    SQLiteDatabase db = getDatabase();
    Cursor cursor = queryBuilder.query(db, projection, selection,
            selectionArgs, null, null, sortOrder);
    cursor.setNotificationUri(getContext().getContentResolver(), uri);

    return cursor;
}

Solution

I just wanted to share how I implemented all of the aforementioned classes with GreenDao. Let me know if there's a better way to do this, but I feel like this is good enough.

  1. Generate a ContentProvider subclass by adding the following code snippet to the GreenDaoGenerator class

    Entity entity = new Entity();
    entity.addContentProvider();
    
  2. Check if the ContentProvider subclass methods are called appropriately. Add the javadoc part to AndroidManifest.xml. For me, I also had to change the generated ContentProvider class BASE_PATH variable to another variable like the following.

    public static final String BASE_PATH = "MYTABLENAME";
    
  3. Add LoaderManager, Loader to the Activity/Fragment class like the one I wrote above. You will also have to set the daoSession object before the ContentProvider is used. I've moved the below snippet to another initializer class so that other Activity/Fragment classes may use it as well.

    ContentProvider.daoSession = Main.daoSession;
    
  4. I'm using a RecyclerView so I subclassed the CursorRecyclerViewAdapter class provided here https://gist.github.com/skyfishjy/443b7448f59be978bc59 with a little customization. You should be able to use a simple CursorAdapter if you are using a ListView. Since the Loader is listening for updates, the Adapter doesn't need to listen for updates.

  5. Now the ContentProvider subclass is able to update the view due to this line

    getContext().getContentResolver().notifyChange(uri, null);
    

    which is automatically generated by GreenDao. What this means is that you can use ContentProvider for all CRUD operations and the view will automatically update accordingly. This seemed to defeat the purpose of using an ORM. As such, I now do normal GreenDao operations and call the notifyChange manually after each insertion, deletion, update.

    GreenDaoObj obj = new GreenDaoObj();
    obj.insert();
    getContext.getContentResolver().notifyChange(MyContentProvider.CONTENT_URI, null);
    

I don't think there is a better way than this as I don't really want to touch the GreenDao generated code for Model and Dao objects. I suppose one could add custom CRUD functions which nests the generated CRUD methods, but that should be trivial.

I've been looking around and there wasn't a well documented way to use GreenDao with Loader/LoaderManager so I thought I would organize this here. Hopefully this helps anyone who is planning on implementing this.

Upvotes: 2

Views: 666

Answers (1)

Danail Alexiev
Danail Alexiev

Reputation: 7772

There are just a couple of things you didn't get exactly right.

  1. ContentProviders are Android components that provide access to data. That data can be stored in a relational database, flat file, a remote server, etc. Providers have a common REST-like interface and serve as an abstraction layer over the different options for data storage you have. ContentProviders can also be accessed from external applications (if configured properly) and this makes them the default way of sharing data between apps. Accessing them doesn't require a LoaderManager. They should be accessed via a ContentResolver.
  2. LoaderManager is responsible for the Loader lifecycle and coordinating it with the Activity and Fragment lifecycles.
  3. Loaders provide a lifecycle aware way to load data into activities or fragments. The CursorLoader is a particular implementation that comes with a tone of features - it uses a worker thread to keep the loading of the UI thread, it abstract using a ContentResolver to access a ContentProvider and also sets up a ContentObserver. The ContentObserver will listen for updates on the query URL and reload data if needed.

In order to get ContentObserver notifications to work, you have to make sure a couple of things are in place. First, your ContentProvider should propagate changes to the observers. This is done using getContext().getContentResolver().notifyChange(url, observer). Then, if you have a CursorLoader that executed a query on the notified URL, it will be automatically reloaded.

Upvotes: 3

Related Questions