美文网首页
Android—使用ContentProvider实现访问记录的

Android—使用ContentProvider实现访问记录的

作者: 东方未曦 | 来源:发表于2019-05-27 23:46 被阅读0次

    一、关于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);
    

    相关文章

      网友评论

          本文标题:Android—使用ContentProvider实现访问记录的

          本文链接:https://www.haomeiwen.com/subject/alkqtctx.html