首页 » Android程序设计:第2版 » Android程序设计:第2版全文在线阅读

《Android程序设计:第2版》一个完整的内容提供者代码:SimpleFinchVideoContentProvider

关灯直达底部

通过前面的介绍,已经了解了和编写内容提供者及Android MVC关联的重要任务——Android内容提供者的通信系统。下面我们一起来看看如何构建自己的内容提供者。如下所示,SimpleFinchVideoContentProvider类继承自ContentProvider类:


public class SimpleFinchVideoContentProvider extends ContentProvider {  

SimpleFinchVideoContentProvider类和实例变量

和前面一样,在查看方法是如何工作的之前,最好先理解该方法所使用的主要类和实例变量。对于SimpleFinchVideoContentProvider,需要理解的成员变量是:


private static final String DATABASE_NAME = /"simple_video.db/";private static final int DATABASE_VERSION = 2;private static final String VIDEO_TABLE_NAME = /"video/";private DatabaseHelper mOpenHelper;  

DATABASE_NAME

设备上的数据库文件名称。对于简单的Finch视频,该文件的完整路径是/data/data/com.oreilly.demo.pa.finchvideo/databases/simple_video.db。

DATABASE_VERSION

和代码兼容的数据库版本。如果其版本号比数据库本身的版本号高,应用会调用DatabaseHelper.onUpdate方法。

VIDEO_TABLE_NAME

simple_video数据库内的视频表的名称。

mOpenHelper

onCreate方法中初始化的数据库helper实例变量。它为insert、query、update和delete方法提供了访问数据库的方式。

sUriMatcher

静态初始化代码块,它执行静态变量的初始化,这些变量不能作为简单的单行语句执行。例如,简单视频内容提供者就是以在UriMatcher的静态初始化部分构建内容提供者URI映射开始的,构建的具体方式如下:


      private static UriMatcher sUriMatcher;      private static final int VIDEOS = 1;      private static final int VIDEO_ID = 2;      static {            sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);            sUriMatcher.addURI(AUTHORITY, FinchVideo.SimpleVideos.VIDEO_NAME, VIDEOS);            // use of the hash character indicates matching of an id            sUriMatcher.addURI(AUTHORITY,                  FinchVideo.SimpleVideos.VIDEO_NAME + /"/#/", VIDEO_ID);      ...      // more initialization to follow  

UriMatcher类提供了基础的便捷工具,Android使用这些工具实现对内容提供者URI的映射。要使用UriMatcher实例,需要把URI字符串,如videos映射到常量成员变量。在这里,映射工作如下:应用首先给提供者UriMatcher的构造函数提供一个参数Uri Matcher.NO_MATCH,定义所有的URI都和给定的URI不匹配。然后,应用把多个视频的映射添加到VIDEOS,然后把特定视频映射到VIDEO_ID。对于映射到整数值的所有提供者URI,该提供者可以执行切换操作,跳到多个和单个视频的相应的处理代码。

该映射使得如content://com.oreilly.demo.pa.finch video.SimpleFinchVideo/video这样的URI映射到常量VIDEOS,表示所有的视频。单个视频的URI,如content://oreilly.demo.pa.finchvideo.SimpleFinchVideo/video/7,对于单个视频,会映射到常量VIDEO_ID。URI匹配绑定的散列标识是以整数结束的通配符。

sVideosProjectionMap

它是query方法使用的项目映射。该HashMap把内容提供者的字段名映射到了数据库的字段。项目映射不是必须的,但是如果使用这个项目映射,就必须列出query方法可能返回的所有字段。在SimpleFinchVideoContentProvider类中,内容提供者的字段和数据库的字段名称是完全一样的,因此sVideosProjectionMap不是必须的。但是在这里,我们提供该项目映射就是为了说明它,有时候应用可能会用到它。在下面这段代码中,创建了一个示例映射:


      // example projection map, not actually used in this application      sVideosProjectionMap = new HashMap<String, String>;      sVideosProjectionMap.put(FinchVideo.Videos._ID,            FinchVideo.Videos._ID);      sVideosProjectionMap.put(FinchVideo.Videos.TITLE,            FinchVideo.Videos.TITLE);      sVideosProjectionMap.put(FinchVideo.Videos.VIDEO,            FinchVideo.Videos.VIDEO);      sVideosProjectionMap.put(FinchVideo.Videos.DESCRIPTION,            FinchVideo.Videos.DESCRIPTION);  

实现onCreate方法

在SimpleFinchVideoContentProvider的初始化中,创建了该视频的SQLite数据存储,具体代码如下:


private static class DatabaseHelper extends SQLiteOpenHelper {      public void onCreate(SQLiteDatabase sqLiteDatabase) {                  createTable(sqLiteDatabase);      }      // create table method may also be called from onUpgrade      private void createTable(SQLiteDatabase sqLiteDatabase) {            String qs = /"CREATE TABLE /" + VIDEO_TABLE_NAME + /" (/" +                  FinchVideo.SimpleVideos._ID + /" INTEGER PRIMARY KEY, /" +                  FinchVideo.SimpleVideos.TITLE_NAME + /" TEXT, /" +                  FinchVideo.SimpleVideos.DESCRIPTION_NAME + /" TEXT, /" +                  FinchVideo.SimpleVideos.URI_NAME + /" TEXT);/";            sqLiteDatabase.execSQL(qs);      }} 

当创建SQLite表以支持内容提供者操作时,开发人员需要提供_id字段。虽然你可能不太清楚为什么要提供这个字段,除非你详细阅读了Android开发者文档,但Android内容管理系统确实强制要求在query方法返回的游标中必须有_id字段。_id用于和内容提供者URL中的特殊的#字符匹配。例如,如content://contacts/people/25这样的URL会映射到contacts表中_id为25的数据记录。强制提供_id实际上是为了用一个专用的名称来表示数据库表的主键。

实现getType方法

下一步,实现getType方法以确定从客户端传递过来的任意URI的MIME类型。正如你将在下面的代码中所见到的,在public API的定义中,采用URI表示VIDEOS,VIDEO_ID表示MIME类型。


public String getType(Uri uri) {    switch (sUriMatcher.match(uri)) {        case VIDEOS:            return FinchVideo.SimpleVideos.CONTENT_TYPE;        case VIDEO_ID:            return FinchVideo.SimpleVideos.CONTENT_VIDEO_TYPE;        default:            throw new IllegalArgumentException(/"Unknown video type: /" + uri);    }}  

实现提供者API

内容提供者的实现中必须覆盖基类ContentProvider的各个数据处理方法:insert、query、update和delete。对于本章的简单视频应用这个例子,这些方法是在SimpleFinchVideoContentProvider类中定义的。

query方法

当匹配了输入的URI后,内容提供者的query方法会把处理委托给SQLiteDatabase.query,在一个可读的数据库上执行相应的选择操作,然后以数据库Cursor对象的形式返回结果。该游标会包含URI参数所描述的所有数据库记录。在执行完查询后,Android的内容提供者机制会自动支持多进程使用cursor实例,它支持提供者的query方法简单地把cursor值作为正常值返回,使得其他进程的客户端可以使用该返回值。

query方法还支持参数uri、projection、selection、selectionArgs和sortOrder,其使用方式和我们在第9章中介绍的SQLiteDatabase.query方法相同。正如任何SQL SELECT语句那样,query方法的参数使得提供者客户端只需要选择和query参数匹配的特定视频。除了传递URI,调用SimpleFinchVideoContentProvider的客户端还可以传递包含where参数的where子句。例如,开发人员可以使用这个参数实现对某个作者的视频的查询。

注意:正如我们看到的,Android的MVC模式依赖于游标和它们包含的数据,以及框架的内容观察者更新消息的传递。因为进程会共享Cursor对象,所以内容提供者实现必须注意不要在query方法中关闭游标。如果游标在query方法中被关闭了,那么客户端将无法看到抛出的异常;相反,游标总是会表现得似乎其指向的数据是空的,而且不再接收更新事件——由activity负责合理地管理返回的游标。

当数据库查询完成后,provider会调用Cursor.setNotificationUri方法来设置URI,提供者架构要根据这个URI决定哪个provider更新事件要被传递给新创建的游标。该URI成为观察URI指向的数据的客户端和通知该URI的内容提供者之间的交互参数。简单的方法调用驱动内容提供者更新消息,我们在P333“Android MVC和内容查看器”一节中探讨过。

下面给出本书描述的内容提供者的query方法,它执行URI匹配,查询数据库并返回光标:


@Overridepublic Cursor query(Uri uri, String projection, String where,                    String whereArgs, String sortOrder){    // If no sort order is specified use the default    String orderBy;    if (TextUtils.isEmpty(sortOrder)) {        orderBy = FinchVideo.SimpleVideos.DEFAULT_SORT_ORDER;    } else {        orderBy = sortOrder;    }    int match = sUriMatcher.match(uri);①    Cursor c;    switch (match) {        case VIDEOS:            // query the database for all videos            c = mDb.query(VIDEO_TABLE_NAME, projection,                    where, whereArgs,                    null, null, sortOrder);            c.setNotificationUri(                    getContext.getContentResolver,                    FinchVideo.SimpleVideos.CONTENT_URI);②            break;        case VIDEO_ID:            // query the database for a specific video            long videoID = ContentUris.parseId(uri);            c = mDb.query(VIDEO_TABLE_NAME, projection,                FinchVideo.Videos._ID + /" = /" + videoID +                    (!TextUtils.isEmpty(where) ?                        /" AND (/" + where + /')/' : /"/"),                whereArgs, null, null, sortOrder);            c.setNotificationUri(                getContext.getContentResolver,                FinchVideo.SimpleVideos.CONTENT_URI);            break;        default:            throw new IllegalArgumentException(/"unsupported uri: /" + uri);    }    return c;③}  

以下是一些重点代码的解释:

① 使用预构建的URI匹配器匹配URI。

② 设置FinchVideo.SimpleVideos.CONTENT_URI的通知URI,它使得游标能够接收到该URI所指向的数据的所有内容解析程序通知事件。在这个例子中,cursor会接收到和所有视频相关的所有事件,因为FinchVideo.SimpleVideos.CONTENT_URI就是指向这些事件。

③ 直接返回光标。正如前面提到的,Android的内容提供者系统支持进程之间光标中的数据的共享。进程间数据作为内容提供者系统的一部分“自由”共享。可以返回光标,选择不同的进程就可以访问该光标。

insert方法

insert方法接收客户端输入的数据值,校验这些值,然后向数据库中增加包含这些值的一条新的记录。这些数据值会传递给ContentValues对象的ContentProvider类:


@Overridepublic Uri insert(Uri uri, ContentValues initialValues) {    // Validate the requested uri    if (sUriMatcher.match(uri) != VIDEOS) {        throw new IllegalArgumentException(/"Unknown URI /" + uri);    }    ContentValues values;    if (initialValues != null) {        values = new ContentValues(initialValues);    } else {        values = new ContentValues;    }    verifyValues(values);    // insert the initialValues into a new database row    SQLiteDatabase db = mOpenDbHelper.getWritableDatabase;    long rowId = db.insert(VIDEO_TABLE_NAME,            FinchVideo.SimpleVideos.VIDEO_NAME, values);    if (rowId > 0) {        Uri videoURi =                ContentUris.withAppendedId(                        FinchVideo.SimpleVideos.CONTENT_URI, rowId);①        getContext.getContentResolver.            notifyChange(videoURi, null);②        return videoURi;    }    throw new SQLException(/"Failed to insert row into /" + uri);}  

insert方法还会匹配输入的URI,执行相应的数据库插入操作,然后返回指向新的数据库记录的URI。因为SQLiteDatabase.insert方法返回的是新插入记录的数据库记录ID,即_id字段的值,所以内容提供者可以很容易地通过把rowID变量附加到在第3章提到的内容提供者public API中定义的内容提供者authority中,生成正确的URI。

以下是代码的一些要点:

① 使用Android工具管理内容提供者URI——特别是,ContentUris.withAppendedId把rowId作为返回结果插入URI的ID。客户端也可以使用该URI查询内容提供者,选择包含插入记录的数据值的游标。

② 内容提供者通知URI,向相关的游标发送和传递内容更新事件。注意,提供者的通知调用是唯一会被发送给内容观察者的事件。

update方法

update方法和insert方法的执行方式相同。update方法在相应的数据库上执行,以改变URI所指向的数据库记录。但update方法返回的是该操作所影响的记录数:


@Overridepublic int update(Uri uri, ContentValues values, String where,                  String whereArgs){    // the call to notify the uri after deletion is explicit    getContext.getContentResolver.notifyChange(uri, null);    SQLiteDatabase db = mOpenDbHelper.getWritableDatabase;    int affected;    switch (sUriMatcher.match(uri)) {        case VIDEOS:            affected = db.update(VIDEO_TABLE_NAME, values,                    where, whereArgs);            break;        case VIDEO_ID:            String videoId = uri.getPathSegments.get(1);            affected = db.update(VIDEO_TABLE_NAME, values,                    FinchVideo.SimpleVideos._ID + /"=/" + videoId                            + (!TextUtils.isEmpty(where) ?                            /" AND (/" + where + /')/' : /"/"),                    whereArgs);            break;        default:            throw new IllegalArgumentException(/"Unknown URI /" + uri);    }    getContext.getContentResolver.notifyChange(uri, null);    return affected;}  

delete方法

delete方法和update方法类似,它会删除给定URI所指向的记录。和update方法类似,delete方法也是返回该操作所影响的记录数:


@Overridepublic int delete(Uri uri, String where, String whereArgs) {    int match = sUriMatcher.match(uri);    int affected;    switch (match) {        case VIDEOS:            affected = mDb.delete(VIDEO_TABLE_NAME,                    (!TextUtils.isEmpty(where) ?                            /" AND (/" + where + /')/' : /"/"),                    whereArgs);            break;        case VIDEO_ID:            long videoId = ContentUris.parseId(uri);            affected = mDb.delete(VIDEO_TABLE_NAME,                    FinchVideo.SimpleVideos._ID + /"=/" + videoId                            + (!TextUtils.isEmpty(where) ?                            /" AND (/" + where + /')/' : /"/"),                    whereArgs);            // the call to notify the uri after deletion is explicit            getContext.getContentResolver.                notifyChange(uri, null);            break;        default:            throw new IllegalArgumentException(/"unknown video element: /" +                uri);    }    return affected;} 

注意,前面介绍的只是简单内容提供者中所需的内容,更复杂的场景中可能涉及某个查询需要连接多张表,或者某个数据项会被级联删除。内容提供者可以通过Android的SQLite API自由选择自己的数据管理机制,只要它不破坏内容提供者的客户端API。

确定通知Observer的频繁度

正如我们在内容提供者数据管理操作列表中看到的,在Android内容管理系统中,通知不是免费的:SQLite表的插入操作不会自动替内容提供者设置发送消息通知,需要提供者的开发人员实现一种机制,确定发送通知的合适时间,决定当内容提供者的数据发生改变时,应该发送哪个URI。通常情况下,Android中的内容提供者会马上为在某个数据操作后发生变化的所有的URI发送通知。

当开发人员设计通知机制时,应该考虑如下的权衡:细粒度的通知机制会带来更精确的变化更新,这会降低用户接口系统的负载。如果在一个列表中,有一个元素发生了变化,如果该元素可见,则该列表可以只重绘该元素。但是细粒度的通知机制的缺点在于,在系统中需要发送更多的事件。由于UI会接收更多的通知事件,它有可能需要绘制更多次。粗粒度的通知在系统中发送的事件较少,但是这通常意味着UI每次接收到通知时,需要重新绘制的工作量更大。例如,一个列表在收到要求更新所有元素的事件时,可能实际上只有3个元素发生了变化。这里建议在选择通知机制时,要考虑到不同粒度的利弊。例如你可能想要等待读完大量的事件后,再发送“改变所有元素”的事件,而不是每接到一个事件就发送一次更新。

通常情况下,内容提供者只是通知和数据变化相关的URI的客户端。