ContentProvider实现举例

  ContentProvider将数据封装起来,对外提供统一的接口,使得对数据的访问独立于数据的存储方式(文件、数据库等)。ContentProvider还提供跨应用的数据访问能力,并可以通过Search Framework实现自定义的搜索建议。可以参考这里来决定是否使用ContentProvider。

  下面例子在RecyclerView使用方法举例(1)的基础上,展示ContentProvider的一个简单实现。

1. 设计数据存储方式

  ContentProvider是访问数据的接口,首先要决定数据的具体存储方式。这里使用SQLite存储从TMDb上取得的一系列电影信息。

1.1. 设计Contract

  Contract定义了数据的具体结构,以及用于访问数据的URI的结构。参考这里的从TMDb取得JSON文件样例,电影信息应包含如下几个字段:

public static final String COLUMN_TITLE = "title";
public static final String COLUMN_POSTER_PATH = "poster_path";
public static final String COLUMN_RELEASE_DATE = "release_date";
public static final String COLUMN_OVERVIEW = "overview";
public static final String COLUMN_BACKDROP_PATH = "backdrop_path";
public static final String COLUMN_POPULARITY = "popularity";
public static final String COLUMN_VOTE_AVERAGE = "vote_average";
public static final String COLUMN_VOTE_COUNT = "vote_count";
public static final String COLUMN_ORIGINAL_TITLE = "original_title";
public static final String COLUMN_ORIGINAL_LANGUAGE = "original_language";
public static final String COLUMN_ADULT = "adult";
public static final String COLUMN_VIDEO = "video";
public static final String COLUMN_GENRE_IDS = "genre_ids";
public static final String COLUMN_ID = "id";

  定义Provider的名称,即Authority:
public static final String CONTENT_AUTHORITY = "com.nex3z.examples.contentprovider";

  在此基础上,定义URI的格式:
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
public static final String PATH_MOVIE = "movie";

public static final Uri CONTENT_URI =
        BASE_CONTENT_URI.buildUpon().appendPath(PATH_MOVIE).build();

  定义Content Type:
public static final String CONTENT_TYPE =
        ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_MOVIE;
public static final String CONTENT_ITEM_TYPE =
        ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_MOVIE;

这里CURSOR_DIR_BASE_TYPE 指示URI包含的Cursor具有零个或多个元素,CONTENT_ITEM_TYPE 指示URI包含的Cursor只具有一个元素。

【完整代码】

1.2. 实现数据库存储

  Android提供了SQLiteOpenHelper用于数据库的创建和版本管理,创建其子类来实现电影数据库的创建:

public class MovieDbHelper extends SQLiteOpenHelper {
    private static final int DATABASE_VERSION = 4;
    static final String DATABASE_NAME = "movie.db";

    public MovieDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        final String SQL_CREATE_MOVIE_TABLE = "CREATE TABLE " + MovieEntry.TABLE_NAME + " (" +
                MovieEntry._ID + " INTEGER PRIMARY KEY," +
                MovieEntry.COLUMN_TITLE + " TEXT NOT NULL, " +
                MovieEntry.COLUMN_POSTER_PATH + " TEXT NOT NULL, " +
                MovieEntry.COLUMN_RELEASE_DATE + " DATE NOT NULL, " +
                MovieEntry.COLUMN_OVERVIEW + " TEXT NOT NULL, " +
                MovieEntry.COLUMN_BACKDROP_PATH + " TEXT NOT NULL, " +
                MovieEntry.COLUMN_POPULARITY + " REAL NOT NULL, " +
                MovieEntry.COLUMN_VOTE_AVERAGE + " REAL NOT NULL, " +
                MovieEntry.COLUMN_VOTE_COUNT + " INTEGER NOT NULL, " +
                MovieEntry.COLUMN_ORIGINAL_TITLE + " TEXT NOT NULL, " +
                MovieEntry.COLUMN_ORIGINAL_LANGUAGE + " TEXT NOT NULL, " +
                MovieEntry.COLUMN_ADULT + " BOOLEAN NOT NULL, " +
                MovieEntry.COLUMN_VIDEO + " BOOLEAN NOT NULL, " +
                MovieEntry.COLUMN_GENRE_IDS + " TEXT NOT NULL, " +
                MovieEntry.COLUMN_ID + " INTEGER UNIQUE NOT NULL " +
                " );";

        db.execSQL(SQL_CREATE_MOVIE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("DROP TABLE IF EXISTS " + MovieEntry.TABLE_NAME);
        onCreate(db);
    }
}

onCreate() 中根据Contract中的定义,构造创建数据库的SQL语句并执行,由此创建数据库。DATABASE_VERSION 指示数据库版本,onUpgrade() 用来处理数据库版本升级,这里只是将数据库删除重新创建,因为例子中使用的数据都可以从TMDb获取。

【完整代码】

1.3. 实现Content Provider

  实现Content Provider首先要继承ContentProvider,主要工作是实现query() 、insert() 、update() 、delete() 四个方法。另外还要实现一个UriMatcher,用于获取访问数据的URI的类型,根据不同的URI类型,前面的四个方法可能需要进行不同的处理。

1.3.1. 实现UriMatcher

  首先定义URI类型,这里只有两种:

static final int MOVIE = 100;
static final int MOVIE_WITH_ID = 101;

MOVIE 类型对应content://com.nex3z.examples.contentprovider/movie ,用于获取所有电影数据;MOVIE_WITH_ID 类型对应形如content://com.nex3z.examples.contentprovider/movie/# 的URI,这里的# 表示任意数字,用于获取id为# 的一个电影。此外还可以用* 表示任意有效字符,这里没有使用。

  然后实现UriMatcher:

private static final UriMatcher sUriMatcher = buildUriMatcher();

static UriMatcher buildUriMatcher() {
    final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
    final String authority = MovieContract.CONTENT_AUTHORITY;

    matcher.addURI(authority, MovieContract.PATH_MOVIE, MOVIE);
    matcher.addURI(authority, MovieContract.PATH_MOVIE + "/#", MOVIE_WITH_ID);

    return matcher;
}

1.3.2. 实现query()

  query()用于请求数据。针对MOVIE_WITH_ID 类型的URI,只向数据库请求指定id的电影;针对MOVIE 类型的URI,向数据库请求所有电影。

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    Cursor retCursor;
    switch (sUriMatcher.match(uri)) {
        case MOVIE_WITH_ID: {
            retCursor = getMovieById(uri, projection, sortOrder);
            break;
        }
        case MOVIE: {
            retCursor = mOpenHelper.getReadableDatabase().query(
                    MovieContract.MovieEntry.TABLE_NAME,
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null,
                    sortOrder
            );
            break;
        }
        default:
            throw new UnsupportedOperationException("Unknown uri: " + uri);
    }
    retCursor.setNotificationUri(getContext().getContentResolver(), uri);
    return retCursor;
}

  这里getMovieById() 实现如下:
private Cursor getMovieById(Uri uri, String[] projection, String sortOrder) {
    int movieId = MovieContract.MovieEntry.getMovieIdFromUri(uri);

    String[] selectionArgs = new String[]{Integer.toString(movieId)};
    String selection = sMovieIdSelection;

    return sMovieQueryBuilder.query(mOpenHelper.getReadableDatabase(),
            projection,
            selection,
            selectionArgs,
            null,
            null,
            sortOrder
    );
}

其中sMovieIdSelection 和sMovieQueryBuilder 分别为:
private static final String sMovieIdSelection = MovieContract.MovieEntry.TABLE_NAME + "." + 
        MovieContract.MovieEntry.COLUMN_ID + " = ?";

private static final SQLiteQueryBuilder sMovieQueryBuilder;
static{
    sMovieQueryBuilder = new SQLiteQueryBuilder();
    sMovieQueryBuilder.setTables(MovieContract.MovieEntry.TABLE_NAME);
}

这里sMovieIdSelection  用问号?代替了参数,sMovieQueryBuilder.query() 通过分别指定selection 和selectionArgs 分离了查询和参数,可以防止数据库注入。

1.3.3. 实现insert()

  insert()用于插入数据,向数据库插入数据后,使用notifyChange() 通知数据变化,生成对应的URI并返回:

@Override
public Uri insert(Uri uri, ContentValues values) {
    final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    final int match = sUriMatcher.match(uri);
    Uri returnUri;

    switch (match) {
        case MOVIE: {
            long _id = db.insert(MovieContract.MovieEntry.TABLE_NAME, null, values);
            if ( _id > 0 )
                returnUri = MovieContract.MovieEntry.buildMovieUri(_id);
            else
                throw new android.database.SQLException("Failed to insert row into " + uri);
            break;
        }
        default:
            throw new UnsupportedOperationException("Unknown uri: " + uri);
    }
    getContext().getContentResolver().notifyChange(uri, null);

    return returnUri;
}

1.3.4. 实现update()

  update()用于更新已有数据,成功更新后,使用notifyChange() 通知数据变化:

@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    final int match = sUriMatcher.match(uri);
    int rowsUpdated;

    switch (match) {
        case MOVIE: {
            rowsUpdated = db.update(MovieContract.MovieEntry.TABLE_NAME, values, selection,
                    selectionArgs);
            break;
        }
        default:
            throw new UnsupportedOperationException("Unknown uri: " + uri);
    }
    if (rowsUpdated != 0) {
        getContext().getContentResolver().notifyChange(uri, null);
    }
    return rowsUpdated;
}

1.3.5. 实现delete()

  delete()用于删除数据,成功删除后,同样使用notifyChange() 通知数据变化:

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
    final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    final int match = sUriMatcher.match(uri);
    int rowsDeleted;
    if ( null == selection ) selection = "1";

    switch (match) {
        case MOVIE: {
            rowsDeleted = db.delete(
                    MovieContract.MovieEntry.TABLE_NAME, selection, selectionArgs);
            break;
        }
        default:
            throw new UnsupportedOperationException("Unknown uri: " + uri);
    }

    if (rowsDeleted != 0) {
        getContext().getContentResolver().notifyChange(uri, null);
    }
    return rowsDeleted;
}

1.3.6. 实现bulkInsert()

  如果数据条目较多,单独使用insert()效率较低,bulkInsert()提供了打包插入数据的方法,更加高效。

@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
    final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    final int match = sUriMatcher.match(uri);
    switch (match) {
        case MOVIE: {
            db.beginTransaction();
            int returnCount = 0;
            try {
                for (ContentValues value : values) {
                    long _id = db.insert(MovieContract.MovieEntry.TABLE_NAME, null, value);
                    if (_id != -1) {
                        returnCount++;
                    }
                }
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
            getContext().getContentResolver().notifyChange(uri, null);
            return returnCount;
        }
        default:
            return super.bulkInsert(uri, values);
    }
}

这里先使用beginTransaction() 以EXCLUSIVE模式开始一个Transaction,然后依次插入数据,此时数据并没有被真正插入。之后调用setTransactionSuccessful() 标记当前Transaction为成功,最后通过endTransaction() 应用修改。

【完整代码】

2. 使用ContentProvider

2.1. 声明Provider

  使用ContentProvider前,首先要在AndroidManifest.xml中声明所使用的Provider:

<application
    ...>
    <activity
        ...
    </activity>
    <provider
        android:name=".provider.MovieProvider"
        android:authorities="com.nex3z.examples.contentprovider" />
</application>

【完整代码】

2.2. 插入数据

  从Retorfit获取到Movie后,首先转换为ContentValue,然后就可以通过bulkInsert() 插入到数据库了。

public static void addToDatabase(Context context, List<Movie> movies) {
    Vector<ContentValues> cVVector = new Vector<ContentValues>(movies.size());

    for (int i = 0; i < movies.size(); i++) {
        ContentValues movieValues = buildContentValues(movies.get(i));
        cVVector.add(movieValues);
    }
    if (cVVector.size() > 0) {
        ContentValues[] cvArray = new ContentValues[cVVector.size()];
        cVVector.toArray(cvArray);
        context.getContentResolver()
                .bulkInsert(MovieContract.MovieEntry.CONTENT_URI, cvArray);
    }
    Log.d(LOG_TAG, "addToDatabase(): Write database Complete, "
            + cVVector.size() + " Inserted.");
}

这里getContentResolver() 会根据所使用的URI获取对应的Content Provider,不需要直接指定Content Provider。

【完整代码】

2.3. 请求数据

  向Content Provider请求数据的方法可以参考使用CursorLoader及更新RecyclerView

3. 完整代码

  完整代码可以参考这里