03、自定义ContentProvider与CRUD实战
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 类用于统一管理:
AUTHORITYCONTENT_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
03、自定义ContentProvider与CRUD实战
https://lautung.com/archives/03%E3%80%81%E8%87%AA%E5%AE%9A%E4%B9%89contentprovider%E4%B8%8Ecrud%E5%AE%9E%E6%88%98