概述
实际上,在上一篇 安卓学习日志 Day16 — 在应用中使用SQLite 当中有点问题,我们的初衷并不是在 Activity 的代码中直接调用 SQLite 数据库,因为这样很容易引入错误。比如,Activity 中有错别字,就会将无效输入插入到数据库中。
而这些问题都可以 通过 ContentProvider 来避免,下面就介绍 如何 Pets 应用中使用 ContentProvider 来管理数据。
ContentProvider
我们可以引入一个名为 ContentProvider 的概念作为数据库 和 Activity 之间的一个层。
使用 ContenProvider 有多方面的好处,比如可以使用它来确保输入的数据是有效的,不过要使用 ContentProvider 就不得不介绍 URI、UriMatcher 和 ContentResolver 这些不同的东西。
ContentProvider 会在当我们想要利用其它框架类从数据库加载数据到 UI 时,提供很多方面的帮助,它会使一切更顺利 与 其他框架类完美配合。
为何使用它?
ContentProvider 提供了三大好处:
-
提供了很好的抽象层
现在来看看 Pets 应用的情况,目前都是直接 在 Activity 中 实例化 PetDbHelper 对象,并通过该对象打开并 执行插入和读取操作。比如,可以直接访问 PetDbHelper,以将一个体重为 7 kg的宠物插入到数据库中,PetDbHelper 会帮我们直接将该宠物插入到数据库中,所以只要知道插入的信息是正确的,这个过程就能良好运行。
但是,万一我们打错了字(假如不小心在 Actvity 中 体重的值加了负号),这时就会将一个为 负 的体重值插入到数据库中,这显然是错误的。像这种 Activity 直接与 PetDbHelper 交互的方法,其缺陷就在于,它会将无效的数据直接插入到数据库当中。
而这就是 ContentProvider 发挥作用之处,我们可以通过 ContentProvider 集中化数据的访问和编辑。在这个模式中,我们的 UI 代码会直接 ContentProvider 交互,而不是直接与 PetDbHelper(数据库)交互。ContentProvider 作为一个数据验证层(可以看出我们确实需要它)会在我们错误输入无效的数据值时进行验证,所以,如果数据库存在任何错误,就会在这一步被捕捉到。
ContentProvider 作为数据源和 UI 代码之间的附加层(通常称为抽象层),这是因为 ContentProvider 会抽象化数据存储的方式 或隐藏数据存储的详情,所以 UI 代码在进入任何数据访问时,只需和 ContentProvider 进行通信,它无需关心 Provider 完成此任务的时间间隔。
ContentProvider 会以 UI 看不到的方式对底层数据进行暗箱处理,因为 UI 不关心数据是存储在数据库中还是存储在单个文件中,甚至可以存储照片文件,而 ContentProvider 可以完美处理与 UI 代码的交互以显示这些图片。所以,如果在应用的更新版本中想将数据库换做不同的存储类型 UI 代码将保持不变,并继续与现有的 ContenProvider 交互,即 除了数据库之外如果想要对每只宠物添加图片文件也是没有任何问题的,或者即使数据存储为文本文件,而非数据库及照片文件,ContentProvider 依旧能很好地加以处理。
UI 代码中能使用各种方法与 Provider 进行交互,在所以 CRUD 操作中 UI 代码会向 ContentProvider 调用方法,而 ContentProvider 也会向数据源调用其自身形式的代码。
总结一下就是,ContentProvider 可帮助我们管理对有结构的数据集的访问,它可以作为 UI 代码和 数据源直接很好的抽象层,在这个抽象层中可以添加数据验证,帮助我们修改数据存储的方式,而 UI 代码始终保持不变。
-
与其他框架类完美地配合工作
更大的好处是,它能与其他框架类完美结合,比如每当 添加或删除一个宠物时,我们都希望在主界面显示最新的信息,这就必须每次都调用
query()
方法以获取数据库最新的内容。那么取代这种繁琐工作的方法就是可以利用一个名为CursorLoader
的框架类,每当有宠物添加或删除时,宠物列表就会借助 CursorLoader 始终处于最新的状态,因为 CursorLoader 会在数据发生更改时自动进行检查,并在确定发生了数据变更后自动更新列表,CursorLoader 可以与 ListView 和 CursorAdapter 协作。而实现 CursorLoader 需要用到 ContentProvider ,所以 ContentProvider 和 CursorLoader 一起为我们省了很多工作,使我们不需要在发生数据更改时一次次手动执行查询并更新 UI。它还能与主屏幕小部件搭配使用,这个部件叫做
SyncAdapter
将数据同步到云并为应用提供搜索建议。假如团队想让 ContentProvider 以一致的方式管理对有结果数据集的访问,如果没有这个部件 就要自己执行大量的管理工作。 -
可以对其他应用分享数据
ContentProvider 还可以用于分享数据,当应用中存在文本数据或文件时,其他应用是无法访问的。不过可以使用 ContentProvider 将数据暴露给其他应用,这样其他应用也可以使用 ContentProvider 提供的接口 从而访问数据。并且 ContentProvider 会以安全的形式管理数据,使用获得 特定访问权限的其他应用才能访问数据
ContentProvider 原理
其中在 UI 代码和 ContentProvider 直接还要一层 ,它是 ContentResolver,下面以一个例子来解释,下图展示一个应用内部 使用 ContentProvider 管理数据的流程:
在这张流程图中,ContactEditorActivity 将使用 ContentProvider 以配合 Loader 来将数据库的数据加载到 UI 中以对我们的联系人进行编辑,那么 ContactEditorActivity 可以使用联系人的 ContentURI (这个 URI 为被访问数据的唯一标识,与 Web URI 的作用类似) 对 Resolver 调用方法,而 Resolver 会将该消息发送到对应的 Provider。这个 Provider 将向数据库发送请求,最终 Provider 会得到一些结果 ,这些 结果会被发送会 Resolver,Resolver 又将这些结果返回到 Activity 并最终显示到 UI 中。
新建 PetProvier
通过以上对 ContentProvider 的描述我们可以画出 Pets 应用中使用 ContentProvider 的基本流程:
而实际实现时,则需要使用自定义的 PetProvider(继承自 ContentProvider),因为 ContentProvider 是一个抽象类。PetProvider 应作为 com.example.pets.data
java包中一个新的 Java 文件,继承自 ContentProvider,因此它需要实现五个方法 insert
、query
、update
、delete
、 getType
和 onCreate
,前四个方法分别对应 数据库操作中的 CRUD,并且需要一个全局的 PetDbHelper 对应用于访问 数据库(在 onCreate 时初始化),PetProvider 的定义如下:
package com.example.pets.data;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class PetProvider extends ContentProvider {
public static final String LOG_TAG = PetContract.class.getSimpleName();
/**
* Database helper object
*/
private PetDbHelper mDbHelper;
@Override
public boolean onCreate() {
mDbHelper = new PetDbHelper(getContext());
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
最后还需要在应用清单文件 AndroidManifest.xml
中声明这个 Provider:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.pets">
………………
<provider
android:name=".data.PetProvider"
android:authorities="com.example.pets"
android:exported="false" />
</application>
</manifest>
这里的 authorities
属性表示 内容主机名(数据库在哪),name
属性表示定义 Provider 的 Java 类,<provider>
标签会将 主机名 和 Provider 关联起来。
更改完成后确保应用依然能够正常运行(但不会有任何改变),代码更改前后差异对比
PetProvider 操作
现在回过头来让我们看看 PetProvider 中除 onCreate 外的五个方法,它有一个共同点:至少且必须要 接受一个 URI 对象,下面以 query()
方法为例进行解释。
这是因为 Activity 要使用 query()
方法调用 ContentResolver ,为了帮助 Resolver 确定最终使用哪个 Provider,则需要为 query()
方法传递一个 URI 对象,这个 URI 对象指定了要访问的资源,也就是被操作数据的所在位置。
接下来 Resolver 从这个 query()
方法获得信息后,它会使用相同的 query()
方法调用合适的 Provider(在 Pets 应用中为 PetProvider),这时候 PetProvider 的 query()
方法会将传入的 参数(projection 等)转换为 SQL 语句从数据库中执行操作 并获得一个包含查询结果的 Cursor 对象,这个 Cursor 对象最终会返回至调用了 query()
方法的 Activity。
这里可能或感觉有点混乱,因为 Activity、Resolver 和 Provider 都有各自的 insert
、query
、update
、delete
方法,只是各自接受的参数不同而已,类似下表:
Activity | Resolver | Provider | |
---|---|---|---|
query | query(Uri) | query(Uri) | query(……),返回含查询结果 Cursor |
insert | insert(Uri, ContentValues) | insert(Uri, ContentValues) | insert(……),返回指向插入数据的 URI |
update | update(Uri, ContentValues) | update(Uri, ContentValues) | update(……),返回被更新行的编号 int |
delete | delete(Uri) | delete(Uri) | delete(……),返回被删除行的编号 int |
所以 数据操作请求 是从 Activity 发起的,最终由 Provider 执行过后会将 执行结果 返回给 发起请求的 Activity。
Content URI
设计宠物 Content URI
在与 Provider 交流时需要将正确的 Uri 作为方法的输入参数,这是因为我们需要让 Provider 知道被访问或修改的数据是什么。
在与 Provider 交流时基本需要告诉它两件事:
- 执行什么操作(
insert
、query
、update
、delete
) - 被操作的数据(整个数据表 或者 表中的某行)
其中被操作的数据就要使用 Uri 来定义。
URI 全称 Uniform Resource Identifier(代表统一资源标识符),正如名称所指 它可以标识出我们 要感兴趣资源的名称、位置 (或有时同时标识名称和位置),也就是标识被操作的数据在哪里,一个 Uri 大概像这样 content://com.android.contacts/contacts
。
这里可能会让人想到 URL(统一资源定位符),URL 是 URI 的子集,它用于定位 某个文件或数据在网站上的具体位置,如 https://github.com/HEY-BLOOD
。
而现在 Pets 应用中,将使用 URI 来标识一些数据的位置,这个位置为手机上的一个类似 SQL 的数据库文件。
而我们要使用的是 ContentUri,ContentUri 主要用与标识 Provider 中的数据,它可以指向数据库的某个部分(单个行、单个表或一组表)。它也可以指向文件,,比如文本文件、照片或其他媒体文件,下面是三个应用中针对不同 Provider 的 ContentUri 示例:
Contacts Provider | Calendar Provider | User Dictionary Provider |
---|---|---|
content://com.android.contacts/contacts | content://com.androidcalendar/events | content://user_dictionary/words |
可以看出 所有 的 ContentUri 以 content://
作为开头,这叫做 Scheme,是 ContentUri 结构中的一部分,完整结构如下:
-
Scheme: 是 Android 应用中 URI 的标准开头
-
Content Authority:也称作 内容主机名,指定要使用的 ContentProvider,必须与应用清单文件中
<provider>
标签的authority
相匹配。当中一个 URI 使用与 应用清单文件
<provider>
中相匹配的主机名时,就会使用<provider>
标签中name
属性对应的 Provider 类(其实,就是指向了一个数据库) -
Type of data:指定了要执行操作的数据,一个常用的模式是 将这部分作为 表名,
/contacts
即表示访问整个数据表。
为了帮助理解,下面列出几个 来自 Contacts Provider 中不同表的几个 URI 示例:
- content://com.android.contacts/contacts
- content://com.android.contacts/profile
- content://com.android.contacts/photo
- content://com.android.contacts/diretories
这几个 URI 都是从同一个 Contacts Provider 中进行调用,因为它们使用了同样的 内容主机名(来自同一数据库),但在结尾列出的表名不同,所有它们可能分别访问了 contacts 表、profile 表、photo 表 或 directories 表,这些表处于同一个数据库中。
那么在 Pets 应用中应该是什么样的呢?以及如何使用 URI 标识表中单个行的数据呢?
-
访问 Pets 应用中整个 pets 数据表使用
content://com.example.pets/pets
其中,
com.example.pets
为内容主机名,最后的/pets
表示 pets 数据表 -
访问表中的单行数据库可以在表名得到后再跟上数字,这个数字为 被访问行的 ID编号,它们看起来可能像这样:
content://com.android.contacts/contacts/1
content://com.android.contacts/contacts/2
content://com.android.contacts/contacts/10
这三个 URI 分别指向联系人应用中 contacts 数据表的第 1、2、10行的数据。
如果在 宠物应用中查询 pets 表中的所有记录的 URI 为 content://com.example.pets/pets
假如要更新 id 为 5 的这行数据,则 URI 为 content://com.example.pets/pets/5
最后强调,Android 应用中的 URI 一定要以 content://
作为标准开头。然后是类似 com.example.pets
的内容主机名,它指定了要使用的 Content Provider,这些是在 应用的清单文件的 <provider>
标签中定义的。最后由 /pets/5
指定了要执行操作的数据(可以是整个表 或表中的单个行)
使用哪个 Content URI
我们为宠物应用设计了两种 URI,访问整个表的 content://com.example.pets/pets
和表中单个行的 content://com.example.pets/pets/5
。
在宠物应用的 CatalogActivity 中我们希望显示 所有的宠物列表,这意味着我们需要查询整个表,则使用以表名 /pets
结尾的 URI。
那么假设要 在 EditorActivity 中显示表中已经存在的某个宠物的信息,就需要从表中查询单个行,即使用含 id 编号的 /pets/5
的 URI,它指向 要显示的宠物信息所在的行。
这里不妨整理一下,列出 CatalogActivity 和 EditorActivity 中所有可能执行的数据操作,并从 A、B 两个选项中选择合适的 URI 类型。
选项A: content://com.example.pets/pets
选项B: content://com.example.pets/pets/1
在 EditorActivity 中:
-
更新表中 id 为 1 的宠物信息?
答案:B,因为需要从表中找到 id 为 1 的行,才能对已有的数据进行更新。
-
删除表中 id 为 1 的宠物信息?
答案:B,先从表中找到 id 为 1 的行,才能对已有的数据进行删除。
-
添加一条新的宠物信息?
答案:A,插入一行新的数据并不需要访问已存在的某行,只需要访问 数据表就足够了。
在 CatalogActivity 中:
-
添加一只虚拟的宠物信息?
答案:A,插入一行新的数据并不需要访问已存在的某行,只需要访问 数据表就足够了。
-
删除所有的宠物信息?
答案:A,删除所有数据是针对整个数据表的操作,所以选 A。
向 Contract 添加 URI
现在是时候在 Pets 应用中把我们 设计的 URI 使用上了,前边写那么多是因为 设计和使用正确的 URI 对我们从表中获取所需的信息非常重要。现在,来看看如何向 PetContract.java
代码添加 URI。
还记得 URI 的 3 个部分吗?scheme(标准开头)、Content Authority (内容主机名) 和 Type of data(数据类型)。
以 content://com.example.pets/pets/2
为例,由于其中某些成分是可重复使用的,不会发生变化,我们可以将它们作为常数。
那么现在的问题是存储这些常数的最佳地方是哪里。记得之间将与数据相关的所有常数都存储在了 Contract 类中,所以这也是存储 URI 常数信息的一个理想选择。
首先来看看之前在 AndroidManifest 标签中设置的 ContentProvider 的内容主机名(Content Authority):
<provider
android:name=”.data.PetProvider”
android:authorities=”com.example.pets”
android:exported=”false” />
在 PetContract.java
中,我们将它设置为一个字符串常数,它的值和 AndroidManifest 中的一样:
public static final String CONTENT_AUTHORITY = "com.example.pets";
接下来,将 CONTENT_AUTHORITY
常数与 scheme标准开头 content://
连接起来,我们将创建常量 BASE_CONTENT_URI
作为基本内容 URI,它将由与 PetsProvider 关联的每一个 URI 共用:
"content://" + CONTENT_AUTHORITY
要使这个 URI 有用,我们将使用 Uri 类的 parse()
方法,它将 URI 字符串作为输入,然后返回一个 URI 类型的对象。
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
然后是表名 pets
,此常数存储位置将会被附加到基本内容 URI 的每个表的路径。
public static final String PATH_PETS = "pets";
最后,在 contract 中的每个 Entry 类中,我们为其创建一个完整的 URI 作为常数 CONTENT_URI
。
Uri.withAppendedPath()
方法将 BASE_CONTENT_URI
(包含 标准开头 和内容主机名)附加到 。
public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, PATH_PETS);
在添加了这些常数后,PetContract.java 类看起来将是