03、自定义 ContentProvider 与 CRUD 实战

一、自定义 ContentProvider 的整体流程

自定义 ContentProvider 一般分为 6 步:

1. 设计数据结构
2. 定义 Contract 类
3. 编写 SQLiteOpenHelper 或其他数据源
4. 继承 ContentProvider
5. 实现 query / insert / update / delete / getType
6. 在 AndroidManifest.xml 中注册 provider

流程图:

flowchart TD
    A[设计要暴露的数据] --> B[定义 authority 与 URI]
    B --> C[配置 UriMatcher]
    C --> D[实现 ContentProvider]
    D --> E[Manifest 注册 provider]
    E --> F[其他 App 通过 ContentResolver 访问]

二、建议先定义 Contract 类

Contract 类用于统一管理:

  • AUTHORITY
  • CONTENT_URI
  • 表名
  • 字段名
  • MIME 类型

这样调用方和 Provider 方都能复用常量,避免字符串写错。

public final class BookContract {

    private BookContract() {}

    public static final String AUTHORITY = "com.example.databasetest.provider";
    public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY);

    public static final class Book implements BaseColumns {
        public static final String PATH = "book";
        public static final Uri CONTENT_URI =
                BASE_CONTENT_URI.buildUpon().appendPath(PATH).build();

        public static final String TABLE_NAME = "Book";

        public static final String COLUMN_NAME = "name";
        public static final String COLUMN_AUTHOR = "author";
        public static final String COLUMN_PAGES = "pages";
        public static final String COLUMN_PRICE = "price";

        public static final String CONTENT_TYPE =
                "vnd.android.cursor.dir/vnd." + AUTHORITY + "." + PATH;

        public static final String CONTENT_ITEM_TYPE =
                "vnd.android.cursor.item/vnd." + AUTHORITY + "." + PATH;
    }

    public static final class Category implements BaseColumns {
        public static final String PATH = "category";
        public static final Uri CONTENT_URI =
                BASE_CONTENT_URI.buildUpon().appendPath(PATH).build();

        public static final String TABLE_NAME = "Category";

        public static final String COLUMN_NAME = "name";
        public static final String COLUMN_CODE = "category_code";

        public static final String CONTENT_TYPE =
                "vnd.android.cursor.dir/vnd." + AUTHORITY + "." + PATH;

        public static final String CONTENT_ITEM_TYPE =
                "vnd.android.cursor.item/vnd." + AUTHORITY + "." + PATH;
    }
}

BaseColumns 会提供 _ID 字段常量,很多 Android 组件对 _id 字段有默认约定。

三、SQLiteOpenHelper 示例

public class MyDatabaseHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "BookStore.db";
    private static final int DB_VERSION = 1;

    private static final String CREATE_BOOK =
            "CREATE TABLE Book (" +
                    "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    "name TEXT, " +
                    "author TEXT, " +
                    "pages INTEGER, " +
                    "price REAL)";

    private static final String CREATE_CATEGORY =
            "CREATE TABLE Category (" +
                    "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    "name TEXT, " +
                    "category_code INTEGER)";

    public MyDatabaseHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 示例代码:实际项目中不要直接 drop,要做好数据迁移
        db.execSQL("DROP TABLE IF EXISTS Book");
        db.execSQL("DROP TABLE IF EXISTS Category");
        onCreate(db);
    }
}

四、Provider 基础骨架

public class DatabaseProvider extends ContentProvider {

    private static final int BOOK_DIR = 0;
    private static final int BOOK_ITEM = 1;
    private static final int CATEGORY_DIR = 2;
    private static final int CATEGORY_ITEM = 3;

    private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        uriMatcher.addURI(BookContract.AUTHORITY, "book", BOOK_DIR);
        uriMatcher.addURI(BookContract.AUTHORITY, "book/#", BOOK_ITEM);
        uriMatcher.addURI(BookContract.AUTHORITY, "category", CATEGORY_DIR);
        uriMatcher.addURI(BookContract.AUTHORITY, "category/#", CATEGORY_ITEM);
    }

    private MyDatabaseHelper dbHelper;

    @Override
    public boolean onCreate() {
        dbHelper = new MyDatabaseHelper(getContext());
        return true;
    }
}

onCreate() 返回:

返回值 含义
true Provider 初始化成功
false Provider 初始化失败

一般初始化数据库帮助类后返回 true

五、实现 query()

@Nullable
@Override
public Cursor query(
        @NonNull Uri uri,
        @Nullable String[] projection,
        @Nullable String selection,
        @Nullable String[] selectionArgs,
        @Nullable String sortOrder
) {
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    Cursor cursor;

    switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            cursor = db.query(
                    "Book",
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null,
                    sortOrder
            );
            break;

        case BOOK_ITEM:
            String bookId = uri.getPathSegments().get(1);
            cursor = db.query(
                    "Book",
                    projection,
                    "_id = ?",
                    new String[]{bookId},
                    null,
                    null,
                    sortOrder
            );
            break;

        case CATEGORY_DIR:
            cursor = db.query(
                    "Category",
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null,
                    sortOrder
            );
            break;

        case CATEGORY_ITEM:
            String categoryId = uri.getPathSegments().get(1);
            cursor = db.query(
                    "Category",
                    projection,
                    "_id = ?",
                    new String[]{categoryId},
                    null,
                    null,
                    sortOrder
            );
            break;

        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
    }

    Context context = getContext();
    if (context != null) {
        cursor.setNotificationUri(context.getContentResolver(), uri);
    }

    return cursor;
}

为什么要 setNotificationUri()

cursor.setNotificationUri(context.getContentResolver(), uri);

作用是把 Cursor 和 URI 关联起来。

当 Provider 后续调用:

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

观察这个 URI 的客户端就能收到数据变化通知。

六、实现 insert()

@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    Uri resultUri;

    switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
        case BOOK_ITEM:
            long newBookId = db.insert("Book", null, values);
            if (newBookId <= 0) {
                throw new SQLException("Failed to insert row into " + uri);
            }
            resultUri = ContentUris.withAppendedId(BookContract.Book.CONTENT_URI, newBookId);
            break;

        case CATEGORY_DIR:
        case CATEGORY_ITEM:
            long newCategoryId = db.insert("Category", null, values);
            if (newCategoryId <= 0) {
                throw new SQLException("Failed to insert row into " + uri);
            }
            resultUri = ContentUris.withAppendedId(BookContract.Category.CONTENT_URI, newCategoryId);
            break;

        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
    }

    notifyChange(resultUri);
    return resultUri;
}

七、实现 update()

@Override
public int update(
        @NonNull Uri uri,
        @Nullable ContentValues values,
        @Nullable String selection,
        @Nullable String[] selectionArgs
) {
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    int updatedRows;

    switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            updatedRows = db.update("Book", values, selection, selectionArgs);
            break;

        case BOOK_ITEM:
            String bookId = uri.getPathSegments().get(1);
            updatedRows = db.update("Book", values, "_id = ?", new String[]{bookId});
            break;

        case CATEGORY_DIR:
            updatedRows = db.update("Category", values, selection, selectionArgs);
            break;

        case CATEGORY_ITEM:
            String categoryId = uri.getPathSegments().get(1);
            updatedRows = db.update("Category", values, "_id = ?", new String[]{categoryId});
            break;

        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
    }

    if (updatedRows > 0) {
        notifyChange(uri);
    }

    return updatedRows;
}

八、实现 delete()

@Override
public int delete(
        @NonNull Uri uri,
        @Nullable String selection,
        @Nullable String[] selectionArgs
) {
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    int deletedRows;

    switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            deletedRows = db.delete("Book", selection, selectionArgs);
            break;

        case BOOK_ITEM:
            String bookId = uri.getPathSegments().get(1);
            deletedRows = db.delete("Book", "_id = ?", new String[]{bookId});
            break;

        case CATEGORY_DIR:
            deletedRows = db.delete("Category", selection, selectionArgs);
            break;

        case CATEGORY_ITEM:
            String categoryId = uri.getPathSegments().get(1);
            deletedRows = db.delete("Category", "_id = ?", new String[]{categoryId});
            break;

        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
    }

    if (deletedRows > 0) {
        notifyChange(uri);
    }

    return deletedRows;
}

九、实现 getType()

@Nullable
@Override
public String getType(@NonNull Uri uri) {
    switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            return BookContract.Book.CONTENT_TYPE;

        case BOOK_ITEM:
            return BookContract.Book.CONTENT_ITEM_TYPE;

        case CATEGORY_DIR:
            return BookContract.Category.CONTENT_TYPE;

        case CATEGORY_ITEM:
            return BookContract.Category.CONTENT_ITEM_TYPE;

        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
    }
}

十、封装 notifyChange()

private void notifyChange(Uri uri) {
    Context context = getContext();
    if (context != null) {
        context.getContentResolver().notifyChange(uri, null);
    }
}

它通常用于:

  • 插入数据后通知;
  • 更新数据后通知;
  • 删除数据后通知。

十一、Manifest 注册

Provider 必须在 AndroidManifest.xml 中注册,否则系统无法找到它。

<provider
    android:name=".DatabaseProvider"
    android:authorities="com.example.databasetest.provider"
    android:exported="true"
    android:readPermission="com.example.databasetest.permission.READ_PROVIDER"
    android:writePermission="com.example.databasetest.permission.WRITE_PROVIDER" />

如果只给自己 App 内部使用:

<provider
    android:name=".DatabaseProvider"
    android:authorities="com.example.databasetest.provider"
    android:exported="false" />

十二、客户端访问示例

查询

Uri uri = Uri.parse("content://com.example.databasetest.provider/book");

Cursor cursor = getContentResolver().query(
        uri,
        null,
        null,
        null,
        null
);

插入

ContentValues values = new ContentValues();
values.put("name", "第一行代码");
values.put("author", "郭霖");
values.put("pages", 570);
values.put("price", 99.0);

Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
Uri resultUri = getContentResolver().insert(uri, values);

更新

Uri uri = Uri.parse("content://com.example.databasetest.provider/book/1");

ContentValues values = new ContentValues();
values.put("price", 79.0);

int rows = getContentResolver().update(uri, values, null, null);

删除

Uri uri = Uri.parse("content://com.example.databasetest.provider/book/1");

int rows = getContentResolver().delete(uri, null, null);

十三、使用 adb 测试 Provider

可以用 Android 设备上的 content 命令快速测试。

查询:

adb shell content query --uri content://com.example.databasetest.provider/book

插入:

adb shell content insert \
  --uri content://com.example.databasetest.provider/book \
  --bind name:s:"Android" \
  --bind author:s:"Google" \
  --bind pages:i:300 \
  --bind price:f:59.9

删除:

adb shell content delete \
  --uri content://com.example.databasetest.provider/book/1

注意:如果 Provider 没有导出,或者需要权限,adb 命令可能访问失败。

十四、实战注意事项

注意点 说明
URI 要稳定 对外暴露后不要随便改
authority 要唯一 通常用包名加 .provider
不要暴露所有表 只暴露确实需要共享的数据
对外字段要稳定 调用方会依赖字段名
Cursor 要设置通知 URI 方便观察数据变化
插入更新后调用 notifyChange() 通知外部数据变更
使用参数化查询 避免 SQL 注入
大数据不要一次性返回 可以分页或限制字段
不要在 Provider 中做耗时网络请求 避免阻塞调用方

十五、核心记忆

自定义 ContentProvider 的核心代码结构:

UriMatcher 判断 URI
  ↓
根据 URI 选择表或资源
  ↓
执行 query / insert / update / delete
  ↓
返回 Cursor / Uri / 行数
  ↓
数据变化时 notifyChange