ContentProvider实现举例
ContentProvider将数据封装起来,对外提供统一的接口,使得对数据的访问独立于数据的存储方式(文件、数据库等)。ContentProvider还提供跨应用的数据访问能力,并可以通过Search Framework实现自定义的搜索建议。可以参考这里来决定是否使用ContentProvider。
下面例子在RecyclerView使用方法举例(1)的基础上,展示ContentProvider的一个简单实现。
Contents
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. 完整代码
完整代码可以参考这里。