[译]使用 MVI 编写响应式 APP — 第五部分 — 简单的调试

使用 MVI 编写响应式 APP — 第 5 部分 — 简单的调试

在前面的系列博客中咱们已经讨论了 Model-View-Intent(MVI)模式和它的特征。在第一部分咱们已经讨论了关于单向数据流的重要性和“业务逻辑”驱动型的应用状态的概念。在这篇博客中咱们将看到如何经过 debug 来简化开发者的开发工做。前端

你之前有没有收到一个崩溃报告,而且你不能复现报告中的 bug?听起来很熟悉?我也以为很熟悉!在花费数小时看 stacktrace 和咱们的源代码,我选择在 issue 跟踪中关闭掉了这样的报告,并且跟随着一个小的 comment 像“不能复现这个 bug”或者“这必定是一个奇怪设备/厂商(大厂)致使的错误”。java

用咱们在这系列博客里开发的购物车 app 作例子:当在 home 页面,咱们的用户能够作下拉刷新,崩溃的报告显示,因为某种未知的缘由,当下拉刷新加载新数据的时候,会触发 NullPointerException 异常。android

你作为开发这开始在 home 页面进行上拉刷新操做,可是,这个 App 并无崩溃。它像预期的那样工做。所以,你关闭了代码。可是,你不能看到 NullPointException 在这里如何被抛出的。接着你开始了断点调试,一步一步地运行相关组件的代码,可是它仍旧是在正常工做。特喵的怎么才能重现这个 bug 呢?ios

这个问题是你不可以重现当崩溃发生的时候的场景。若是有用户在遇到崩溃问题时,可以给你崩溃报告,包含 App(发生崩溃前)的状态信息和调用堆栈信息,岂不美哉?伴随着单项数据流和 Model-View-Intent 模式那么这种状况将变得十分简单。咱们简单记录用户触发的全部的 intent 和渲染到 view 上的 model(model 表明了 app 的状态、view 的状态)。 让咱们在 home 页面上这样去作,在 HomePresenter 类上添加 log (对于更多的细节能够看第三部分 在第三部分中咱们已经讨论过状态折叠器的优势)。在下面的代码中我将贴出咱们使用 Crashlytics(相似于 Bugly) 的代码片断,可是它应当与其余的 crash 报告工具的使用是相同的。git

class HomePresenter extends MviBasePresenter<HomeView, HomeViewState> {

  private final HomeViewState initialState; // Show loading indicator

  public HomePresenter(HomeViewState initialState){
    this.initialState = initialState;
  }

  @Override protected void bindIntents() {

    Observable<PartialState> loadFirstPage = intent(HomeView::loadFirstPageIntent)
          .doOnNext(intent -> Crashlytics.log("Intent: load first page"))
          .flatmap(...); // business logic calls to load data

    Observable<PartialState> pullToRefresh = intent(HomeView::pullToRefreshIntent)
          .doOnNext(intent -> Crashlytics.log("Intent: pull-to-refresh"))
          .flatmap(...); // business logic calls to load data

    Observable<PartialState> nextPage = intent(HomeView::loadNextPageIntent)
          .doOnNext(intent -> Crashlytics.log("Intent: load next page"))
          .flatmap(...); // business logic calls to load data

    Observable<PartialState> allIntents = Observable.merge(loadFirstPage, pullToRefresh, nextPage);
    Observable<HomeViewState> stateObservable = allIntents
          .scan(initialState, this::viewStateReducer) // call the state reducer
          .doOnNext(newViewState -> Crashlytics.log( "State: "+gson.toJson(newViewState) ));

    subscribeViewState(stateObservable, HomeView::render); // display new state
  }

  private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){
    ...
  }
}
复制代码

应用RxJava的 .doOnNext() 操做符,在每一个 intent、每一个 intent 的结果和以后渲染到 view 上的状态上添加日志,咱们序列化 view 状态为json对象(咱们稍后来讨论这个)。github

咱们能够看一下这些 logs:sql

logs

看一下这些 log,咱们不只能够看应用崩溃前的最新状态,并且能够看到用户达到这个状态的整个过程。为了更好的可读性,我已经强调了状态过滤,而且用_[…]_替换掉“数据”(这些项将被显示到 recycler view 上)。 所以,用户开启这个 app -加载第一页的意图。而后加载指示条显示"loadFirstPage"。而后,真的数据就被加载进来了(data[…])。 接下来用户滑动列表项而且到达了 recyclerView 的底部,这将触发加载下一页的意图去加载更多数据(分页),这将形成状态转换成"loadingNextPage":对。一旦下一页被加载的数据(data[…])已经被更新而且"loadNextPage":错误已经被矫正。用户第二次作一样的事情。而且它开始采用下拉刷新意图而且状态,状态转变为“loadingPullRefresh”:true。忽然 App 崩溃了(没有更多以后的 log 信息)。json

所以如何利用这些信息帮助咱们修复这个 bug?显然,咱们知道那个意图用户触发了,所以咱们能够人工去复现 bug。此外,咱们能够将咱们的 app 的状态快照成 json。咱们能够简单的将最后一个状态反序列化 json,而且成为咱们的初始状态去修复这个 Bug:后端

String json =" {\"data\":[...],\"loadingFirstPage\":false,\"loadingNextPage\":false,\"loadingPullToRefresh\":false} ";
HomeViewState stateBeforeCrash = gson.fromJson(json, HomeViewState.class);
HomePresenter homePresenter = new HomePresenter(stateBeforeCrash);
复制代码

而后,咱们打开调试工具,触发下拉刷新的意图(intent)。它将出如今若是用户已经向下滑第二次滑到第二页没有更多的数据存在,而且,咱们的 app 没有正确的处理,所以下拉刷新形成了崩溃。服务器

总结

制做 app 的状态"快照"让咱们的开发工做更加轻松。不只咱们能够容易的复现崩溃场景,另外,咱们能够序列化状态去写回归测试,不用额外消耗任意代码。记住这仅仅适用于若是 app 的状态遵循单项数据流(被业务逻辑驱动),不变性和纯函数的原则。Model-View-Intent 带领咱们去正确的方向,所以咱们构建“可快照”的 app 是很是好和十分有用,这就是这种架构的“反作用”。

"可快照的" app 有什么缺点?显然咱们序列化 app 的状态(例如:使用 Gson)。这将添加额外的计算时间。在个人通常大小的 app 中,首次使用 Gson 序列化须要大约 30 毫秒。由于 Gson 须要使用反射来扫描类去决定须要序列化的字段。随后的状态序列化在 Nexus 4 中平均须要花费 6 毫秒。当序列化运行在 .doOnNext() 这是通常运行在其余线程,可是,我 app 的用户不得不等 6 毫秒比那些没有快照的 app。个人观点是等 6 毫秒用户是很难察觉到。不管如何,关于快照状态的一个讨论是当崩溃发生时,从用户的设备经过崩溃日志工具向服务器上传的数据量是十分巨大的。若是用户链接着 wifi 没什么大不了的,但可能对于在使用手机流量的用户确实是一个问题。最后可是也很重要的一点,你也许泄露了伴随着状态的敏感数据的崩溃日志。要么就不要在上传的崩溃报告中去序列化那些敏感的数据(所以报告可能不完整而且几乎没啥用),要么就将这敏感数据加密(这可能须要一些额外的CPU时间)。

总结一下:就我我的而言,在给个人 app 作快照处理时我发现了不少益处,然而,你也不得不作一些权衡.也许你能够在内部版本或者 beta 版本上启用快照功能,看看在你本身的 app 上工做得如何。

红利:时间旅行

在开发时,若是能够拥有时间旅行的选择项,岂不美哉。也许嵌入一个调试侧边栏像 Jake Wharton 的 u2020 dome app。

全部咱们须要相似于调试侧边栏只须要两个按钮“前一个状态”和“后一个状态”所以咱们能够一步一步地从一个状态及时的到前一个状态(或下一个状态)。例如:若是咱们已经作了一个 HTTP 请求做为状态变化的一部分,能够肯定的是,在往前回溯时,咱们并不想再次进行真正的 http 请求,由于与此同时后端的数据也可能会发生变化。

时间旅行要求一些额外的层,像一个代理层在一个 app 的边界部分。所以咱们能够“录制”和“回放”状态像 http 请求(同理 sqlite等等)。对这类事情十分的感兴趣?这就像个人朋友 Felipe 为OKHttp作相似的事情。能够随意联系他来获得他正在写的库的更多细节。

Snipaste_2018-03-07_11-40-30.png

你是否正在找一个十分有用的安卓库,能够录制和回放 OkHttp 网络交互,好比说 Espresso 测试?

— Felipe Lima (@felipecsl) 28. Februar 2017

这篇博客是使用 MVI 开发响应式 APP 的一部分。 这里是内容表:

这是中文翻译:


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索