一、关于ContentProvider
ContentProvider提供了一种跨应用访问数据的方式,它在UI与数据库之间添加一个抽象层,隐藏了数据存储的细节,对于UI来说,直接与ContentProvider交互即可,即使之后内部的存储方式改变也不影响UI层的代码。UI通过对应的Uri访问本应用或其他应用的数据,例如很多应用都会访问本地通讯录,这是通过访问系统的CallLog.Calls.CONTENT_URI
这个Uri来获取数据。
每个ContentProvider都有一个唯一的Uri,只有传递与当前ContentProvider对应的Uri才能访问到对应的数据。Uri的格式如下:
content://<authority>/<type of data>/<id>
<authority>由每个ContentProvider在AndroidManifest文件中指定,该值具有唯一性,如果两个应用配置了相同的authority,它们无法安装到同一台手机上;<type of data>在访问数据库的情况下为表名;<id>为可选,如果没有<id>表示对整个表进行操作,如果包含<id>,表示对单条记录操作,可用于实现单条记录的删除和更新。
UI与ContentProvider的交互如下,用户可以在UI中调用ContentResolver的方法并传递Uri实现对特定ContentProvider的CRUD(增删改查)操作。因此UI实际上是ContentResolver进行直接交互的,ContentResolver根据Uri找到对应ContentProvider,得到数据后返回给UI层。
UI <-> ContentResolver <-> ContentProvider
本文主要讲解自定义ContentProvider存取本应用的数据,如果用户需要实现对本应用中数据的CRUD操作,可以自定义一个继承自ContentProvider的类并重写其中的方法。
public class MyContentProvider extends ContentProvider {
//...
}
随后在AndroidManifest中的<application>下注册该ContentProvider 。其中authorities由用户自己指定,只要是唯一的即可;name必须是用户定义的继承自ContentProvider的类;exported表示数据是否对外部应用开放,设置为false表示只有本应用能访问该数据。
<provider
android:authorities="com.lister.historysave.provider"
android:name=".MyContentProvider"
android:exported="false"/>
根据之前所说的Uri格式,如果用户要通过MyContentProvider操作数据,传入的Uri应该为:
content://com.lister.historysave.provider/数据表名
每次MyContentProvider接收到Uri时,都会检查Uri是否符合规范,如果符合则执行对应操作,如果不符合可以抛出异常。
二、功能分析与数据库表
本文要通过ContentProvider实现对访问记录的管理,试想这样一个场景,用户在应用中访问网页或者新闻,系统将用户每次的访问记录和访问时间保存到数据库中,随后用户可以查看历史记录,也可以选择清空记录。根据需求,可以定义一张数据库表history如下
history(
_id INTEGER PRIMARY KEY AUTOINCREMENT,
record TEXT NOT NULL,
visited_time INTEGER NOT NULL
)
_id为自增的主键,表示当前是第几条记录。由于SQLite中不存在表示时间的数据格式,这里使用visited_time记录毫秒数来表示访问时间。在查询访问记录时,只需要全部查询并按_id倒序排列即可得到时间上由近到远的所有访问记录。
这里存在一个问题,如果用户访问了已经存在的记录呢,是继续添加一条相同的记录(访问时间不同)?还是先将之前的记录删除再添加?这里选择将之前的记录删除并添加一条新的记录,当然也可以更新之前记录的访问时间,不过这样做的话,在查询时得到的记录并不是按访问顺序排列的。
三、具体实现
下面根据功能进行具体实现,由于需要对数据库表操作,首先需要SQLiteOpenHelper新建数据库还有表,随后自定义ContentProvider实现对数据的CRUD操作,最后在UI层通过ContentResolver访问数据。
3.1 新建数据库及表
public class HistoryDBHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "history.db";
private static final int VERSION = 1;
public HistoryDBHelper(@Nullable Context context) {
super(context, DATABASE_NAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String create_database = "create table if not exists " + HistoryConstant.TABLE_NAME + " ("
+ HistoryConstant._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ HistoryConstant.RECORD + " TEXT NOT NULL, "
+ HistoryConstant.VISITED_TIME + " INTEGER NOT NULL)";
db.execSQL(create_database);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 在数据库更新时调用, 如增加列时
}
public class HistoryConstant {
public static final String TABLE_NAME = "history";
public static final String _ID = "_id";
public static final String RECORD = "record";
public static final String VISITED_TIME = "visited_time";
}
}
3.2 自定义ContentProvider
public class HistoryProvider extends ContentProvider {
public static final String CONTENT_AUTHORITY = "com.lister.historysave.provider";
public static final String CONTENT_PATH = "history";
/**
* Uri 匹配 Code
*/
private static final int CODE_HISTORY = 100;
private static final UriMatcher sUriMatcher;
private HistoryDBHelper mHistoryDBHelper;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(CONTENT_AUTHORITY, CONTENT_PATH, CODE_HISTORY);
}
@Override
public boolean onCreate() {
mHistoryDBHelper = new HistoryDBHelper(getContext());
return true;
}
可以看到在HistoryProvider中首先定义Uri的匹配规则,将所有能接收的Uri的格式通过addURI方法添加到sUriMatcher中,该流程在static静态代码块中执行,其中的代码会随着类的加载执行并只执行一次。
Uri每个匹配规则对应一个code,当下方的CRUD方法接收到Uri时会放入sUriMatcher进行匹配,并返回之前定义的code。如果Uri不匹配则抛出异常。
@Nullable
@Override
public Cursor query(@NonNull Uri uri, String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder) {
SQLiteDatabase database = mHistoryDBHelper.getReadableDatabase();
int match = sUriMatcher.match(uri);
if (match == CODE_HISTORY) {
return database.query(HistoryDBHelper.HistoryConstant.TABLE_NAME,
projection, selection, selectionArgs, null, null, sortOrder);
} else {
throw new IllegalArgumentException("incorrect uri: " + uri);
}
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
SQLiteDatabase database = mHistoryDBHelper.getWritableDatabase();
int match = sUriMatcher.match(uri);
if (match == CODE_HISTORY) {
long id = database.insert(HistoryDBHelper.HistoryConstant.TABLE_NAME,
null, values);
return ContentUris.withAppendedId(uri, id);
} else {
throw new IllegalArgumentException("incorrect uri: " + uri);
}
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
SQLiteDatabase database = mHistoryDBHelper.getWritableDatabase();
int match = sUriMatcher.match(uri);
if (match == CODE_HISTORY) {
return database.delete(HistoryDBHelper.HistoryConstant.TABLE_NAME,
selection, selectionArgs);
} else {
throw new IllegalArgumentException("incorrect uri: " + uri);
}
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
@Nullable String[] selectionArgs) {
return 0;
}
}
最后别忘了在AndroidManifest中添加:
<provider
android:authorities="com.lister.historysave.provider"
android:name=".HistoryProvider"
android:exported="false"/>
到这里我们就可以在UI层通过ContentResolver来访问数据了,例如要查询所有历史访问记录时代码如下。
Uri uri = Uri.parse("content://com.lister.historysave.provider/history");
ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.query(uri, null, null, null, null);
while (cursor.moveToNext()) {
// 取出 cursor 中的记录
}
cursor.close();
虽然功能可以实现,但是不免有些繁琐,整个UI层也会变得臃肿。尤其是在新增记录时,还要判断之前是否存在。仔细想想,我们对数据库表的操作可以分为以下三个功能:1. 添加一条记录;2. 查询所有记录;3. 清空记录。因此我们可以将这三个功能封装为三个方法放在一个工具类里,在UI层调用时只需要一句话,整体的代码就会非常简洁,也提高了可读性。封装如下。
3.3 HistoryUtils 工具类封装
/**
* 本类用于维护历史记录, 将对历史记录的操作封装
* 对外暴露新增记录、获取全部记录、清空记录 3 个方法
*/
public class HistoryUtils {
private Uri mUri;
private ContentResolver mContentResolver;
public HistoryUtils(Context context) {
mUri = Uri.parse("content://"
+ HistoryProvider.CONTENT_AUTHORITY + "/" + HistoryProvider.CONTENT_PATH);
mContentResolver = context.getContentResolver();
}
/**
* 根据 record 和系统时间存入记录
* 如果之前存在该记录, 则删除之前的记录
*/
public void saveHistory(String record) {
// 如果之前存在对应记录, 则需要先将对应记录删除
String selection = HistoryDBHelper.HistoryConstant.RECORD + " = ?"; // where 子句中的列
String[] selectionArgs = {record}; // where 子句中的值
Cursor cursor = mContentResolver.query(mUri, null, selection, selectionArgs, null);
if (cursor != null && cursor.getCount() > 0) {
mContentResolver.delete(mUri, selection, selectionArgs);
}
if (cursor != null) cursor.close();
// 保存至数据库
ContentValues values = new ContentValues();
values.put(HistoryDBHelper.HistoryConstant.RECORD, record);
values.put(HistoryDBHelper.HistoryConstant.VISITED_TIME, System.currentTimeMillis());
mContentResolver.insert(mUri, values);
}
/**
* 获取所有记录
* 返回 List<HistoryEntity>
*/
public List<HistoryEntity> getAllHistory() {
List<HistoryEntity> list = new ArrayList<>();
Cursor cursor = mContentResolver.query(mUri,
null, null, null, HistoryDBHelper.HistoryConstant._ID + " desc");
if (cursor != null) {
while (cursor.moveToNext()) {
list.add(new HistoryEntity(cursor.getInt(0),
cursor.getString(1), cursor.getInt(2)));
}
cursor.close();
}
return list;
}
/**
* 清空记录
*/
public void clearHistory() {
mContentResolver.delete(mUri, null, null);
}
/**
* 历史记录实体类
*/
public class HistoryEntity {
public int _id;
public String record;
public int visited_time;
public HistoryEntity(int _id, String record, int visited_time) {
this._id = _id;
this.record = record;
this.visited_time = visited_time;
}
@Override
public String toString() {
return String.valueOf(_id) + ", " + record + ", " + visited_time;
}
}
}
代码中注释比较详尽,就不多说了。
四、CursorLoader
上面讲了怎么自定义ContentProvider来存取历史记录,一般情况下历史记录都是保存在列表例如ListView或RecyclerView中,然后在历史记录发生变化时进行更新。
而CursorLoader为我们提供了这样一种方式,CursorLoader是一个继承自AsyncTaskLoader的异步任务类,当数据发生变化时它在后台线程开始查询数据,结束之后得到Cursor给Adapter更新数据。为了让CursorLoader知道数据发生了变化,我们需要在操作数据后通知相应的Uri。大致的流程如下:
数据更新 -> 通知uri -> doInbackground后台查询 -> onPostExecute得到Cursor -> 在列表中更新数据
我们通过ListView来展示历史记录,首先定义ListView的适配器,注意该Adapter继承自CursorAdapter。
public class HistoryAdapter extends CursorAdapter {
public HistoryAdapter(Context context, Cursor c) {
super(context, c, 0);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return LayoutInflater.from(context).inflate(R.layout.item_history, parent, false);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
// ......
}
}
由于ListView在MainActivity中,因此在MainActivity中初始化ListView并为其设置Adapter。同时MainActivity需要实现LoaderManager.LoaderCallbacks<Cursor>接口用于异步回调,后面的泛型表示回调之后返回的数据类型。
public class MainActivity extends AppCompatActivity
implements LoaderManager.LoaderCallbacks<Cursor> {
@BindView(R.id.rv_history) ListView mListHistory;
private static final int HISTORY_LOADER = 1;
private HistoryAdapter mHistoryAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
mHistoryAdapter = new HistoryAdapter(this, null);
mListHistory.setAdapter(mHistoryAdapter);
getLoaderManager().initLoader(HISTORY_LOADER, null, this);
}
// ......
该接口重写的3个方法如下。在onCreateLoader方法中新建一个一个CursorLoader用于监听数据变化,传入的参数代表要查询的Uri和条件;查询结束之后onLoadFinished方法会被调用,其中的Cursor data就是查询的结果,将其设置到适配器Adapter中即可;onLoaderReset在当前 Loader 被销毁,或者最新的 Cursor 数据无效时调用,最终清空列表。
@Override
public android.content.Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri = Uri.parse("content://" + HistoryProvider.CONTENT_AUTHORITY
+ "/" + HistoryProvider.CONTENT_PATH);
String[] projection = {
HistoryDBHelper.HistoryConstant._ID,
HistoryDBHelper.HistoryConstant.RECORD,
HistoryDBHelper.HistoryConstant.VISITED_TIME };
return new CursorLoader(this, uri, projection,
null, null, "_id desc");
}
@Override
public void onLoadFinished(android.content.Loader<Cursor> loader, Cursor data) {
mHistoryAdapter.swapCursor(data);
}
@Override
public void onLoaderReset(android.content.Loader<Cursor> loader) {
mHistoryAdapter.swapCursor(null);
}
最后在ContentProvider中的query方法设置要通知的uri。
cursor.setNotificationUri(getContext().getContentResolver(), uri);
并在insert, delete, update 方法中,每次数据发生变化时进行通知即可。
getContext().getContentResolver().notifyChange(uri, null);
网友评论