Android离线数据同步策略
需求
最近做了个Todo类的android客户端,客户需要客户端可以离线使用,并且当客户端再次连接到服务端时,需要将数据同步至服务端,并且要支持多终端同一账号同时登录操作。这个需求和印象笔记十分相似,在网上也检索了许多相关的资料,但基本上都帮助不大。最后只好自己构思设计,在这里记录下该需求的实现思路和核心代码。
实现思路
- android客户端要能够离线使用,这就要求android客户端能够在本地管理数据,在这里采用Sqlite,经过对比后采用Suger ORM。
- android客户端离线操作的数据要支持多端登录 ,为了防止在本地生成的数据主键和其他离线客户端生成的数据主键冲突,那么数据主键便不适合使用自增INT来实现,这里采用UUID4作为数据表主键。
- 离线操作的数据要统一由前台ui发送至后台service,再由当前网络状态来判断当前动作的操作逻辑,为了明确处理逻辑。使用EventBus作为消息总线来负责传递数据操作、ui变化等操作消息。
- 离线数据再再次联网时要和服务器当前数据进行比对,要记录数据最后一次操作的时间作为数据的是否需要更新的凭证。具体处理逻辑如下:
场景 | 操作 |
---|---|
本地有数据A,服务器没有数据A | 向服务器新增数据A |
本地没有数据A,服务器有数据A | 向服务器删除数据A |
服务器和本地均有数据A,服务器记录的更新时间更晚 | 数据A由服务器向本地更新 |
服务器和本地均有数据A,服务器记录的更新时间更早 | 数据A由本地向服务器更新 |
服务器和本地均有数据A,两端更新时间相同 | 不处理 |
- 本地数据模型中记录如下三个字段,用于区分数据最近进行的操作。
private Boolean isCreate = false;
private Boolean isUpdate = false;
private Boolean isDelete = false;
核心代码
/**
* 同步数据
*/
public void sync() {
Logger.i("Start sync category");
if (!Utils.isNetworkConnected(this)) {
Logger.w("End sync category, because network is not connected!");
return;
}
// step1 向服务器新增本地新增
List<Category> categoryCreateList = Category.find(Category.class, "IS_CREATE = ?", "1");
for (Category category : categoryCreateList) {
addCategory(category);
}
// step2 向服务器删除本地删除
List<Category> categoryDeleteList = Category.find(Category.class, "IS_DELETE = ?", "1");
for (Category category : categoryDeleteList) {
deleteCategory(category);
}
Request request = new Request.Builder()
.get()
.url(Constants.CategorySyncList)
.build();
HttpClientUtil.getClient().newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Logger.e("Category onFailure");
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
final String data = response.body().string();
switch (response.code()) {
case 200:
Logger.json(data);
Gson gson = new Gson();
// 服务端数据
List<Category> categoryServerList = gson.fromJson(data, new TypeToken<List<Category>>() {
}.getType());
List<String> categoryServerIdList = new ArrayList<>();
for (Category category : categoryServerList) {
categoryServerIdList.add(category.getUuid());
}
// 本地数据
List<Category> categoryDbList = Category.listAll(Category.class);
HashMap<String, String> categoryDbIdList = new HashMap<>();
for (Category category : categoryDbList) {
categoryDbIdList.put(category.getUuid(), category.getUpdate_time());
}
for (Category categoryServer : categoryServerList) {
if (!categoryDbIdList.keySet().contains(categoryServer.getUuid())) {
// step3 服务器比本地多的,向本地新增
Logger.i("need to add " + categoryServer.getUuid());
categoryServer.save();
} else {
Category categoryDb = Category.findByUUID(categoryServer.getUuid());
switch (Utils.timeEqual(categoryServer.getUpdate_time(), categoryDbIdList.get(categoryServer.getUuid()))) {
case 0:
// step5.1 更新时间相同的不处理
Logger.i("not change " + categoryServer.getUuid());
break;
case 1:
// step5.2 本地比服务器更新时间晚的,向服务器更新
Logger.i("server_time:%s local_time:%s, need to update %s to server", categoryServer.getUpdate_time(),
categoryDbIdList.get(categoryServer.getUuid()), categoryServer.getUuid());
if (categoryDb != null) {
updateCategory(categoryDb);
}
break;
case -1:
// step5.3 本地比服务器更新时间早的,向本地更新
Logger.i("server_time:%s local_time:%s, need to update %s to local", categoryServer.getUpdate_time(),
categoryDbIdList.get(categoryServer.getUuid()), categoryServer.getUuid());
if (categoryDb != null) {
categoryServer.setId(categoryDb.getId());
categoryServer.save();
}
break;
default:
break;
}
}
}
for (Category categoryDb : categoryDbList) {
if (!categoryServerIdList.contains(categoryDb.getUuid())) {
// step4 服务器比本地少的,从本地删除
categoryDb.delete();
}
}
break;
default:
break;
}
// step6 同步UI
Sync sync = new Sync("category_list");
EventBus.getDefault().post(sync);
Logger.i("End sync category");
}
});
}
Todo: for then if 可以优化为取差集、补集。