Android四大组件之Content Provider
Content Provider是Android提供的第三方应用数据的访问方案。在Android中,对数据的保护是很严密的,除了放在SD卡中的数据,一个应用所持有的数据库、文件等内容,都是不允许其它程序直接访问的。Android当然不会真的把每个应用都做成一座孤岛,它为所有应用都准备了一扇窗,这就是Content Provider。应用向对外提供的数据,可以通过派生Content Provider类,封装成一枚Content Provider,每个Content Provider都用一个uri作为独立的标识,形如:content://com.xxxx。
所有东西看着像REST的样子,但实际上,它比REST更为灵活。和REST类似,uri也可以有两种类型,一种是带id的,另一种是列表的,但实现者不需要按照这个模式来做,给你id的uri你也可以返回列表类型的数据,只要调用者明白就无妨,不用苛求所谓的REST。
另外,Content Provider不和REST一样只有uri可用,它还可以接受Projection、Selection、OrderBy等参数,这样,就可以像数据库那样进行投影、选择和排序。查询到的结果,以Cursor的形式进行返回,调用者可以移动Cursor来访问各列的数据。
Content Provider屏蔽了内部数据的存储细节,向外提供了上述的统一接口模型,这样的抽象层次,大大简化了上层应用的编写,也对数据的整合提供了更方便的途径。Content Provider内部常用数据库来实现,Android提供了强大的Sqlite支持,但很多时候你也可以封装文件或其它混合的数据。
在Android中,ContentResolver是用来发起Content Provider的定位和访问的。但通常,Content Provider需要访问的可能是数据库等大数据源,效率上不足够快,会导致调用线程的拥塞。因此Android提供了一个AsyncQueryHandler,帮助进行异步访问Content Provider。
在各大组件中,Service和Content Provider都是需要持续访问的。Service如果是一个耗时的场景,往往会提供异步访问的接口,而Content Provider不论效率如何,都提供的是约定的同步访问接口。
下面提供两个实例------一个是ContentProvider所在的应用,一个是使用ContentProvider的应用,来说明如何使用ContentProvider。
我们先新建一个工程MyContentProvider,在这个工程中我们要实现ContentProvider,项目结构如下图所示。
MyContentProvider目录结构
在MyUser.java中,MyUser这个类主要用来存放一些常量,比如这个ContentProvider的Uri以及使用的数据库表的列,如下所示:
public class MyUser {
public static final String AUTHORITY = "com.veione.MyContentProvider";
public static final class User implements BaseColumns {
// 定义Uri
public static final Uri CONTENT_URI = Uri.parse("content://"+ AUTHORITY);
// 定义数据表列
public static final String USER_NAME = "USER_NAME";
}
}
接下来我们在MyContentProvider.java中实现这个Content Provider,在MyContentProvider中我们主要构造了一个继承于SQLiteOpenHelper的类DatabaseHelper,然后在这个SQLiteOpenHelper类中实现数据的增、删、改、查,如下所示:
/**
* MyContentProvider继承ContentProvider类,实现其insert,update,delete,
* getType,onCreate等方法
*
* @author veione
*
*/
public class MyContentProvider extends ContentProvider {
//定义一个SQLiteDatabase变量
private SQLiteDatabase sqlDB;
//定义一个DatabaseHelper变量
private DatabaseHelper dbHelper;
//数据库名
private static final String DATABASE_NAME="Users.db";
//数据库版本
private static final int DATABASE_VERSION=1;
//表名
private static final String TABLE_NAME="User";
/**
* 定义一个内部类
* 这个内部类继承了SQLiteOpenHelper类,重写其方法
*/
public static class DatabaseHelper extends SQLiteOpenHelper{
//构造方法
public DatabaseHelper(Context context) {
//父类构造方法
super(context,DATABASE_NAME,null,DATABASE_VERSION);
}
//当第一次创建数据库的时候调用该方法,可以为数据库增加一些表和初始化一些数据
@Override
public void onCreate(SQLiteDatabase db) {
//在数据库里生成一张表
db.execSQL("CREATE TABLE "+TABLE_NAME
+" (_id INTEGER PRIMARY KEY AUTOINCREMENT,USER_NAME TEXT);");
}
//当更新数据库版本的时候,调用该方法。可以删除、修改表的一些信息
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS "+TABLE_NAME);
onCreate(db);
}
}
//这是一个回调函数,当生成所在类的对象时,这个方法被调用,创建一个数据库
@Override
public boolean onCreate() {
dbHelper=new DatabaseHelper(getContext());
return (dbHelper==null)?false:true;
}
//查询
@Override
public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) {
//新建数据库查询类
SQLiteQueryBuilder qb=new SQLiteQueryBuilder();
SQLiteDatabase db=dbHelper.getReadableDatabase();
qb.setTables(TABLE_NAME);
//取得查询结果的游标
Cursor c=qb.query(db, projection,selection, null, null, null,sortOrder);
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
//取得类型
@Override
public String getType(Uri uri) {
return null;
}
//插入数据
@Override
public Uri insert(Uri uri, ContentValues values) {
sqlDB=dbHelper.getWritableDatabase();
long rowId=sqlDB.insert(TABLE_NAME, "", values);
if(rowId>0){
Uri rowUri=ContentUris.appendId(MyUser.User.CONTENT_URI.buildUpon(), rowId).build();
//通知更改
getContext().getContentResolver().notifyChange(rowUri, null);
return rowUri;
}
throw new SQLException("Failed to insert row into "+uri);
}
//删除数据
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
//更新数据
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return 0;
}
}
另外我们新建一个MyContentProvider的界面,代码如下所示,在这个界面中简单地往数据库中插入两条记录Test和Guo作为实例数据,主要是验证客户端中是否能正确读取到这些数据。插入数据之后,我们还通过Toast的方式将数据库中的所有数据显示一遍,表明我们已经将数据正确存储到了数据库中。
public class MyContentActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 插入两条记录
insertRecord("Test");
insertRecord("Guo");
// 显示记录
displayRecord();
}
private void insertRecord(String userName) {
ContentValues values=new ContentValues();
values.put(MyUser.User.USER_NAME, userName);
getContentResolver().insert(MyUser.User.CONTENT_URI, values);
}
private void displayRecord() {
//构建一个字符串数组用于存放用户的记录
String[] columns=new String[]{MyUser.User._ID,MyUser.User.USER_NAME};
//设定ContentProvider的Uri
Uri myUri=MyUser.User.CONTENT_URI;
Cursor cur=getContentResolver().query(myUri, columns, null, null, null);
if(cur.moveToFirst()){
String id=null;
String userName=null;
do{
id=cur.getString(cur.getColumnIndex(MyUser.User._ID));
userName=cur.getString(cur.getColumnIndex(MyUser.User.USER_NAME));
//显示数据表中的数据
Toast.makeText(this, id+" "+userName, Toast.LENGTH_SHORT).show();
}while(cur.moveToNext());
}
}
}
这里我们没有用到任何的布局文件,因此我们直接来看看AndroidManifest.xml,如下所示。与其它AndroidManifest.xml不同点主要在于加了声明provider的部分,如代码所示。核心的两个属性分别是android:name和android:authorities。
运行之后效果如图所示,显示了添加的用户Test和Guo。
MyContentProvider运行效果图
接下来我们新建另外一个程序MyContentClient,在这个程序中使用我们刚才建立的ContentProvider,对该数据表进行插入和查询操作。
如下所示,是MyContentClientActivity的源代码,代码中关键的地方在于要正确设置访问的Uri地址,如图所示。这个Uri是由"content://"加上我们在ContentProvider定义的authority组成。然后就是使用getContentResolver()函数得到数据表的所有数据,并保存到一个游标变量中。接着,通过移动这个游标我们可以得到数据表中的所有数据,并将它们保存到一个StringBuffer中,最后将它们输出到一个TextView控件中。
同时为了试验插入数据的功能,我们创建了一个EditText的文本输入框,在文本框中输入信息,并单击Button,可以将数据写入到数据库中。数据写入过程也要采用特有的格式:新建一个ContentResolver对象和一个ContentValue的对象,再往这个ContentValue对象中输入数据,最后通过这个ContentResolver对象将数据插入到指定的ContentProvider中。当然,这里没有做一个可以实时更新的界面,不过没关系,重新打开程序,就可以在TextView控件里看到刚才输入的数据。
package com.veione.mycontentprovider;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
public class MyContentClientActivity extends Activity{
public static final String AUTHORITY="com.veione.MyContentProvider";
private Button insertBtn=null;
//访问ContentProvider的Uri
Uri CONTENT_URI=Uri.parse("content://"+AUTHORITY);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView show=(TextView)findViewById(R.id.show);
StringBuffer sb=new StringBuffer("");
//得到ContentProvider对应表的所有数据,以游标格式保存
Cursor c=getContentResolver().query(CONTENT_URI, new String[]{"_id","USER_NAME"}, null, null, null);
//循环输出ContentProvider的数据
if(c.moveToFirst()){
String _id=null;
String userName=null;
do{
_id=c.getString(c.getColumnIndex("USER_NAME"));
userName=c.getString(c.getColumnIndex("USER_NAME"));
sb.append("_id="+_id+",userName="+userName+"\n");
}while(c.moveToNext());
}
show.setText(sb);
//根据Id得到控件对象
insertBtn=(Button)findViewById(R.id.insertBtn);
//给按钮添加监听器
insertBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//得到EditText输入的数据
String username=((EditText)findViewById(R.id.sername)).getText().toString();
//生成一个ContentResolver对象
ContentResolver cr=getContentResolver();
//生成一个ContentValues对象
ContentValues values=new ContentValues();
values.put("USER_NAME", username);
//插入数据
cr.insert(CONTENT_URI, values);
}
});
}
}
activity_main.xml布局文件如下: