原文链接: https://loshine.me/2016/08/25/a-universal-solution-of-recyclerview-adapter-notify/
在之前我们用 ListView 或者 GridView 的时候,通知适配器刷新是这样的:
adapter.notifyDataSetChanged();
但是当我们使用了更强大的 RecyclerView 之后,如果直接这样通知适配器刷新将不会显示动画效果。它会直接将所有的 item 重新绘制。
我们需要使用如下的方法来通知适配器刷新,这样 RecyclerView 才会显示对应的动画效果:
adapter.notifyItemInserted();
adapter.notifyItemChanged();
adapter.notifyItemMoved();
adapter.notifyItemRemoved();
adapter.notifyItemRangeChanged();
adapter.notifyItemRangeInserted();
adapter.notifyItemRangeRemoved();
在这次更新的 Support Library 24.2.0 中添加了一个新的工具类,可以用来方便快捷的处理 RecyclerView.Adapter 的通知刷新。
DiffUtil
DifUtil 就是这次引入的工具类,它会找出 Adapter 中每一个 Item 对应发生的变化,然后对每一个变化给予对应的刷新。
最重要的就是如下的两个重载方法
DifUtil.calculateDiff(Callback cb, boolean detectMoves);
DifUtil.calculateDiff(Callback cb);
其中DifUtil.calculateDiff(Callback cb);实际上就是DifUtil.calculateDiff(callback, true);所以我们着重研究第一个方法即可。
该方法会接收两个参数,其中第二个参数是一个 boolean 值,查看源码注释我们知道这个参数有如下作用:
True if DiffUtil should try to detect moved items, false otherwise.
如果 DiffUtil 尝试检测移动的项目就设为 true,否则设为 false。
这个参数实际上是指定是否需要项目移动的检测,如果设为 false ,那么一个项目移动了会先判定为 remove,再判定为 insert。
而Callback是一个抽象类,它有四个方法需要实现:
public abstract static class Callback {
/**
* 旧的数据源的大小
*/
public abstract int getOldListSize();
/**
* 新的数据源的大小
*/
public abstract int getNewListSize();
/**
* 该方法用于判断两个 Object 是否是相同的 Item,比如有唯一标识的时候应该比较唯一标识是否相等
*/
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
/**
* 当 areItemsTheSame 返回 true 时调用该方法,返回显示的 Item 的内容是否一致
*/
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
}
如上所述,我们四个需要实现的方法的作用都在注释中写出了。前两个方法都很好理解,需要重点说明的是后两个
areItemsTheSame:这个方法用来判断两个 Object 是否是相同的 Item,此处最好不要简单的用equals方法判断,我们可以根据 Object 的唯一标识或者自己指定一个规则来判断两个 Object 是否是展示的相同的 Item。
areContentsTheSame:该方法只有在areItemsTheSame返回true之后才会被调用,我们在重写该方法的时候,只需要判断两个 Object 显示的元素是否一致即可。如我们有两个 Object,它们可能拥有很多属性,但是其中只有两个属性需要被显示出来,那只要这两个属性一致我们这个方法就要返回true。
使用 DiffUtils 通知刷新
下面我们写一个简单的例子来学习使用 DiffUtil
首先我们来一个 Item 对应的数据类:
public class Student {
public String id; // 学号是唯一的
public String name; // 名字可能重复
public Student(String id, String name) {
this.id = id;
this.name = name;
}
}
然后写一个 Adapter:
class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private final List<Student> datas;
public MyAdapter(List<Student> datas) {
this.datas = datas;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_recycler, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setData(datas.get(position));
}
@Override
public int getItemCount() {
return datas.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(View itemView) {
super(itemView);
}
public void setData(Student student) {
TextView textView = (TextView) this.itemView.findViewById(R.id.text);
textView.setText(student.name);
}
}
}
其对应的布局文件就是一个简单的 TextView:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
tools:text="content"/>
然后我们在 Activity 里使用它们并显示出来:
class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
mRandom = new Random();
datas = new ArrayList<>();
for (int i = 0; i < 10; i++) {
datas.add(new Student(mRandom.nextInt(3000) + "", "Students: " + i));
}
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new MyAdapter(datas);
mRecyclerView.setAdapter(mAdapter);
// ...
}
}
这样我们就获得了一个简单的展示学生数据的 RecyclerView 了。
然后我们对 Adapter 的数据源进行更改,并通知刷新:
mFab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 创建一个原来的 List 的副本
final ArrayList<Student> oldTemp = new ArrayList<>(datas);
// 更改原数据源
datas.remove(mRandom.nextInt(mAdapter.getItemCount()));
for (int i = 0; i < mRandom.nextInt(3); i++) {
datas.add(mRandom.nextInt(mAdapter.getItemCount() - 1),
new Student(mRandom.nextInt(3000) + "", "Students: " + mRandom.nextDouble()));
}
// 实现 Callback
DiffUtil.Callback callback = new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return oldTemp.size();
}
@Override
public int getNewListSize() {
return datas.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldTemp.get(oldItemPosition).id.equals(datas.get(newItemPosition).id);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldTemp.get(oldItemPosition).name.equals(datas.get(newItemPosition).name);
}
};
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback);
// 把结果应用到 adapter
diffResult.dispatchUpdatesTo(mAdapter);
}
});
效果如下:
DiffUtil 的使用就是这样,根据 DiffUtil.Callback 计算出 Result,然后应用更新到 Adapter。
封装
有的人可能说了,这样其实并不好用啊,我们原来数据的改变就直接使用对应的方法就可以了,你这里每次还要写得这么麻烦。那么我们就使用 DiffUtil 和 Adapter 结合再进行一次封装吧。
我们抽取一个 BaseAdapter 出来:
public abstract class BaseAdapter<T, V extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<V>{
protected final List<T> temp; // 用于保存修改之前的数据源的副本
protected final List<T> datas; // 数据源
public BaseAdapter(List<T> datas) {
this.datas = datas;
temp = new ArrayList<>(datas);
}
protected abstract boolean areItemsTheSame(T oldItem, T newItem);
protected abstract boolean areContentsTheSame(T oldItem, T newItem);
@Override
public int getItemCount() {
return datas.size();
}
public void notifyDiff() {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return temp.size();
}
@Override
public int getNewListSize() {
return datas.size();
}
// 判断是否是同一个 item
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return BaseAdapter.this.areItemsTheSame(temp.get(oldItemPosition), datas.get(newItemPosition));
}
// 如果是同一个 item 判断内容是否相同
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return BaseAdapter.this.areContentsTheSame(temp.get(oldItemPosition), datas.get(newItemPosition));
}
});
diffResult.dispatchUpdatesTo(this);
// 通知刷新了之后,要更新副本数据到最新
temp.clear();
temp.addAll(datas);
}
}
然后我们只需要令 Adapter 实现 BaseAdapter即可:
class MyAdapter extends BaseAdapter<Student, MyAdapter.ViewHolder> {
public MyAdapter(List<Student> datas) {
super(datas);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_recycler, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setData(datas.get(position));
}
@Override
public boolean areItemsTheSame(Student oldItem, Student newItem) {
return oldItem.id.equals(newItem.id);
}
@Override
public boolean areContentsTheSame(Student oldItem, Student newItem) {
return oldItem.name.equals(newItem.name);
}
class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(View itemView) {
super(itemView);
}
public void setData(Student student) {
TextView textView = (TextView) this.itemView.findViewById(R.id.text);
textView.setText(student.name);
}
}
}
之后我们如果数据源 List 中的数据有任何改动,我们只需要调用notifyDiff()就可以了:
mFab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
datas.remove(mRandom.nextInt(mAdapter.getItemCount()));
for (int i = 0; i < mRandom.nextInt(3); i++) {
datas.add(mRandom.nextInt(mAdapter.getItemCount() - 1),
new Student(mRandom.nextInt(3000) + "", "Students: " + mRandom.nextDouble()));
}
mAdapter.notifyDiff();
}
});
总结
最新 Support 包中的 DiffUtil 类给我们带来了一个对 RecyclerView 的不同数据变化的统一处理方案,可以对所有数据变化之后的通知刷新简化,非常好用,强烈推荐使用。
参考
Android开发学习之路-DiffUtil使用教程
【Android】你可能不知道的Support(一) 0步自动定向刷新:SortedList
原文链接:http://www.sxrczx.com/pages/kohoh1992.github.io/cursor-auto-sync/index_1431878338570.html
在Android日常开发中,时常会请求数据到Cursor,然后再通过Cursor获取数据。像SQLiteDatabase和ContentProvider都使用了Cursor。在这些应用中,往往希望当数据发生改变时,Cursor也会自动的更新数据。这篇文章,我就会向你阐述如何通过Android自身的API实现Cursor的自动更新。另外我还将向你阐述这背后的原理。通过这些原理你可以举一反三的实现更为广泛的自动跟新。
文章中的代码
可以在https://github.com/KOHOH1992/CursorSyncDemo中找到文章中出现的代码
该项目共有4个分支。use_provider分支介绍了使用ContentProvider实现Cursor同步更新的方法。use_database分支介绍了不使用ContentProvider实现Cursor同步更新的方法。use_adapter分支介绍了不使用Loader实现Cursor同步更新的方法。
Cursor自动更新的实现
前提
首先假设项目使用了如下的前提
- 数据存储在SqliteDataBase当中
- 通对ContentProvider的请求,获取封装了数据的Cursor
- 使用CursorLoader加载数据
- 使用AdapterView和CursorAdapter显示数据
定义同步标志
static final Uri SYNC_SIGNAL_URI = Uri.parse("content://com.kohoh.cursorsyncdemo/SYNC_SIGNAL");
在ContentProvider的query中设置NotificationUri
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[]
selectionArgs,String sortOrder) {
SQLiteDatabase database = sqLiteOpenHelper.getReadableDatabase();
Cursor cursor = database.query(ContactContract.CONTACT_TABLE, projection,
selection,selectionArgs, null, null, sortOrder);
//设置NotificationUri
cursor.setNotificationUri(contentResolver, ContactContract.SYNC_SIGNAL_URI);
return cursor;
}
在ContentProvider的insert,update,delete中触发NotificationUri
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase database = sqLiteOpenHelper.getWritableDatabase();
long id = database.insert(ContactContract.CONTACT_TABLE, null, values);
if (id >= 0) {
//触发NotificationUri
contentResolver.notifyChange(ContactContract.SYNC_SIGNAL_URI, null);
}
return uri.withAppendedPath(ContactContract.CONTACT_URI, String.valueOf(id));
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SQLiteDatabase database = sqLiteOpenHelper.getWritableDatabase();
int result = database.update(ContactContract.CONTACT_TABLE, values,
selection, selectionArgs);
if (result > 0) {
//触发NotificationUri
contentResolver.notifyChange(ContactContract.SYNC_SIGNAL_URI, null);
}
return result;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase database = sqLiteOpenHelper.getWritableDatabase();
int result = database.delete(ContactContract.CONTACT_TABLE, selection,
selectionArgs);
if (result > 0) {
//触发NotificationUri
contentResolver.notifyChange(ContactContract.SYNC_SIGNAL_URI, null);
}
return result;
}
CursorLoader
ForceLoadContentObserver mObserver;
public Cursor loadInBackground() {
...
try {
//不过多解释,耗时的查询操作
Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder, mCancellationSignal);
if (cursor != null) {
try {
// Ensure the cursor window is filled.
cursor.getCount();
//给Cursor设置观察者;ContentProvider通知Cursor的观察者数据发生了改变,
//Cursor通知CursorLoader的观察者数据发生了改变,CursorLoader通过ContentProvider重新加载新的数据
cursor.registerContentObserver(mObserver);
//cursor.setNotificationUri(getContext().getContentResolver(), otificationUri);//给Cursor设置要观察的URI
} catch (RuntimeException ex) {
cursor.close();
throw ex;
}
}
return cursor;
}
}
Cursor的实现原理
Android的Cursor自动更新是通过观察者模式实现的,整个过程如下图所示
- 通过ContentPorvider和ContentResolver使得数据发生了改变
- ContentProvider通知Cursor的观察者数据发生了改变
- Cursor通知CursorLoader的观察者数据发生了改变
- CursorLoader通过ContentProvider加载新的数据
- ContentPovider向DataBase请求新的数据
- CursorLoader调用CursorAdapter# changeCursor,用封装了新数据的Cursor替换旧的Cursor
- CursorAdapter告知AdapterView的观察者有新的数据
- AdapterView重新加载并显示数据
在Android的android.database包下,有一个ContentObserver。Android正是通过他来实现观察者模式的。当数据改变之后,观察者会将数据改变的消息通知相应的对象,进而做出反馈。在代码中,当数据改变之后,我会调用ContentResolver# notifyChange,发出ContactContract.SYNC_SIGNAL_URI信号,通知数据发生了改变。而在此之前,从ContentProvider# query中获得的Cursor已经通过Cursor# setNotificationUri对ContactContract.SYNC_SIGNAL_URI信号进行了监视。当该信号出现,Cursor就会将信息改变的消息告诉CursorLoader的观察者(在此之前CursorLoader已经对该Cursor设立了观察者)。CursorLoader会开始重新开始加载数据。当数据加载成功,CursorLoader会通过CursorAdapter# changeCursor设置封装了新数据的Cursor。而后CursorAdapter又会通知AdapterView的观察者数据发生了改变(在此之前AdapterView已经对CursorAdapter设立了观察者)。最后AdapterView就会重新加载并显示新的数据。
在整个过程当中,我要做的就是在改变数据时发出信号,对封装数据的Cursor设置需要监视的信号。具体的说就是在query中调用Cursor# setNotificationUri,在insert、update、delete中调用ContentResolver# notifyChange。这里需要补充的是Cursor和ContentResolver的信号机制同样是通过观察者模式实现的。
其他的实现方式
这里要介绍的其他的实现方式,依旧是通过观察者模式实现的。区别在于是否使用ContentProvider和CursorLoader
不使用ContentProvider
在开发过程中,如果数据不用于应用之间的共享,使用ContentProvider似乎有一些多余。然而Android提供的CursorLoader的API必须通过ContentProvider才能实现数据加载和数据同步更新。但是你任然可以在不使用ContentProvider的情况下实现Cursor的自动更新。你需要做的只是在你的Loader中加入下面的代码
// 实例化一个全局的ForceLoadContentObserver
ForceLoadContentObserver mObserver = new ForceLoadContentObserver();
@Override
public Cursor loadInBackground() {
SQLiteDatabase database = mSqLiteOpenHelper.getReadableDatabase();
Cursor cursor = database.query(mTable, mColumns, mSelection, mSelectionArgs,
mGroupBy,mHaving, mOrderBy);
if (cursor != null) {
cursor.getCount();
// 对Cursor设立观察者
cursor.registerContentObserver(mObserver);
// 设置Cursor的观察信号
cursor.setNotificationUri(getContext().getContentResolver(),
mNotificationUri);
}
return cursor;
}
ForceLoadContentObserver是Loader的内部类。当观察到数据发生变化之后,该类会调用Loader# forceLoad,进而开始重新加载数据。另外你也可以直接使用我项目中的DatabaseLoader。该类是我参照CursorLoader编写的一个工具,通过它你可以绕过ContentProvider,直接请求Database。
不使用Loader
如果你不想要使用Loader(我非常不赞成你这么做),你可以通过如下的代码实现Cursor的同步更新。
// 使用CursorAdapter.FLAG_AUTO_REQUERY标志
adapter = new SimpleCursorAdapter(this, R.layout.contact_item, null, from, to,
CursorAdapter.FLAG_AUTO_REQUERY);
private void loadData() {
SQLiteOpenHelper sqliteOpenHelper = ContactContract.getSqliteOpenHelper(this);
SQLiteDatabase database = sqliteOpenHelper.getReadableDatabase();
String[] columns = {ContactContract._ID, ContactContract.NAME,
ContactContract.PHONE};
Cursor cursor = database.query(ContactContract.CONTACT_TABLE, columns, null,
null, null,null, null);
//设置NotificationUri
cursor.setNotificationUri(this.getContentResolver(),
ContactContract.SYNC_SIGNAL_URI);
adapter.changeCursor(cursor);
}
这里的关键在于,在实例化CursorAdapter时使用了CursorAdapter.FLAGAUTOREQUERY标志。当使用该标志后,每当收到数据更新的消息,CursorAdapter就会自己调用CursorAdapter# requery重新加载数据。然而整个加载过程会再UI线程中发生,这很有可能会使得程序运行部流畅。正是因为这个原因该方法以及被Android设置为Deprecated了。因此如果有可能,我还是推荐你使用Loader。
ContactContract.java
package com.kohoh.cursorsyncdemo;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.provider.BaseColumns;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
/**
* Created by kohoh on 14-11-3.
*/
public class ContactContract implements BaseColumns {
static final int DATABSE_VERSION = 1;
static final String DATABASE_NAME = "contact.db";
static final String CONTACT_TABLE = "contact";
static final String NAME = "name";
static final String PHONE = "phone";
static final String AUTHORITY = "com.kohoh.cursorsyncdemo";
static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
static final Uri CONTACT_URI = Uri.withAppendedPath(BASE_URI, "contact");
static final Uri SYNC_SIGNAL_URI = Uri.withAppendedPath(BASE_URI, "SYNC_SIGNAL_URI");
static public ContactDatabaseHelper getSqliteOpenHelper(Context context) {
return new ContactDatabaseHelper(context);
}
static class ContactDatabaseHelper extends SQLiteOpenHelper {
public ContactDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABSE_VERSION);
}
static public long addContact(SQLiteDatabase database, String name, int phone) {
Preconditions.checkNotNull(database);
Preconditions.checkNotNull(phone);
Preconditions.checkArgument(!Strings.isNullOrEmpty(name));
ContentValues contentValues = new ContentValues();
contentValues.put(NAME, name);
contentValues.put(PHONE, phone);
return database.insert(CONTACT_TABLE, null, contentValues);
}
static public void deleteContact(Context context, long id) {
Preconditions.checkNotNull(context);
Preconditions.checkArgument(id >= 0);
ContactContract.ContactDatabaseHelper databaseHelper = ContactContract.
getSqliteOpenHelper(context);
SQLiteDatabase databasea = databaseHelper.getWritableDatabase();
String where = ContactContract._ID + " = ?";
String[] whereArgs = {String.valueOf(id)};
databasea.delete(ContactContract.CONTACT_TABLE, where, whereArgs);
context.getContentResolver().notifyChange(ContactContract.SYNC_SIGNAL_URI, null);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + CONTACT_TABLE + "( " +
_ID + " INTEGER PRIMARY KEY," +
NAME + " TEXT," +
PHONE + " INTERGER)");
addContact(db, "aaa", 111);
addContact(db, "bbb", 222);
addContact(db, "ccc", 333);
addContact(db, "ddd", 444);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
}
ContactProvider.java
package com.kohoh.cursorsyncdemo;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
/**
* Created by kohoh on 14-11-3.
*/
public class ContactProvider extends ContentProvider {
private SQLiteOpenHelper sqLiteOpenHelper;
private ContentResolver contentResolver;
private UriMatcher uriMatcher;
final private int DIR = 0;
final private int ITEM = 1;
@Override
public boolean onCreate() {
sqLiteOpenHelper = ContactContract.getSqliteOpenHelper(getContext());
contentResolver = getContext().getContentResolver();
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContactContract.AUTHORITY, "contact", DIR);
uriMatcher.addURI(ContactContract.AUTHORITY, "contact/#", ITEM);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
if (uriMatcher.match(uri) == ITEM) {
return null;
}
SQLiteDatabase database = sqLiteOpenHelper.getReadableDatabase();
Cursor cursor = database.query(ContactContract.CONTACT_TABLE, projection, selection,
selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(contentResolver, ContactContract.SYNC_SIGNAL_URI);
return cursor;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case ITEM:
return "vnd.android.cursor.item/vnd.con.kohoh.cursorsyncdemo";
case DIR:
return "vnd.android.cursor.dir/vnd.con.kohoh.cursorsyncdemo";
default:
return null;
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
if (uriMatcher.match(uri) == ITEM) {
return null;
}
SQLiteDatabase database = sqLiteOpenHelper.getWritableDatabase();
long id = database.insert(ContactContract.CONTACT_TABLE, null, values);
if (id >= 0) {
contentResolver.notifyChange(ContactContract.SYNC_SIGNAL_URI, null);
}
return uri.withAppendedPath(ContactContract.CONTACT_URI, String.valueOf(id));
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
if (uriMatcher.match(uri) == ITEM) {
return 0;
}
SQLiteDatabase database = sqLiteOpenHelper.getWritableDatabase();
int result = database.delete(ContactContract.CONTACT_TABLE, selection, selectionArgs);
if (result > 0) {
contentResolver.notifyChange(ContactContract.SYNC_SIGNAL_URI, null);
}
return result;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
if (uriMatcher.match(uri) == ITEM) {
return 0;
}
SQLiteDatabase database = sqLiteOpenHelper.getWritableDatabase();
int result = database.update(ContactContract.CONTACT_TABLE, values, selection, selectionArgs);
if (result > 0) {
contentResolver.notifyChange(ContactContract.SYNC_SIGNAL_URI, null);
}
return result;
}
}
DatabaseLoader.java
package com.kohoh.cursorsyncdemo;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
/**
* Created by kohoh on 14-11-3.
*/
public class DatabaseLoader extends AsyncTaskLoader<Cursor> {
final ForceLoadContentObserver mObserver;
Uri mNotificationUri;
String mTable;
String[] mColumns;
String mSelection;
String[] mSelectionArgs;
String mGroupBy;
String mHaving;
String mOrderBy;
SQLiteOpenHelper mSqLiteOpenHelper;
Cursor mCursor;
public DatabaseLoader(Context context) {
super(context);
this.mObserver = new ForceLoadContentObserver();
}
public DatabaseLoader(Context context, SQLiteOpenHelper sqLiteOpenHelper, Uri mNotificationUri,
String mTable, String[] mColumns, String mSelection, String[] mSelectionArgs,
String mGroupBy, String mHaving, String mOrderBy) {
super(context);
this.mNotificationUri = mNotificationUri;
this.mTable = mTable;
this.mColumns = mColumns;
this.mSelection = mSelection;
this.mSelectionArgs = mSelectionArgs;
this.mGroupBy = mGroupBy;
this.mHaving = mHaving;
this.mOrderBy = mOrderBy;
this.mSqLiteOpenHelper = sqLiteOpenHelper;
this.mObserver = new ForceLoadContentObserver();
}
@Override
public Cursor loadInBackground() {
SQLiteDatabase database = mSqLiteOpenHelper.getReadableDatabase();
Cursor cursor = database.query(mTable, mColumns, mSelection, mSelectionArgs, mGroupBy,
mHaving, mOrderBy);
if (cursor != null) {
cursor.getCount();
cursor.registerContentObserver(mObserver);
cursor.setNotificationUri(getContext().getContentResolver(), mNotificationUri);
}
return cursor;
}
@Override
public void deliverResult(Cursor cursor) {
if (isReset()) {
if (cursor != null) {
cursor.close();
}
return;
}
Cursor oldCursor = mCursor;
mCursor = cursor;
if (isStarted()) {
super.deliverResult(cursor);
}
if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
oldCursor.close();
}
}
@Override
protected void onStartLoading() {
if (mCursor != null) {
deliverResult(mCursor);
}
if (takeContentChanged() || mCursor == null) {
forceLoad();
}
}
@Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
@Override
public void onCanceled(Cursor cursor) {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
@Override
protected void onReset() {
super.onReset();
onStopLoading();
if (mCursor != null && !mCursor.isClosed()) {
mCursor.close();
}
mCursor = null;
}
public SQLiteOpenHelper getSqLiteOpenHelper() {
return mSqLiteOpenHelper;
}
public void setSqLiteOpenHelper(SQLiteOpenHelper mSqLiteOpenHelper) {
this.mSqLiteOpenHelper = mSqLiteOpenHelper;
}
public Uri getNotificationUri() {
return mNotificationUri;
}
public void setNotificationUri(Uri mNotificationUri) {
this.mNotificationUri = mNotificationUri;
}
public String getTable() {
return mTable;
}
public void setTable(String mTable) {
this.mTable = mTable;
}
public String[] getColumns() {
return mColumns;
}
public void setColumns(String[] mColumns) {
this.mColumns = mColumns;
}
public String getSelection() {
return mSelection;
}
public void setSelection(String mSelection) {
this.mSelection = mSelection;
}
public String[] getSelectionArgs() {
return mSelectionArgs;
}
public void setSelectionArgs(String[] mSelectionArgs) {
this.mSelectionArgs = mSelectionArgs;
}
public String getGroupBy() {
return mGroupBy;
}
public void setGroupBy(String mGroupBy) {
this.mGroupBy = mGroupBy;
}
public String getHaving() {
return mHaving;
}
public void setHaving(String mHaving) {
this.mHaving = mHaving;
}
public String getOrderBy() {
return mOrderBy;
}
public void setOrderBy(String mOrderBy) {
this.mOrderBy = mOrderBy;
}
}
use provider
package com.kohoh.cursorsyncdemo;
import android.app.Activity;
import android.app.LoaderManager;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CursorAdapter;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
public class CursorSyncDemo extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {
private ListView listView;
private SimpleCursorAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_cursor_sync_demo);
String[] from = {ContactContract.NAME, ContactContract.PHONE};
int[] to = {R.id.name, R.id.phone};
listView = (ListView) findViewById(R.id.lv);
adapter = new SimpleCursorAdapter(this, R.layout.contact_item, null, from, to,
CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
listView.setAdapter(adapter);
getLoaderManager().initLoader(0, null, this);
registerForContextMenu(listView);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.delete:
AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo)
item.getMenuInfo();
ContactContract.ContactDatabaseHelper.deleteContact(this, menuInfo.id);
return true;
default:
return super.onContextItemSelected(item);
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.contact_item_menu, menu);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String[] projection = {ContactContract._ID, ContactContract.NAME, ContactContract.PHONE};
return new CursorLoader(this, ContactContract.CONTACT_URI, projection, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
adapter.changeCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
}
use database
package com.kohoh.cursorsyncdemo;
import android.app.Activity;
import android.app.LoaderManager;
import android.content.Loader;
import android.database.Cursor;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CursorAdapter;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
public class CursorSyncDemo extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {
private ListView listView;
private SimpleCursorAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_cursor_sync_demo);
String[] from = {ContactContract.NAME, ContactContract.PHONE};
int[] to = {R.id.name, R.id.phone};
listView = (ListView) findViewById(R.id.lv);
adapter = new SimpleCursorAdapter(this, R.layout.contact_item, null, from, to,
CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
listView.setAdapter(adapter);
getLoaderManager().initLoader(0, null, this);
registerForContextMenu(listView);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String[] columns = {ContactContract._ID, ContactContract.NAME, ContactContract.PHONE};
return new DatabaseLoader(this,
ContactContract.getSqliteOpenHelper(this),
ContactContract.SYNC_SIGNAL_URI,
ContactContract.CONTACT_TABLE,
columns,
null,
null,
null,
null,
null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
adapter.changeCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.delete:
AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo)
item.getMenuInfo();
ContactContract.ContactDatabaseHelper.deleteContact(this, menuInfo.id);
return true;
default:
return super.onContextItemSelected(item);
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.contact_item_menu, menu);
}
}
use adapter
package com.kohoh.cursorsyncdemo;
import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CursorAdapter;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
public class CursorSyncDemo extends Activity {
private ListView listView;
private SimpleCursorAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_cursor_sync_demo);
String[] from = {ContactContract.NAME, ContactContract.PHONE};
int[] to = {R.id.name, R.id.phone};
listView = (ListView) findViewById(R.id.lv);
adapter = new SimpleCursorAdapter(this, R.layout.contact_item, null, from, to,
CursorAdapter.FLAG_AUTO_REQUERY);
listView.setAdapter(adapter);
loadData();
registerForContextMenu(listView);
}
private void loadData() {
SQLiteOpenHelper sqliteOpenHelper = ContactContract.getSqliteOpenHelper(this);
SQLiteDatabase database = sqliteOpenHelper.getReadableDatabase();
String[] columns = {ContactContract._ID, ContactContract.NAME, ContactContract.PHONE};
Cursor cursor = database.query(ContactContract.CONTACT_TABLE, columns, null, null, null,
null, null);
cursor.setNotificationUri(this.getContentResolver(), ContactContract.SYNC_SIGNAL_URI);
adapter.changeCursor(cursor);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.delete:
AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo)
item.getMenuInfo();
ContactContract.ContactDatabaseHelper.deleteContact(this, menuInfo.id);
return true;
default:
return super.onContextItemSelected(item);
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.contact_item_menu, menu);
}
}