原文作者:Yonatan V. Levin
原文鏈接:https://medium.com/@yonatanvlevin/offline-support-try-again-later-no-more-afc33eba79dc
文章翻譯只用作知識分享故爵。
翻譯時省略了一些內(nèi)容,如果翻譯有誤請大家糾正诗越。
我有幸生活在一個遍布著4G網(wǎng)絡(luò)和WIFI的國家叔壤。在家,在公司匆篓,甚至是在我朋友公寓的浴室里惨奕。但是不知為何火架, 我仍然遇到
或者是
也許是因?yàn)槲业腜ixel phone和我開了一個玩笑诡延。哦..天哪
因特網(wǎng)是我曾經(jīng)使用過的最不穩(wěn)定的東西滞欠。95%的時間它是正常工作的,我可以流暢的播放我的喜歡的音樂沒有任何問題肆良,但是當(dāng)我站在電梯里嘗試發(fā)一個消息時筛璧,出問題了.
開發(fā)者生活在一個網(wǎng)絡(luò)連接十分強(qiáng)大的環(huán)境中往往認(rèn)為它不是問題,但事實(shí)上它是一個問題惹恃。更多的時候隧哮,就像墨菲定律那樣,當(dāng)用戶期望你的程序運(yùn)行的很快座舍,甚至更快的時候,這種情況會傷害用戶陨帆。
作為一個Android的用戶發(fā)現(xiàn)許多安裝在我手機(jī)上的程序都會提示請稍后再試時曲秉。我想努力為這種情況做點(diǎn)什么,至少在我做的引用程序上疲牵。
有很多關(guān)于離線如何工作的話題承二,例如Yigit Boyar和他的 IO talk
**作者做的APP **
在創(chuàng)業(yè)公司,所有人都知道要做一個最小的可行性產(chǎn)品(Minimum viable product 百度了一下)嘗試你的想法纲爸。這個過程是至關(guān)重要而又十分艱難的亥鸠。有太多的原因可能失敗,因?yàn)殡x線而失去一個用戶是絕對無法接受的。用戶因?yàn)槲覀兊膽?yīng)用體驗(yàn)不好而離開不能成為一個因素负蚊。
作者的APP用途很簡單神妹,臨床醫(yī)生發(fā)起一個請求,相關(guān)實(shí)驗(yàn)室收到請求報(bào)價(jià)家妆,臨床醫(yī)生從所有的報(bào)價(jià)中選則一個鸵荠。
當(dāng)我們討論用戶體驗(yàn)時(UX),我們的決定如下:不需要任何的加載效果伤极。應(yīng)用應(yīng)該能夠順利的運(yùn)行蛹找,不應(yīng)把用戶放在一個等待的狀態(tài)中。要達(dá)成的最基本目標(biāo)是:網(wǎng)絡(luò)不好的情況下哨坪,APP仍然能夠正常工作庸疾。
當(dāng)用戶在離線的情況下,它提交的請求仍然可以成功当编。僅僅有一個很小的圖標(biāo)---同步圖標(biāo)届慈,表面用戶在離線狀態(tài)。當(dāng)它的網(wǎng)絡(luò)正常的時候凌箕,APP將把請求發(fā)送出去拧篮,不論APP是在前臺還是后臺。對于每一個網(wǎng)絡(luò)請求都是如此牵舱,除了登錄和注冊以外串绩。
那么我們是如何做到這一點(diǎn)的呢:
首先要做的就是將頁面,邏輯和持久層進(jìn)行分離芜壁。
這意味著你的數(shù)據(jù)將異步的方式通過回調(diào)/Events的方式傳遞給Presenter層再傳遞到視圖層礁凡。視圖層只負(fù)責(zé)和用戶交互,將交互的結(jié)果傳遞給其他模塊慧妄,并接收模塊的反應(yīng)呈現(xiàn)出另一種狀體顷牌。
1.本地存儲我們使用SQLite,在它之上我們決定通過ContentProvider封裝一下塞淹。因?yàn)樗?ContentObserver功能窟蓝。ContentProvider對數(shù)據(jù)的訪問和數(shù)據(jù)的操作做了很好的抽象。至于為什么不使用RxJava封裝作者也給出了意見饱普。
2.對于后臺同步的任務(wù)运挫,我們選擇使用 GCMNetworkManager,它能夠在滿足一些確切的條件時執(zhí)行指定的任務(wù)套耕,比如說網(wǎng)絡(luò)連接恢復(fù)谁帕,它對低電量的模式也有很好的支持。所以項(xiàng)目結(jié)構(gòu)圖如下
步驟流程
1.創(chuàng)建訂單并同步
業(yè)務(wù)層創(chuàng)建一個訂單傳遞給ContentProvider并保存起來冯袍。
public class NewOrderPresenter extends BasePresenter<NewOrderView> {
//...
private int insertOrder(Order order) {
//turn order to ContentValues object (used by SQL to insert values to Table)
ContentValues values = order.createLocalOrder(order);
//call resolver to insert data to the Order table
Uri uri = context.getContentResolver().insert(KolGeneContract.OrderEntry.CONTENT_URI, values);
//get Id for order.
if (uri != null) {
return order.getLocalId();
}
return -1;
}
//...
}
2.ContentProvider通知所有的觀察者有一個新的數(shù)據(jù)接入,數(shù)據(jù)狀態(tài)是有待操作的
public class KolGeneProvider extends ContentProvider {
//...
@Nullable @Override public Uri insert(@NonNull Uri uri, ContentValues values) {
//open DB for write
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
//match URI to action.
final int match = sUriMatcher.match(uri);
Uri returnUri;
switch (match) {
//case of creating order.
case ORDER:
long _id = db.insertWithOnConflict(KolGeneContract.OrderEntry.TABLE_NAME, null, values,
SQLiteDatabase.CONFLICT_REPLACE);
if (_id > 0) {
returnUri = KolGeneContract.OrderEntry.buildOrderUriWithId(_id);
} else {
throw new android.database.SQLException(
"Failed to insert row into " + uri + " id=" + _id);
}
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
//notify observables about the change
getContext().getContentResolver().notifyChange(uri, null);
return returnUri;
}
//...
}
3.后臺服務(wù)接收到通知后交給特定的服務(wù)去執(zhí)行
public class BackgroundService extends Service {
//注冊了一個監(jiān)聽匈挖,監(jiān)聽數(shù)據(jù)的改變
@Override public int onStartCommand(Intent intent, int i, int i1) {
if (observer == null) {
observer = new OrdersObserver(new Handler());
getContext().getContentResolver()
.registerContentObserver(KolGeneContract.OrderEntry.CONTENT_URI, true, observer);
}
}
//...
@Override public void handleMessage(Message msg) {
super.handleMessage(msg);
//當(dāng)數(shù)據(jù)改變時通知SendOrderService去執(zhí)行
Order order = (Order) msg.obj;
Intent intent = new Intent(context, SendOrderService.class);
intent.putExtra(SendOrderService.ORDER_ID, order.getLocalId());
context.startService(intent);
}
//...
}
**4.服務(wù)從數(shù)據(jù)庫獲取到數(shù)據(jù)嘗試在網(wǎng)絡(luò)環(huán)境下同步它碾牌。更新訂單的狀態(tài)到同步完成通過ContentResolver **
public class SendOrderService extends IntentService {
@Override protected void onHandleIntent(Intent intent) {
int orderId = intent.getIntExtra(ORDER_ID, 0);
if (orderId == 0 || orderId == -1) {
return;
}
Cursor c = null;
try {
c = getContentResolver().query(
KolGeneContract.OrderEntry.buildOrderUriWithIdAndStatus(orderId, Order.NOT_SYNCED), null,
null, null, null);
if (c == null) return;
Order order = new Order();
if (c.moveToFirst()) {
order.getSelfFromCursor(c, order);
} else {
return;
}
OrderCreate orderCreate = order.createPostOrder(order);
List<LocationId> locationIds = new LabLocation().getLocationIds(this, order.getLocalId());
orderCreate.setLabLocations(locationIds);
//嘗試通過網(wǎng)絡(luò)去更新訂單狀態(tài)
Response<Order> response = orderApi.createOrder(orderCreate).execute();
if (response.isSuccessful()) {
if (response.code() == 201) {
//成功時更新訂單狀態(tài)到 同步完成
Order responseOrder = response.body();
responseOrder.setLocalId(orderId);
responseOrder.setSync(Order.SYNCED);
ContentValues values = responseOrder.getContentValues(responseOrder);
Uri uri = getContentResolver().update(
KolGeneContract.OrderEntry.buildOrderUriWithId(order.getLocalId()), values);
return;
}
} else {
//失敗時
if (response.code() == 401) {
ClientUtils.broadcastUnAuthorizedIntent(this);
return;
}
}
} catch (IOException e) {
} finally {
if (c != null && !c.isClosed()) {
c.close();
}
}
SyncOrderService.scheduleOrderSending(getApplicationContext(), orderId);
}
}
5.當(dāng)請求失敗時,將在滿足.setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) 條件時通過GCMNetworkManager執(zhí)行一次任務(wù)儡循,滿足標(biāo)準(zhǔn)的時候 GCMNetworkManager講執(zhí)行onRunTask()回調(diào)舶吗,APP將嘗試再次同步訂單,如果再次失敗了贮折,將改期再執(zhí)行
使用GCM 需要引入一些庫裤翩,但是在國內(nèi)的支持不是很好,可以考慮使用AlarmManger
dependencies { compile 'com.google.android.gms:play-services-gcm:8.1.0' }
public class SyncOrderService extends GcmTaskService {
//...
public static void scheduleOrderSending(Context context, int id) {
GcmNetworkManager manager = GcmNetworkManager.getInstance(context);
Bundle bundle = new Bundle();
bundle.putInt(SyncOrderService.ORDER_ID, id);
OneoffTask task = new OneoffTask.Builder().setService(SyncOrderService.class)
.setTag(SyncOrderService.getTaskTag(id))
.setExecutionWindow(0L, 30L)
.setExtras(bundle)
.setPersisted(true)
.setRequiredNetwork(Task.NETWORK_STATE_CONNECTED)
.build();
manager.schedule(task);
}
//...
@Override public int onRunTask(TaskParams taskParams) {
int id = taskParams.getExtras().getInt(ORDER_ID);
if (id == 0) {
return GcmNetworkManager.RESULT_FAILURE;
}
Cursor c = null;
try {
c = getContentResolver().query(
KolGeneContract.OrderEntry.buildOrderUriWithIdAndStatus(id, Order.NOT_SYNCED), null, null,
null, null);
if (c == null) return GcmNetworkManager.RESULT_FAILURE;
Order order = new Order();
if (c.moveToFirst()) {
order.getSelfFromCursor(c, order);
} else {
return GcmNetworkManager.RESULT_FAILURE;
}
OrderCreate orderCreate = order.createPostOrder(order);
List<LocationId> locationIds = new LabLocation().getLocationIds(this, order.getLocalId());
orderCreate.setLabLocations(locationIds);
Response<Order> response = orderApi.createOrder(orderCreate).execute();
if (response.isSuccessful()) {
if (response.code() == 201) {
Order responseOrder = response.body();
responseOrder.setLocalId(id);
responseOrder.setSync(Order.SYNCED);
ContentValues values = responseOrder.getContentValues(responseOrder);
Uri uri = getContentResolver().update(
KolGeneContract.OrderEntry.buildOrderUriWithId(order.getLocalId()), values);
return GcmNetworkManager.RESULT_SUCCESS;
}
} else {
if (response.code() == 401) {
ClientUtils.broadcastUnAuthorizedIntent(getApplicationContext());
}
}
} catch (IOException e) {
} finally {
if (c != null && !c.isClosed()) c.close();
}
return GcmNetworkManager.RESULT_RESCHEDULE;
}
//...
}
當(dāng)同步成功以后调榄,將通過ContentResolve去更新數(shù)據(jù)踊赠。
當(dāng)然這樣的架構(gòu)并不是完善的,你需要考慮許多臨屆的情況每庆。比如你更新了一個在服務(wù)端已經(jīng)存在的訂單筐带,但是已經(jīng)在服務(wù)端修改或者刪除了訂單。兩端同時修改了一張訂單怎么辦缤灵。等等問題將在作者的下一篇文章提出伦籍。
I have the privilege of living in a country 我有幸生活在一個國家? priv(?)lij
I have ever used 我曾經(jīng)使用過的
It will hurt your users exactly when they most need your App to work 當(dāng)恰好用戶需要你的程序工作會傷害到用戶
I struggled to do something about it 我努力為它做點(diǎn)什么。
In startups 在創(chuàng)業(yè)公司
as most of you know 所有人都知道
testing your assumptions 嘗試你的猜想
The process is so crucial and hard 這個過程是重要和困難的
totally unacceptable 絕對無法接受的
If there were leaving because the experience of using the application was bad?—?well, it’s not even an option
When we discussed various UX solutions 當(dāng)我們討論各種用戶體驗(yàn)時腮出。
we decided on the following:我們的決定如下
So basically what we want to achieve:要達(dá)成的最基本目標(biāo)是
certain specific conditions met 滿足具體的條件
The service obtains the data from DB and tries to sync it over the network.服務(wù)從數(shù)據(jù)庫獲取到數(shù)據(jù)嘗試在網(wǎng)絡(luò)環(huán)境下同步它帖鸦。
the order is updated with status “synced” via ContentResolver 更新訂單的狀態(tài)到同步完成通過ContentResolver
When the criteria is met 當(dāng)滿足標(biāo)準(zhǔn)的時候
The different approaches that we took to solve these isseus 解決這些問題的不同方式