此篇文章为《打造高质量的Android应用 Android开发必知的50个诀窍》中的Hank23-同步适配器模式,主要模拟Gmail可以很好的处理在线和离线状态,给用户带来良好的用户体验。
Gmail通过同步适配器(AyncAdapter)实现上述功能,遗憾的是,尽管同步适配器是android提供的最好特性之一,但是却缺乏相应文档。
本文以what to do为例,实现前后端的代码,模拟离线和在线的状态,基本效果如下图:
这一部分先说客户端的离线实现方式。
1. 实现上面的ui效果,数据源为空时,显示暂无数据,虽然这样的效果实现方式有多种,这里还是提供一种高大上的方式,代码如下:
listView.setEmptyView(findViewById(R.id.kong));
<TextView
android:id="@+id/kong"
android:layout_width= "match_parent"
android:layout_height= "match_parent"
android:gravity="center"
android:text="暂无数据"
android:textSize="26sp" />
2. listView中的数据从数据库来,实现数据库的代码如下:
public class DatabaseHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "todo.db";
public static final int DATAASE_VERSION = 1;
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATAASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String sql = "CREATE TABLE " + ToDoContentProvider.TODO_TABLE_NAME
+ " (" + ToDoContentProvider. COLUMN_ID
+ " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ ToDoContentProvider. COLUMN_SERVER_ID + " INTEGER, "
+ ToDoContentProvider. COLUMN_TITLE + " LONGTEXT, "
+ ToDoContentProvider. COLUMN_STATUS_FLAG + " INTEGER);";
db.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
String sql = "drop table if exists todo";
db.execSQL(sql);
onCreate(db);
}
}
3.Dao实现代码:dao中目前实现了增和删的操作,其关键代码是使用
contentResolver操作内容提供者。dao其实可有可无,完全可以直接操作内容提供者。
public class ToDoDao {
private static final ToDoDao instance = new ToDoDao();
private ToDoDao() {
}
public static ToDoDao getInstance() {
return instance;
}
public void addNewToDo(ContentResolver contentResolver, ToDo todo, int flag) {
ContentValues cv = new ContentValues();
cv.put(ToDoContentProvider. COLUMN_SERVER_ID, todo.getId());
cv.put(ToDoContentProvider. COLUMN_TITLE, todo.getTitle());
cv.put(ToDoContentProvider. COLUMN_STATUS_FLAG, flag);
contentResolver.insert(ToDoContentProvider. CONTENT_URI, cv);
}
public void deleteToDo(ContentResolver contentResolver, int id) {
contentResolver.delete(ToDoContentProvider. CONTENT_URI,
ToDoContentProvider. COLUMN_ID + "=" + id, null);
}
}
4. 内容提供者代码
public class ToDoContentProvider extends ContentProvider {
public static final String TODO_TABLE_NAME = "todos";
// getCanonicalName获取类全名(包名.类名)
public static final String AUTHORITY = ToDoContentProvider.class
.getCanonicalName();
public static final String COLUMN_ID = "_id";
public static final String COLUMN_SERVER_ID = "server_id";
public static final String COLUMN_TITLE = "title";
public static final String COLUMN_STATUS_FLAG = "status_flag";
private static final int TODO = 1;
private static final int TODO_ID = 2;
private static HashMap<String, String> projectionMap;
private static UriMatcher sUriMatcher;
// 带id表示单个,不带id表示多个
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.androidhacks.todo" ;
public static final String CONTENT_TYPE_ID = "vnd.android.cursor.item/vnd.androidhacks.todo" ;
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
+ "/" + TODO_TABLE_NAME);
private DatabaseHelper dbHelper;
static {
// 根据内容uri判断执行流程
sUriMatcher = new UriMatcher(UriMatcher. NO_MATCH);
sUriMatcher.addURI( AUTHORITY, TODO_TABLE_NAME, TODO);
sUriMatcher.addURI( AUTHORITY, TODO_TABLE_NAME + "/#", TODO_ID);
projectionMap = new HashMap<>();
projectionMap.put( COLUMN_ID, COLUMN_ID);
projectionMap.put( COLUMN_SERVER_ID, COLUMN_SERVER_ID);
projectionMap.put( COLUMN_TITLE, COLUMN_TITLE);
projectionMap.put( COLUMN_STATUS_FLAG, COLUMN_STATUS_FLAG);
}
@Override
public boolean onCreate() {
dbHelper = new DatabaseHelper(getContext());
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
switch ( sUriMatcher.match(uri)) {
case TODO:
qb.setTables( TODO_TABLE_NAME);
qb.setProjectionMap( projectionMap);
break;
case TODO_ID:
qb.setTables( TODO_TABLE_NAME);
qb.setProjectionMap( projectionMap);
qb.appendWhere( COLUMN_ID + "=" + uri.getPathSegments().get(1));
break;
default:
throw new RuntimeException( "Unknow uri");
}
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor c = qb.query(db, projection, selection, selectionArgs, null,
null, sortOrder);
// 在cursor返回给调用者之前,我们注册了“ uri内容变动通知”,这样cursor会监控 uri内容变化,当发现 uri内容改变时,cursor会自动更新数据。
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
@Override
public String getType(Uri uri) {
int match = sUriMatcher.match(uri);
switch (match) {
case TODO:
return CONTENT_TYPE;
case TODO_ID:
return CONTENT_TYPE_ID;
default:
throw new RuntimeException( "no matcher uri");
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// 操作数据库
SQLiteDatabase db = dbHelper.getWritableDatabase();
long _id = db.insertOrThrow( TODO_TABLE_NAME, null, values);
// 通知数据已经改变
getContext().getContentResolver().notifyChange(uri, null);
return buildUri( CONTENT_URI, String. valueOf(_id));
}
public Uri buildUri(Uri uri, String id) {
return uri.buildUpon().appendPath(id).build();
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 操作数据库
dbHelper.getWritableDatabase().delete( TODO_TABLE_NAME, selection,
selectionArgs);
// 通知数据改变
getContext().getContentResolver().notifyChange(uri, null);
return 1;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return 0;
}
public class StatusFlag {
public static final int NORMAL = 0;
public static final int DELETE = 1;
public static final int CLEAN = 2;
}
}
5. listview设置的adapter是cursorAdapter
public class ToDoAdapter extends CursorAdapter {
private Activity mActivity;
private Holder holder;
public ToDoAdapter(Context context, Cursor c) {
super(context, c);
mActivity = (Activity) context;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
// 实例化view且处理逻辑
View view = View. inflate(context, R.layout.item_layout, null );
holder = new Holder();
holder. idView = (TextView) view.findViewById(R.id.id);
holder. titleView = (TextView) view.findViewById(R.id.title );
holder. delete = (Button) view.findViewById(R.id.delete );
int columnId = cursor.getColumnIndex(ToDoContentProvider.COLUMN_ID );
int columnTitle = cursor
.getColumnIndex(ToDoContentProvider. COLUMN_TITLE);
final int id = cursor.getInt(columnId);
String title = cursor.getString(columnTitle);
holder. idView.setText(id + "");
holder. titleView.setText(title);
holder. delete.setOnClickListener( new OnClickListener() {
@Override
public void onClick(View v) {
ToDoDao. getInstance().deleteToDo(
mActivity.getContentResolver(), id);
;
}
});
view.setTag( holder);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
holder = (Holder) view.getTag();
// view不为null,只处理逻辑
int columnId = cursor.getColumnIndex(ToDoContentProvider.COLUMN_ID );
int columnTitle = cursor
.getColumnIndex(ToDoContentProvider. COLUMN_TITLE);
final int id = cursor.getInt(columnId);
String title = cursor.getString(columnTitle);
holder. idView.setText(id + "");
holder. titleView.setText(title);
holder. delete.setOnClickListener( new OnClickListener() {
@Override
public void onClick(View v) {
ToDoDao. getInstance().deleteToDo(
mActivity.getContentResolver(), id);
// 这里未调用notifyDataSetChanged,是因为之前调用过setNotificationUri方法,当通过provider更新数据库的时候, contentprovider返回的cursor也会被更新
}
});
}
static class Holder {
TextView idView, titleView;
Button delete;
}
}
6. 其实也可以不用内容提供者,直接使用数据库,这样做的好处是,通过cursor可以直接更新数据,还有2010年google开发者大会的一篇《rest》的文章,可能不充分,再补充。