如何优雅地构建易维护、可复用的 Android 业务流程

本文首发:http://prototypez.github.io/2018/04/30/best-practices-of-android-process-management/

有一定实际 Android 项目开发经验的人,一定曾经在项目中处理过很多重复的业务流程。例如开发一个社交 App ,那么出于用户体验考虑,会需要允许匿名用户(不登录的用户)可以浏览信息流的内容(或者只能浏览受限的内容),当用户想要进一步操作(例如点赞)时,提示用户需要登录或者注册,用户完成这个流程才可以继续刚刚的操作。而如果用户需要进行更深入的互动(例如评论,发布状态),则需要实名认证或者补充手机号这样的流程完成才可以继续操作。

而上面列举的还只是比较简单的情况,流程之间还可以互相组合。例如:匿名用户点击了评论,那么需要连续做完:

  1. 登录/注册
  2. 实名认证

这两个流程才可以继续评论某条信息。另外 1 中,登录流程还可能嵌套“忘记密码”或者“密码找回”这样的流程,也有可能因为服务端检测到用户异地登录插入一个两步验证/手机号验证流程。

需要解决的问题

(一) 流程的体验应当流畅

根据本人使用市面上 App 的经验,处理业务流程按体验分类可以分为两类,一种是触发流程完成后,回到原页面,没有任何反应,用户需要再点一下刚才的按钮,或者重新操作一遍刚才触发流程的行为,才能进行原来想要的操作。另外一种是,流程完成后,如果之前不满足的某些条件此时已经满足,那么自动帮用户继续刚刚被打断的操作。显然,后一种更符合用户的预期,如果我们需要开发一个新的流程框架,那么这个问题需要被解决。

(二) 流程需要支持嵌套

如果在进行一个流程的过程中,某些条件不满足,需要触发一个新的流程,应当可以启动那个流程,完成操作,并且返回继续当前流程。

(三) 流程步骤间数据传递应当简单

传统 Activity 之间数据传递是基于 Intent 的,所以数据类型需要支持 Parcelable 或者 Serializable,并且需要以 key-value 的方式往 Intent 内填充,这是有一定局限性的。此外,流程步骤间有些数据是共享的,有些是独有的,如何方便地去读写这些数据?

有人可能会说,那可以把这些数据放到一个公共的空间,想要读写这些数据的 Activity 自行访问这些数据。但是如果真的这样,带来的新问题是:应用进程是可能在任意时刻销毁重建的,重建以后内存中保存的这些数据也消失了。如果不希望看到这样,就需要考虑数据持久化,而持久化的数据也只是被这一次流程用到,何时应该销毁这些数据?持久化的数据需要考虑自身的生命周期的问题,这引入了额外的复杂度。且并没有比使用 Intent 传递方便多少。

(四) 流程需要适应 Android 组件生命周期

前面说到了应用进程销毁重建的问题,由于很多操作触发流程以后,启动的流程页面是基于 Activity 实现的,所以完成流程回到的 Activity 实例很有可能不是原来触发流程时的那个 Activity 实例,原来那个实例可能已经被销毁了,必须有合适的手段保证流程完成后,回到触发流程的页面可以正确恢复上下文。

(五) 流程需要可以简单复用

还有流程往往是可以复用的,例如登录流程可以在应用的很多地方触发,所以触发后流程结束以后的跳转页面也都是不一样的,不可以在流程结束的页面写死跳转的页面。

(六) 流程页面在完成后需要比较容易销毁

流程结束以后,流程每个步骤页面可以简单地销毁,回到最初触发流程的界面。

(七) 流程进行中回退行为的处理

如果一个流程包含多个中间步骤,用户进行到中间某个步骤,按返回键时,行为应该如何定义?在大多数情况下,应该支持返回上一个步骤,但是在某些情况下,也应当支持直接返回到流程起始步骤。

方案一:基于 startActivityForResult

其实说起流程这个事情,我们最容易想到的应该就是 Android 原生提供给我们的 startActivityForResult方法,以 Android 官网中的一个例子(从通讯录中选择一个联系人)为例:

static final int PICK_CONTACT_REQUEST = 1;  // The request code
...
private void pickContact() {
      
    Intent pickContactIntent = new Intent(Intent.ACTION_PICK, Uri.parse("content://contacts"));
    pickContactIntent.setType(Phone.CONTENT_TYPE); // Show user only contacts w/ phone numbers
    startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
      
    // Check which request we're responding to
    if (requestCode == PICK_CONTACT_REQUEST) {
      
        // Make sure the request was successful
        if (resultCode == RESULT_OK) {
      
            // The user picked a contact.
            // The Intent's data Uri identifies which contact was selected.

            // Do something with the contact here (bigger example below)
        }
    }
}

在上面的例子中,当用户点击按钮(或者其他操作)时,pickContact 方法被触发,系统启动通讯录,用户从通讯录中选择联系人以后,回到原页面,继续处理接下来的逻辑。从通讯录选择用户并返回结果 就可以被看作为一个流程。

不过上面的流程是属于比较简单的情况,因为流程逻辑只有一个页面,而有时候一个复杂流程可能包含多个页面:例如注册,包含手机号验证界面(接收验证码验证),设置昵称页面,设置密码页面。假设注册流程是从登录界面启动的,那么使用 startActivityForResult 来实现注册流程的 Activity 任务栈的变化如下图所示:

上图的注册流程实现细节如下:

  1. 登录界面通过 startActivityForResult 启动注册页面的第一个界面 —- 验证手机号;
  2. 手机号验证成功后,验证手机号界面通过 startActivityForResult 启动设置昵称页面;
  3. 昵称检查合法后,昵称信息通过 onActivityResult 返回给验证手机号界面,验证手机号界面通过 startActivityForResult 启动设置密码界面,由于设置密码是最后一个流程,验证手机号界面把之前收集好的手机号信息,昵称信息都一并传递给密码界面,密码检查合法后,根据现有的手机号、昵称、密码发起注册;
  4. 注册成功后,服务器返回注册用户信息,设置密码界面通过 onActivityResult 把注册结果反馈给设置手机号界面;
  5. 注册成功,设置手机号界面结束自己,同时把注册成功信息通过 onActivityResult 反馈给流程发起者(本例中即登录界面);

通过这个例子可以看出来,手机号验证界面 不仅承担了在注册流程中验证手机号的功能,还承担了注册流程对外的接口的职责。也就是说,触发注册流程的任意位置,都不需要对注册流程的细节有任何了解,而只需要通过 startActivityForResult 和 onActivityResult 与流程对外暴露的 Activity 交互即可,如下图:

上面的例子中可能有一点令您疑惑:为什么每个步骤需要返回到验证手机号页面,然后由验证手机号页面负责启动下个步骤呢?一方面,由于验证手机号是流程的第一个页面,它承担了流程调度者的身份,所以由它来进行步骤的分发,这样的好处是每个步骤(除了第一步)之间是解耦和内聚的,每个步骤只需要做好自己的事情并且通过 onActivityResult 返回数据即可,假如后续流程的步骤发生增删,维护起来比较简单;另一方面,由于每个步骤做完都返回,当最后一个步骤做完以后,之前流程的中间页面都不存在了,不需要手动去销毁做完的流程页,这样编码起来也比较方便。

但是这么做带来一个小小的副作用:如果在流程的中间步骤按返回键,就会回到流程的第一个步骤,而用户有时候是希望可以回到上一个步骤。为了让用户可以在按返回键的时候返回上一个步骤,就必须要把每个步骤的 Activity 压栈,但是这样做的话最后一步做完之后如何销毁流程相关的所有 Activity 又是一个问题。

为了解决流程相关 Activity 的销毁问题,需要对上面的图做一点修改,如下:

原先,每个步骤做完自己的任务以后只需要结束自己并返回结果,修改后,每个步骤做完自己的任务后不结束自己,也不返回结果,同时需要负责启动流程的下一个步骤(通过 startActivityForResult),当它的下一个步骤结束并返回它的结果的时候,这个步骤能在自己的 onActivityResult 里接住,它在onActivityResult里需要做的是把自己的结果和它的下一个步骤的结果合在一起,传递给它的上一个步骤,并结束自己。

通过这样,实现了用户按返回键所需要的行为,但是这种做法的缺点是造成了流程内步骤间的耦合,一方面是启动顺序之间的耦合,另一方面由于需要同时携带它下个步骤的结果并返回造成的数据的耦合。

除此以外我还见过有人会单独使用一个栈,来保存流程中启动过的 Activity , 然后在流程结束后自己去手动依次销毁每个 Activity。我不太喜欢这种方法,它相比上面的方法没有解决实质问题,而且需要额外维护一个数据结构,同时还要考虑生命周期,得不偿失。

最后总结一下前文, startActivityForResult 这个方法有着它自己的优势:

  1. 足够简单,原生支持。
  2. 可以处理流程返回结果,继续处理触发流程前的操作。
  3. 流程封装良好,可复用。
  4. 虽然引入了额外的 requestCode,但是在某种程度上保留了请求的上下文。

但是这个原生方案存在的问题也是显而易见的:

  1. 写法过于 Dirty,发起请求和处理结果的逻辑被分散在两处,不易维护。
  2. 页面中如果存在的多个请求,不同流程回调都被杂糅在一个 onActivityResult 里,不易维护。
  3. 如果一个流程包含多个页面,代码编写会非常繁琐,显得力不从心。
  4. 流程步骤间数据共享基于 Intent,没有解决 问题(三)
  5. 流程页面的自动销毁和流程进行中回退行为存在矛盾,问题(六) 和 问题(七) 没有很好地解决。

实际开发中,这几个问题都非常突出,影响开发效率,所以无法直接拿来使用。

方案二:EventBus 或者其他基于事件总线的解决方案

基于事件解耦也是一种比较优雅的解决方案,尤其是著名的 EventBus 框架了,它实现了非常经典的发布订阅模型,完成了出色的解耦:

我相信很多 Android 开发者都曾经很愉快地使用过这个框架……………………………………最后放弃了它,或者只在小范围使用它。比如说我,目前已经在项目中逐渐删除使用 EventBus 的代码,并且使用 Rx

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值