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);
        });
    }
}
小技巧:如果你需要跨进程通知,可以在 Room 的 insert()delete()update() 方法执行后,手动调用 ContentResolver.notifyChange()。这样其他进程的 ContentObserver 就能收到通知了。

4. 整体架构图

下面这张图,是我自己项目里用的架构。你看一眼就明白了:

Room + Content Provider + LiveData 整合架构 进程 A(当前应用) Activity 观察 LiveData ViewModel 持有 LiveData Room Database DAO → LiveData<List<Note>> Content Provider 跨进程暴露数据(Cursor) 进程 B(其他应用) 其他应用 通过 ContentResolver 访问 ContentObserver 监听数据变化 ContentResolver.query()

从图上你能看到:进程 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 里用 ListenableFutureRxJava 做异步处理。

核心总结:

  • Room 管本地数据,Provider 管跨进程暴露
  • LiveData 是连接 Room 和 UI 的桥梁
  • ContentObserver 是跨进程通知的机制
  • 三者结合,既能享受 Room 的简洁,又能利用 Provider 的跨进程能力

好了,这一章的内容就到这里。Room 和 Provider 的整合,说白了就是「各取所长」。你只要记住:本地用 Room,跨进程用 Provider,观察用 LiveData,基本就不会跑偏。

公众号:蓝海资料掘金营,微信deep3321