3、创建 Content Provider:继承 ContentProvider 类、实现 CRUD 方法、在 Manifest 中注册
好,咱们今天聊聊怎么亲手搭建一个 Content Provider。说实话,很多新手看到 ContentProvider 这个抽象类就头大——四个方法要重写,还得配 UriMatcher,感觉门槛不低。但你别怕,这东西说白了就是个「数据中间人」,你把它拆开看,每一步其实都很清晰。
3.1 继承 ContentProvider 类
第一步,创建一个类,继承 ContentProvider。我习惯先写个空壳子,把 IDEA 提示要重写的方法全部生成出来。你会看到这么几个方法:
public class MyContentProvider extends ContentProvider {
@Override
public boolean onCreate() {
// 初始化数据库或数据源
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// 查询数据
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// 插入数据
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// 更新数据
return 0;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 删除数据
return 0;
}
@Override
public String getType(Uri uri) {
// 返回 MIME 类型
return null;
}
}
嗯,这里要注意:onCreate() 返回 true 表示 Provider 初始化成功。我曾经见过有人在这里返回 false,结果整个 App 启动后 Content Provider 根本不可用,查了半天才发现是这个小坑。
3.2 实现 CRUD 方法
CRUD 是 Content Provider 的核心。说白了,就是让外部应用能通过 URI 来操作你的数据。我个人习惯先定义好 URI 和 UriMatcher,这样后面写起来思路清晰。
3.2.1 定义 URI 和 UriMatcher
public static final String AUTHORITY = "com.example.myapp.provider";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/items");
private static final int ITEMS = 1;
private static final int ITEM_ID = 2;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(AUTHORITY, "items", ITEMS);
uriMatcher.addURI(AUTHORITY, "items/#", ITEM_ID);
}
你想想看,UriMatcher 的作用就是帮你区分「我要操作全部数据」还是「我要操作某一条数据」。比如 content://com.example.myapp.provider/items 代表所有条目,而 content://com.example.myapp.provider/items/3 代表 id 为 3 的那一条。
3.2.2 query 方法实现
query 方法是最常用的。我在项目中遇到过一个问题:外部应用传进来的 projection 可能为 null,这时候你得自己处理,不能直接扔给数据库。
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor;
switch (uriMatcher.match(uri)) {
case ITEMS:
cursor = db.query(TABLE_NAME, projection, selection, selectionArgs,
null, null, sortOrder);
break;
case ITEM_ID:
String id = uri.getLastPathSegment();
cursor = db.query(TABLE_NAME, projection, "_id = ?",
new String[]{id}, null, null, null);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
// 设置监听,当数据变化时通知观察者
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
cursor.setNotificationUri()。如果不设置,外部应用通过 ContentResolver 注册的观察者就收不到数据变更通知。我早期就吃过这个亏,数据改了 UI 不刷新,用户还以为 App 卡死了。
3.2.3 insert 方法实现
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
long rowId;
switch (uriMatcher.match(uri)) {
case ITEMS:
rowId = db.insert(TABLE_NAME, null, values);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
if (rowId > 0) {
Uri resultUri = ContentUris.withAppendedId(CONTENT_URI, rowId);
// 通知数据变化
getContext().getContentResolver().notifyChange(resultUri, null);
return resultUri;
}
throw new SQLException("Failed to insert row into " + uri);
}
这里有个细节:insert 方法返回的 URI 要带上新插入行的 ID。这样外部应用拿到返回值后,可以直接用这个 URI 去查询刚插入的数据。说白了,这就是一种「回执」机制。
3.2.4 update 和 delete 方法
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int count;
switch (uriMatcher.match(uri)) {
case ITEMS:
count = db.update(TABLE_NAME, values, selection, selectionArgs);
break;
case ITEM_ID:
String id = uri.getLastPathSegment();
count = db.update(TABLE_NAME, values, "_id = ?",
new String[]{id});
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int count;
switch (uriMatcher.match(uri)) {
case ITEMS:
count = db.delete(TABLE_NAME, selection, selectionArgs);
break;
case ITEM_ID:
String id = uri.getLastPathSegment();
count = db.delete(TABLE_NAME, "_id = ?",
new String[]{id});
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
3.3 在 Manifest 中注册
Content Provider 必须在 AndroidManifest.xml 中注册,否则系统根本找不到它。这一步很多人会忘,尤其是从别的组件复制代码时容易漏掉。
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<provider
android:name=".MyContentProvider"
android:authorities="com.example.myapp.provider"
android:exported="true"
android:multiprocess="false" />
<!-- 其他组件 -->
</application>
几个关键属性:
- android:name:Provider 类的完整路径。如果类在子包中,要写全路径,比如
.data.MyContentProvider。 - android:authorities:唯一标识符,通常用包名加后缀。这个值要和代码中定义的 AUTHORITY 一致。
- android:exported:是否允许其他应用访问。如果设为
false,只有本应用能调用这个 Provider。 - android:multiprocess:是否在每个进程中创建 Provider 实例。默认
false,所有进程共享一个实例。
3.4 知识体系总览
下面这张图把创建 Content Provider 的完整流程串起来了。你可以对照着看,每一步都对应着具体的代码实现。
3.5 避坑指南
最后,我把自己踩过的坑总结一下,你写代码时多留个心眼:
- UriMatcher 匹配失败: 检查 URI 中的路径和 authorities 是否完全一致。我曾经因为大小写写错,查了整整一个下午。
- 多线程安全问题: Content Provider 的方法可能被多个线程同时调用。如果你的数据库操作不是线程安全的,记得加同步锁。
- getType 方法不能返回 null: 这个方法用于返回数据的 MIME 类型。如果返回 null,某些系统组件(比如 SearchView)会崩溃。
- exported 属性要谨慎: 如果 Provider 只给本应用用,设成
false。暴露给外部应用时,一定要做权限校验。
嗯,创建 Content Provider 其实没那么复杂。你只要把 URI 定义清楚,CRUD 方法写完整,Manifest 注册好,剩下的就是数据操作的具体逻辑了。多写几次,你会发现它就是个「带 URI 路由的数据库封装」而已。
公众号:蓝海资料掘金营,微信deep3321