Michał Klimczak
Michał Klimczak

Reputation: 13154

ContentProvider - returning multiple Cursors or custom Object

I have a working implementation of a ContentProvider loading data via CursorLoader into a listview (with custom CursorAdapter). It's a list of events. Every item has a title, place, etc. but also a set of offers which should be displayed in a LinearLayout inside every list row.

The problem is that a Cursor row can only contain flat data, not a set of other items.

My only idea is to make a joined query on database like this:

SELECT * FROM events, offers WHERE events.id=offers.event_id;

But then I'll have as much rows as there are offers (and the list should display events, so it's not good) and the list would be overpopulated. Maybe there is a possibility to tell CursorAdapter to only populate list rows with unique events.id but somehow retrieve the offers data as well?

The best solution would be to put a Cursor or custom Object containing offers inside the events Cursor. But afaik it's not possible.

Upvotes: 2

Views: 2131

Answers (3)

JRun
JRun

Reputation: 3428

I was facing the same problem. In fact, I think a lot of people are. The whole mechanism of URI - to Relational DB through contentprovider, and everything that was built around it (like the various change listeners, file and stream handling) - this is all very impressive and useful, but for very simple data models.

Once your application needs a more elaborate data model, like - a hierarchy of tables, object relational semantics - this model breaks. I've found a bunch of ORM tools for Android, but they seem too 'bleeding edge' to me (plus, for the life of me, I couldn't figure out if they have data change notification support).

ORM is very common today, I really hope the Android folks agree and add ORM capabilities to the platform.

This is what I ended up doing: A cursor of cursors, with a a leading index cursor that helps choose the correct internal curosr. It's kind of a temp solution, I just needed to move on with my code and get back to this later. Hope this helps. Of course if you use a listview, you probably need to also create a custom adapter to inflate the correct views, and do the binding.

public class MultiCursor implements Cursor {
private final String        TAG             = this.getClass().getSimpleName();

ArrayList<Cursor>           m_cursors       = new ArrayList<Cursor>();
Map<Long, CursorRowPair>    m_idToCursorRow = Collections.synchronizedMap(new HashMap<Long, CursorRowPair>());
Set<Long>                   m_idSet         = new HashSet<Long>();
Cursor                      m_idCursor;


/**
 * @precondition: _id column must exist on every type of cursor, and has to have index of 0 (be the first)
 * @param idCursor
 */
public MultiCursor(Cursor idCursor) {
    m_idCursor = idCursor;// this cursor binds the order (1,2,3) to ids

    // go over all the ids in id cursor and add to m_idSet
    initIdSet();

    // m_cursors.add(idCursor);
    // m_position = -1;

}


private void initIdSet() {
    m_idSet.clear();

    long id;
    m_idCursor.moveToPosition(-1);
    while (m_idCursor.moveToNext()) {
        id = m_idCursor.getLong(m_idCursor.getColumnIndex(ContentDescriptor.ShowViewItem.Cols.ID));
        m_idSet.add(id);
    }
    m_idCursor.moveToFirst();
}


public void addCursor(Cursor cursor) {
    // when something changes in the child cursor, notify parent on change, to notify subscribers
    // cursor.registerContentObserver(new SelfContentObserver(this)); // calls my onchange, which calls the ui
    m_cursors.add(cursor);
    updateIdToCursorMap(cursor);
}

private class CursorRowPair {
    public final Cursor cursor;
    public final int    row;


    public CursorRowPair(Cursor cursor, int row) {
        this.cursor = cursor;
        this.row = row;
    }
}


private void updateIdToCursorMap(Cursor cursor) {
    // get object_type
    // for each row in cursor, take id, row number
    // add id, <cursor,rowNum> to map
    long id;
    int row = 0;
    cursor.moveToPosition(-1);
    while (cursor.moveToNext()) {
        id = cursor.getLong(cursor.getColumnIndex(ContentDescriptor.ShowViewItem.Cols.ID));
        if (m_idSet.contains(id)) m_idToCursorRow.put(id, new CursorRowPair(cursor, row));
        row++;

    }
    cursor.moveToFirst();
}


private Cursor getInternalCursor() {
    if (getPosition() < 0 || getCount()==0) return m_idCursor; // todo throw a proper exception

    // get the id of the current row
    long id = m_idCursor.getLong(m_idCursor.getColumnIndex(ContentDescriptor.BaseCols.ID));

    CursorRowPair cursorRowPair = m_idToCursorRow.get(id);
    if (null == cursorRowPair) return null;

    Cursor cursor = cursorRowPair.cursor;
    int row = cursorRowPair.row;
    cursor.moveToPosition(row);
    return cursor;
}


// //////////////////////////////////////////////

@Override
public void close() {
    Log.d(TAG, "close");

    for (Cursor cursor : m_cursors) {
        cursor.close();
    }
    m_idCursor.close();
}


@Override
public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
    Log.d(TAG, "copyStringToBuffer");

    getInternalCursor().copyStringToBuffer(columnIndex, buffer);
}

etc etc etc.

Upvotes: 3

Michał Klimczak
Michał Klimczak

Reputation: 13154

Unfortunately one CursorLoader can only load one Cursor. So the solution was to write a custom AsyncTaskLoader which returned two Cursors.

Upvotes: 0

Barak
Barak

Reputation: 16393

In you're adapter query the offers cursor for all records and make it a class variable. Then in your getView use the event id to iterate through the offer cursor and add the necessary textviews to your row layout when it find an appropriate match. It's not elegant, but it should work.

Upvotes: 0

Related Questions