Content Provider 与 Room 整合:Room 数据库、Room 与 Provider 结合、LiveData 观察
说实话,很多开发者对 Content Provider 的印象还停留在「老古董」阶段。觉得它笨重、难用,不如直接拿 Room 一把梭。嗯,我早期也这么想。直到有一次做通讯录同步模块,需要跨进程暴露数据给系统联系人应用,我才发现——Content Provider 和 Room 其实是天生一对。
今天我们就来聊聊,怎么把 Room 这个现代化的数据库框架,和 Content Provider 这个老牌组件优雅地整合在一起。说白了,就是用 Room 的简洁,去填补 Provider 的繁琐。
1. Room 数据库:先打好地基
在整合之前,我们得先把 Room 搭好。我个人习惯是,先定义 Entity,再写 DAO,最后搞 Database。这样思路最清晰。
// Entity
@Entity(tableName = "notes")
public class Note {
@PrimaryKey(autoGenerate = true)
public int id;
public String title;
public String content;
public long createdAt;
}
// DAO
@Dao
public interface NoteDao {
@Query("SELECT * FROM notes ORDER BY createdAt DESC")
LiveData<List<Note>> getAllNotes();
@Insert
long insert(Note note);
@Delete
int delete(Note note);
@Update
int update(Note note);
}
// Database
@Database(entities = {Note.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract NoteDao noteDao();
}
这里有个小细节:getAllNotes() 返回的是 LiveData。为什么?因为后面我们要和 Provider 的观察机制打通,LiveData 是最自然的桥梁。我在项目中踩过坑,如果返回普通 List,每次数据变化都得手动通知,太容易漏了。
2. Room 与 Provider 结合:让数据跨进程
好,数据库有了。现在我们要把它暴露给其他应用。怎么做?写一个 Content Provider,内部调用 Room 的 DAO。
你可能会问:为什么不直接用 Room?因为 Room 不支持跨进程访问。Content Provider 是 Android 官方钦定的跨进程数据共享方案。所以,Room 管本地,Provider 管跨进程,各司其职。
public class NoteProvider extends ContentProvider {
private AppDatabase database;
private static final String AUTHORITY = "com.example.app.note.provider";
private static final String TABLE_NAME = "notes";
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
private static final int CODE_NOTES = 1;
private static final int CODE_NOTE_ID = 2;
static {
URI_MATCHER.addURI(AUTHORITY, TABLE_NAME, CODE_NOTES);
URI_MATCHER.addURI(AUTHORITY, TABLE_NAME + "/#", CODE_NOTE_ID);
}
@Override
public boolean onCreate() {
database = AppDatabase.getInstance(getContext());
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection,
@Nullable String selection, @Nullable String[] selectionArgs,
@Nullable String sortOrder) {
// 这里直接调用 Room 的 DAO
// 但 Room 返回的是 LiveData,我们需要转成 Cursor
// 嗯,这里有个坑,后面会讲
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
Note note = new Note();
note.title = values.getAsString("title");
note.content = values.getAsString("content");
note.createdAt = System.currentTimeMillis();
long id = database.noteDao().insert(note);
getContext().getContentResolver().notifyChange(uri, null);
return ContentUris.withAppendedId(uri, id);
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection,
@Nullable String[] selectionArgs) {
// 省略具体实现
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values,
@Nullable String selection, @Nullable String[] selectionArgs) {
// 省略具体实现
return 0;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return "vnd.android.cursor.dir/vnd.com.example.note";
}
}
query() 我留了个空。为什么?因为 Room 的 DAO 返回的是 LiveData,而 Provider 的 query 必须返回 Cursor。这两个类型不兼容。我曾经在这里卡了整整一个下午,最后发现需要手动转换。
解决方案有两种:
- 方案一:在 DAO 中额外写一个返回 Cursor 的方法,用
@RawQuery或直接执行 SQLite 查询。 - 方案二:用 Room 的
query()方法拿到SupportSQLiteDatabase,然后自己查。
我个人推荐方案一,因为更干净。但要注意,这样你就绕过了 Room 的类型安全机制。嗯,有得必有失。
3. LiveData 观察:让 UI 自动刷新
好,现在 Provider 能跨进程了。但还有个问题:当其他进程修改了数据,当前进程的 UI 怎么知道?
传统做法是用 ContentObserver。但说实话,那玩意儿用起来挺别扭的。你想想看,你得手动注册、手动注销,还得在回调里重新查询。太啰嗦了。
我的做法是:在 Provider 的 query 方法里,把 Room 的 LiveData 和 ContentObserver 打通。具体来说,就是让 LiveData 在数据变化时,自动触发 ContentResolver.notifyChange()。
// 在 DAO 中,用 LiveData 包装查询结果
@Dao
public interface NoteDao {
@Query("SELECT * FROM notes ORDER BY createdAt DESC")
LiveData<List<Note>> getAllNotesLiveData();
}
// 在 ViewModel 中,观察 LiveData
public class NoteViewModel extends AndroidViewModel {
private AppDatabase database;
private LiveData<List<Note>> notes;
public NoteViewModel(@NonNull Application application) {
super(application);
database = AppDatabase.getInstance(application);
notes = database.noteDao().getAllNotesLiveData();
}
public LiveData<List<Note>> getNotes() {
return notes;
}
}
// 在 Activity 中,直接观察
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
NoteViewModel viewModel = new ViewModelProvider(this).get(NoteViewModel.class);
viewModel.getNotes().observe(this, notes -> {
// UI 自动更新
adapter.submitList(notes);
});
}
}
insert()、delete()、update() 方法执行后,手动调用 ContentResolver.notifyChange()。这样其他进程的 ContentObserver 就能收到通知了。
4. 整体架构图
下面这张图,是我自己项目里用的架构。你看一眼就明白了:
从图上你能看到:进程 A 内部,Activity 观察 ViewModel 的 LiveData,ViewModel 从 Room 拿数据。进程 B 通过 ContentResolver 访问 Provider,Provider 内部再调 Room。数据变化时,通过 ContentObserver 通知。
5. 避坑指南
最后,分享几个我踩过的坑:
- LiveData 转 Cursor:Room 的 LiveData 不能直接给 Provider 用。我建议在 DAO 里额外写一个返回 Cursor 的方法,或者用
@RawQuery执行原生 SQL。 - 跨进程通知:如果你用 LiveData 观察本地数据变化,别忘了在 Provider 的 insert/delete/update 里调用
notifyChange()。否则其他进程收不到通知。 - 生命周期管理:LiveData 是生命周期感知的,但 ContentObserver 不是。如果你在 Activity 里注册了 ContentObserver,记得在 onDestroy 里注销。否则会内存泄漏。
- 线程问题:Room 默认不允许在主线程做数据库操作。但 Provider 的 query 方法可能被主线程调用。我建议在 DAO 里用
ListenableFuture或RxJava做异步处理。
核心总结:
- Room 管本地数据,Provider 管跨进程暴露
- LiveData 是连接 Room 和 UI 的桥梁
- ContentObserver 是跨进程通知的机制
- 三者结合,既能享受 Room 的简洁,又能利用 Provider 的跨进程能力
好了,这一章的内容就到这里。Room 和 Provider 的整合,说白了就是「各取所长」。你只要记住:本地用 Room,跨进程用 Provider,观察用 LiveData,基本就不会跑偏。