Cursor、CursorAdapter、LoaderManager、Loader的关系

Relationship between Cursor, CursorAdapter, LoaderManager, Loader

所以在过去的几个月里,我一直在进行 IOS 开发工作,并且广泛使用了 Core Data 和 NSFetchedResultsController。

Core Data + NSFetchedResultsController:自动检测数据库的变化并相应地更新 table 元素。

现在我已经切换到 Android 开发并且我一直在寻找与上述相同的东西。我查看了各种可用的 classes,但有点困惑。

classes 的不同类型:

  1. 游标:提供对数据库查询结果的访问。
  2. CursorAdapter:将(列表、回收器)视图与光标链接并显示对象。
  3. ContentProvider:提供对数据库对象的访问。需要使用 LoaderManager。
  4. LoaderManager:由 Activity 或 Fragment 实现以无阻塞地加载数据 UI
  5. 加载器:当内容改变时生成游标对象的加载器。

我认为还值得一提的是我正在使用 greendao,我正在使用它生成的 ContentProvider。

更新流程

这是我有点疑惑的地方。这是我的假设。

  1. Content Provider 维护一个游标 -- 由 LoaderManager 使用,也可能由 CursorAdapter 使用。
  2. 当数据库发生变化时,Loader 有一个 ForceLoadContentObserver,它会观察游标并在其内容发生变化时调用 onLoadFinished。
  3. 通常游标适配器会在 onLoadFinished() 中调用 swap(),从而更新 table。

牢记以上内容,我创建了一个 ContentProvider 并实现了 LoaderManager,但是当我持久化一个新对象(使用 greenDao)时函数 onLoadFinished() 没有被调用——这让我开始质疑是否我正确地理解了这个过程。这是一段代码,展示了到目前为止我通常编写的代码。

片段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
}

如果有人可以确认我是否正确地考虑了这个过程and/or阐明可能出了什么问题,我将不胜感激。

编辑 1

这是 greenDao 生成的 ContentProvider subclass 中 query() 函数的片段。

@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;
}

解决方案

我只是想分享一下我是如何使用 GreenDao 实现上述所有 classes 的。让我知道是否有更好的方法来执行此操作,但我觉得这已经足够了。

  1. 通过将以下代码片段添加到 GreenDaoGenerator class

    来生成一个 ContentProvider subclass
    Entity entity = new Entity();
    entity.addContentProvider();
    
  2. 检查是否正确调用了 ContentProvider subclass 方法。将 javadoc 部分添加到 AndroidManifest.xml。对我来说,我还必须将生成的 ContentProvider class BASE_PATH 变量更改为另一个变量,如下所示。

    public static final String BASE_PATH = "MYTABLENAME";
    
  3. 在Activity/Fragmentclass中添加LoaderManager、Loader,就像我上面写的那样。在使用 ContentProvider 之前,您还必须设置 daoSession 对象。我已将下面的代码片段移至另一个初始化程序 class,以便其他 Activity/Fragment classes 也可以使用它。

    ContentProvider.daoSession = Main.daoSession;
    
  4. 我使用的是 RecyclerView,因此我对此处 https://gist.github.com/skyfishjy/443b7448f59be978bc59 提供的 CursorRecyclerViewAdapter class 进行了子class 并进行了一些自定义。如果您使用的是 ListView,则应该能够使用简单的 CursorAdapter。由于 Loader 正在监听更新,因此 Adapter 不需要监听更新。

  5. 现在 ContentProvider subclass 由于这条线

    能够更新视图
    getContext().getContentResolver().notifyChange(uri, null);
    

    由GreenDao自动生成。这意味着您可以将 ContentProvider 用于所有 CRUD 操作,并且视图将相应地自动更新。这似乎违背了使用 ORM 的目的。因此,我现在执行正常的 GreenDao 操作并在每次插入、删除、更新后手动调用 notifyChange

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

我认为没有比这更好的方法了,因为我真的不想接触 GreenDao 为 Model 和 Dao 对象生成的代码。我想可以添加嵌套生成的 CRUD 方法的自定义 CRUD 函数,但这应该是微不足道的。

我一直在四处寻找,并没有一个很好的文档记录方法来使用 GreenDao 和 Loader/LoaderManager 所以我想我会在这里组织这个。希望这对计划实施此计划的任何人有所帮助。

有几件事您没有完全正确。

  1. ContentProvider 是 Android 提供数据访问的组件。该数据可以存储在关系数据库、平面文件、远程服务器等中。提供程序具有类似 REST 的通用接口,并作为您拥有的不同数据存储选项的抽象层。 ContentProviders 也可以从外部应用程序访问(如果配置正确),这使它们成为应用程序之间共享数据的默认方式。访问它们不需要 LoaderManager。应通过 ContentResolver.
  2. 访问它们
  3. LoaderManager 负责 Loader 生命周期并将其与 ActivityFragment 生命周期协调。
  4. Loaders 提供了一种生命周期感知方式来将数据加载到活动或片段中。 CursorLoader 是一个特殊的实现,带有一些特性——它使用工作线程来保持 UI 线程的加载,它抽象使用 ContentResolver 来访问 ContentProvider并且还设置了一个ContentObserverContentObserver 将侦听查询 URL 的更新并在需要时重新加载数据。

为了让 ContentObserver 通知正常工作,您必须确保准备好几件事。首先,您的 ContentProvider 应该将更改传播给观察者。这是使用 getContext().getContentResolver().notifyChange(url, observer) 完成的。然后,如果您有一个 CursorLoader 在通知的 URL 上执行查询,它将自动重新加载。