离线时不用再说:请稍后再试

原文作者:Yonatan V. Levin
原文链接:https://medium.com/@yonatanvlevin/offline-support-try-again-later-no-more-afc33eba79dc
文章翻译只用作知识分享。
翻译时省略了一些内容,如果翻译有误请大家纠正。

我有幸生活在一个遍布着4G网络和WIFI的国家。在家,在公司,甚至是在我朋友公寓的浴室里。但是不知为何, 我仍然遇到

离线时不用再说:请稍后再试_第1张图片
image.png

或者是

离线时不用再说:请稍后再试_第2张图片
image.png

也许是因为我的Pixel phone和我开了一个玩笑。哦..天哪
因特网是我曾经使用过的最不稳定的东西。95%的时间它是正常工作的,我可以流畅的播放我的喜欢的音乐没有任何问题,但是当我站在电梯里尝试发一个消息时,出问题了.
开发者生活在一个网络连接十分强大的环境中往往认为它不是问题,但事实上它是一个问题。更多的时候,就像墨菲定律那样,当用户期望你的程序运行的很快,甚至更快的时候,这种情况会伤害用户。
作为一个Android的用户发现许多安装在我手机上的程序都会提示请稍后再试时。我想努力为这种情况做点什么,至少在我做的引用程序上。
有很多关于离线如何工作的话题,例如Yigit Boyar和他的 IO talk

**作者做的APP **

离线时不用再说:请稍后再试_第3张图片
image.png

在创业公司,所有人都知道要做一个最小的可行性产品(Minimum viable product 百度了一下)尝试你的想法。这个过程是至关重要而又十分艰难的。有太多的原因可能失败,因为离线而失去一个用户是绝对无法接受的。用户因为我们的应用体验不好而离开不能成为一个因素。

作者的APP用途很简单,临床医生发起一个请求,相关实验室收到请求报价,临床医生从所有的报价中选则一个。

当我们讨论用户体验时(UX),我们的决定如下:不需要任何的加载效果。应用应该能够顺利的运行,不应把用户放在一个等待的状态中。要达成的最基本目标是:网络不好的情况下,APP仍然能够正常工作。

离线时不用再说:请稍后再试_第4张图片
image.png

当用户在离线的情况下,它提交的请求仍然可以成功。仅仅有一个很小的图标---同步图标,表面用户在离线状态。当它的网络正常的时候,APP将把请求发送出去,不论APP是在前台还是后台。对于每一个网络请求都是如此,除了登录和注册以外。

离线时不用再说:请稍后再试_第5张图片
image.png

那么我们是如何做到这一点的呢:

首先要做的就是将页面,逻辑和持久层进行分离。

这意味着你的数据将异步的方式通过回调/Events的方式传递给Presenter层再传递到视图层。视图层只负责和用户交互,将交互的结果传递给其他模块,并接收模块的反应呈现出另一种状体。

离线时不用再说:请稍后再试_第6张图片
image.png

1.本地存储我们使用SQLite,在它之上我们决定通过ContentProvider封装一下。因为它的 ContentObserver功能。ContentProvider对数据的访问和数据的操作做了很好的抽象。至于为什么不使用RxJava封装作者也给出了意见。

2.对于后台同步的任务,我们选择使用 GCMNetworkManager,它能够在满足一些确切的条件时执行指定的任务,比如说网络连接恢复,它对低电量的模式也有很好的支持。所以项目结构图如下

离线时不用再说:请稍后再试_第7张图片
image.png

步骤流程

1.创建订单并同步
业务层创建一个订单传递给ContentProvider并保存起来。

离线时不用再说:请稍后再试_第8张图片
image.png

public class NewOrderPresenter extends BasePresenter {
  //...
  
  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通知所有的观察者有一个新的数据接入,数据状态是有待操作的

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.后台服务接收到通知后交给特定的服务去执行

public class BackgroundService extends Service {
  //注册了一个监听,监听数据的改变
  @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);
     //当数据改变时通知SendOrderService去执行
      Order order = (Order) msg.obj;
      Intent intent = new Intent(context, SendOrderService.class);
      intent.putExtra(SendOrderService.ORDER_ID, order.getLocalId());
      context.startService(intent);
  }
  
  //...
}

**4.服务从数据库获取到数据尝试在网络环境下同步它。更新订单的状态到同步完成通过ContentResolver **


离线时不用再说:请稍后再试_第9张图片
image.png
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 locationIds = new LabLocation().getLocationIds(this, order.getLocalId());
      orderCreate.setLabLocations(locationIds);
     //尝试通过网络去更新订单状态
      Response response = orderApi.createOrder(orderCreate).execute();

      if (response.isSuccessful()) {
        if (response.code() == 201) {
         //成功时更新订单状态到 同步完成
          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.当请求失败时,将在满足.setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) 条件时通过GCMNetworkManager执行一次任务,满足标准的时候 GCMNetworkManager讲执行onRunTask()回调,APP将尝试再次同步订单,如果再次失败了,将改期再执行

使用GCM 需要引入一些库,但是在国内的支持不是很好,可以考虑使用AlarmManger

dependencies {  compile 'com.google.android.gms:play-services-gcm:8.1.0' }
离线时不用再说:请稍后再试_第10张图片
image.png
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 locationIds = new LabLocation().getLocationIds(this, order.getLocalId());
      orderCreate.setLabLocations(locationIds);
      
      Response 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;
  }

  //...
}
离线时不用再说:请稍后再试_第11张图片
image.png

当同步成功以后,将通过ContentResolve去更新数据。

当然这样的架构并不是完善的,你需要考虑许多临届的情况。比如你更新了一个在服务端已经存在的订单,但是已经在服务端修改或者删除了订单。两端同时修改了一张订单怎么办。等等问题将在作者的下一篇文章提出。

I have the privilege of living in a country 我有幸生活在一个国家ˈ priv(ə)lij
I have ever used 我曾经使用过的
It will hurt your users exactly when they most need your App to work 当恰好用户需要你的程序工作会伤害到用户
I struggled to do something about it 我努力为它做点什么。
In startups 在创业公司
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 当我们讨论各种用户体验时。
we decided on the following:我们的决定如下
So basically what we want to achieve:要达成的最基本目标是
certain specific conditions met 满足具体的条件
The service obtains the data from DB and tries to sync it over the network.服务从数据库获取到数据尝试在网络环境下同步它。
the order is updated with status “synced” via ContentResolver 更新订单的状态到同步完成通过ContentResolver
When the criteria is met 当满足标准的时候
The different approaches that we took to solve these isseus 解决这些问题的不同方式

你可能感兴趣的:(离线时不用再说:请稍后再试)