Android最全如何优雅地构建易维护、可复用的 Android 业务流程(二),2024年最新阿里的面试官

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

new ActivityResult(REQUEST_CODE_LOGIN, RESULT_OK, new Intent())
);
} else {
Intent intent = new Intent(activity, LoginActivity.class);
ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_CODE_LOGIN);
}
return ActivityResultFragment.getActivityResultObservable(activity)
.filter(ar -> ar.getRequestCode() == REQUEST_CODE_LOGIN)
.filter(ar -> ar.getResultCode() == RESULT_OK);
}
}

这个 Activity 对外提供一个静态的 loginState 方法,返回类型为 Observable<ActivityResult>,在已经登录的情况下,Observable 会立即发送一个 ActivityResult 表示登录成功,在非登录态下, 会唤起登录流程,如果登录流程最后的结果是登录成功,Observable 也会发送一个 ActivityResult 表示登录成功,所以凡是使用到这个登录流程的地方,对这个登录流程的调用,应该如下代码所示(仍然以点赞操作为例):

likeBtn.setOnClickListener(v -> {
LoginActivity.loginState(this)
.subscribe(this::doLike);
})

原先需要书写复杂的 startActivityForResultonActivityResult 两个方法,才能完成和登录流程交互,而且还需要在发起流程前先确认是否当前已经是登录态,现在只需要一行 LoginActivity.loginState(), 然后指定一个 Observer 即可达到一样的效果,更重要的是,写法变简单了以后,整个登录流程变得非常容易复用,任何需要检查登录态然后再做操作的地方,都只需要这一行代码即可完成登录态检测,实现了 流程的高度可复用

这种写法可以在流程完成以后,继续之前的操作(例如本例中点赞),不需要用户重新进行一遍先前被流程所打断的操作(例如本例中的点赞操作)。但是细心的你可能并不这么认为,因为上面的代码本质上是有问题的,问题在于在上面 LoginActivity.loginState() 的调用在 likeBtn.setOnClickListener 的内部回调,那么考虑极端情况,如果登录流程被唤起,而发起登录流程的 Activity 不幸被系统回收,那么当登录流程做完回到的发起登录流程的 Activity 将会是系统重新创建的 Activity,这个全新的 Activity 是没有执行过 likeBtn.setOnClickListener 的内部回调的任何代码的,所以 .subscribe() 方法指定的观察者不会受到任何回调,this::doLike 不会被执行。

为了可以让封装的流程兼容这种情况,可以采用这种方案:修改 loginState 方法,使其返回 ObservableTransformer, 我们重命名 loginState 方法为 ensureLogin:

public static ObservableTransformer<T, ActivityResult> ensureLogin(Activity activity) {
return upstream -> {
Observable loginOkResult = ActivityResultFragment.getActivityResultObservable(activity)
.filter(ar -> ar.getRequestCode() == REQUEST_CODE_LOGIN)
.filter(ar -> ar.getResultCode() == RESULT_OK);

upstream.subscribe(t -> {
if (LoginInfo.isLogin()) {
ActivityResultFragment.insertActivityResult(
activity,
new ActivityResult(REQUEST_CODE_LOGIN, RESULT_OK, new Intent())
);
} else {
Intent intent = new Intent(activity, LoginActivity.class);
ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_CODE_LOGIN);
}
}

return loginOkResult;
}
}

如果您之前没有接触过 ObservableTransformer, 这里做一个简单介绍,它通常和 compose 操作符一起使用,用来把一个 Observable 进行加工、修饰,甚至替换为另一个 Observable

登录流程的封装,现在对外体现为 ensureLogin 这一个方法,那么其它代码如何调用这个登录流程呢,还是以点赞操作为例,现在的代码应该是这样:

RxView.clicks(likeBtn)
.compose(LoginActivity.ensureLogin(this))
.subscribe(this::doLike);

这里的 RxView.clicks 使用了 RxBinding 这个开源库,用于把 View 的事件,转化为 Observable,当然其实你也可以自己封装。改完这种写法以后,刚刚提到的极端情况下也可以正常工作了,即使发起流程的页面在流程被唤起后被系统回收,在流程完成以后回到发起页,发起页被重新创建了,发起页的 Observer 依然可以正常收到流程结果,之前被中端的操作得以继续执行。

现在我们可以稍微总结一下,根据上一篇和本篇提出建议,如何封装一个业务流程:

  1. 一个业务流程对应一个 Activity,这个 Activity 作为对外的接口以及流程内部步骤的调度者;
  2. 一个流程内部的一个步骤对应一个 Fragment,这个 Fragment 只负责完成自己的任务以及把自己的数据反馈给 Activity;
  3. 流程对外暴露的接口应该封装为一个 ObservableTransformer,流程发起者应该提供发起流程的 Observable(例如以 RxView.clicks 的形式提供),两者通过 compose 操作符关联起来。

这是我个人实践出的一套封装流程的经验,它并不是完美的方案,但是在可靠性、可复用程度、接口简单程度上已经足以胜任我个人的日常开发,所以才有了这两篇分享。

我们已经封装了一个最简单的流程 – 登录流程,但是实际项目中往往会遇到更严峻的挑战,例如流程组合与流程嵌套。

复杂流程实践:流程组合

举例:某款基金销售 App,在用户点击购买基金时,可能存在如下图流程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可用从上图中看出,某个未登录用户想要购买一款基金的最长路径包含:登录 - 绑卡 - 风险测评 - 投资者适当性管理 这几个步骤。但是,并不是所有用户都要经历所有这些步骤,例如,如果用户已登录并且已做过风险测评,那这个用户只需要再做 绑卡 - 适当性管理 这两步就可以了。

这样的一个需求,如果用传统的写法来写,可以预见肯定会在 click 事件处理的地方罗列很多 if - else

// …
// 设置点击事件处理函数
buyFundBtn.setOnClickListener(v -> handleBuyFund());

// …
// 处理结果
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_LOGIN:
case REQUEST_ADD_BANKCARD:
case REQUEST_RISK_TEST:
case REQUEST_INVESTMNET_PROMPT:
if (resultCode == RESULT_OK) handleBuyFund();
break;
}
}

// …

private void handleBuyFund() {
// 判断是否已登录
if (!isLogin()) {
startLogin();
return;
}
// 判断是否已绑卡
if (!hasBankcard()) {
startAddBankcard();
return;
}
// 判断是否已做风险测试
if (!isRisktestDone()) {
startRiskTest();
return;
}
// 判断是否需要按照投资者适当性管理规定,给予用户必要提示
if (investmentPrompt()) {
startInvestmentPrompt();
return;
}

startBuyFundActivity();
}

上面这种写法一方面代码比较长,另一方面,流程发起和结果处理分散在两处,代码较为不易维护。我们分析一下,整个大的流程是几个小流程的组合,我们可以把上面的图上的流程换一种画法:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

按照上文的思想,我们令每个流程对外暴露一个 Activity,并且已经使用 RxJava ObservableTransformer 封装好,那么前面复杂的代码可以简化为:

RxView.clicks(buyFundBtn)
// 确保未登录情况下,发起登录流程,已登录情况下自动流转至下一个流程
.compose(ActivityLogin.ensureLogin(this))
// 确保未绑卡情况下,发起绑卡流程,已绑卡情况下自动流转至下一个流程
.compose(ActivityBankcardManage.ensureHavingBankcard(this))
// 确保未风险测评情况下,发起风险测评流程,已测评情况下自动流转至下一个流程
.compose(ActivityRiskTest.ensureRiskTestDone(this))
// 确保需要适当性提示情况下,发起适当性提示,已提示或不需要提示情况下自动流转至下一个流程
.compose(ActivityInvestmentPrompt.ensureInvestmentPromptOk(this))
// 所有条件都满足,进入购买基金页
.subscribe(v -> startBuyFundActivity(this));

通过 RxJava 的良好封装,我们做到了可以用更少的代码来表达更复杂的逻辑。上面的例子中的 4 个被组合的流程,它们有一个共同的特点,就是彼此独立,互相不依赖其它剩余流程的结果,现实中,我们可能会遇到这样的情况: B 流程启动,需要依赖 A 流程完成的结果,为了能满足这种情况,我们只需要对上面的封装稍作修改。

假设绑卡流程需要依赖登录流程完成后的用户信息,那么首先,在登录流程结束调用 setResult 的位置, 传递用户信息:

this.setResult(
RESULT_OK,
IntentBuilder.newInstance().putExtra(“user”, user).build()
);
finish();

然后,修改 ensureLogin 方法,使经过 ObservableTransformer 处理后,返回的新的 Observable 由发射 ActivityResult 改为发射 User

public static ObservableTransformer<T, User> ensureLogin(Activity activity) {
return upstream -> {
Observable loginOkResult = ActivityResultFragment.getActivityResultObservable(activity)
.filter(ar -> ar.getRequestCode() == REQUEST_CODE_LOGIN)
.filter(ar -> ar.getResultCode() == RESULT_OK)
.map(ar -> (User)ar.getData.getParcelableExtra(“user”));

upstream.subscribe(t -> {
if (LoginInfo.isLogin()) {
ActivityResultFragment.insertActivityResult(
activity,
new ActivityResult(
REQUEST_CODE_LOGIN,
RESULT_OK,
IntentBuilder.newInstance().putExtra(“user”, LoginInfo.getUser()).build()
)
);
} else {
Intent intent = new Intent(activity, LoginActivity.class);
ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_CODE_LOGIN);
}
}

return loginOkResult;
}
}

与此同时,原来的 ensureHavingBankcard 方法的 ObservableTransformer 方法接受的 Observable 原来是任意类型 T 的,由于我们现在规定,绑卡流程需要依赖登录流程的结果 User ,所以我们把 T 类型,改为 User 类型:

public static ObservableTransformer<User, ActivityResult> ensureHavingBankcard(Activity activity) {
return upstream -> {
Observable bankcardOk = ActivityResultFragment.getActivityResultObservable(activity)
.filter(ar -> ar.getRequestCode() == REQUEST_ADD_BANKCARD)
.filter(ar -> ar.getResultCode() == RESULT_OK);

upstream.subscribe(user -> {
if (getBankcardNum() > 0) {
ActivityResultFragment.insertActivityResult(
activity,
new ActivityResult(
REQUEST_ADD_BANKCARD,
RESULT_OK,
new Intent()
)
);
} else {
Intent intent = new Intent(activity, AddBankcardActivity.class);
intent.putExtra(“user”, user);
ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_ADD_BANKCARD);
}
}

return bankcardOk;
}
}

这样,这两个流程之间就有了依赖关系,绑卡依赖登录流程返回的结果,但是组合这两个流程的写法还是不会有任何改变:

RxView.clicks(someBtn)
.compose(ActivityLogin.ensureLogin(this))
.compose(ActivityBankcardManage.ensureHavingBankcard(this))
.subscribe(v -> doSomething());

除此以外,绑卡流程还是可复用的,它是依赖可以返回 User 的流程的,所以只要是其他可以返回 User 作为结果的流程,都可以与绑卡流程组合。

复杂流程实践:流程嵌套

举例:登录流程中的登录页面,除了可以选择用户名密码登录外,往往还提供其他选项,最典型的就是注册和忘记密码两个功能:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从直觉上,我们肯定是认为注册和忘记密码应该是不属于登录这个流程的,它们是相对独立的两个流程,也就是说在登录这流程内部,嵌入了其它的流程,我把这种情况称之为流程的嵌套。

按照同样的套路,我们应该先把注册、忘记密码这两个流程使用 ObservableTransformer 进行封装,然后我们把上图流程按照本文思想整理一下,如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,现在的区别是,发起流程的地方不再是一个普通的 Activity,而是另一个流程中的某个步骤,按照先前的讨论,流程中的步骤是由 Fragment 承载的。所以这里有两种处理方法,一种是 Fragment 把发起流程的任务交给宿主 Activity,由宿主 Activity 分配给属于它的“看不见的 Fragment” 去发起流程并处理结果,另一种是直接由该 Fragmnet 发起流程,由于 Fragment 也有属于它自己的 ChildFragmentManager,所以只需要对“使用 RxJava 进行封装”这一节中的相关方法做一些修改即可支持由 Fragment 内部发起流程,具体修改内容为把 activity.getFragmentManager() 改为 fragment.getChildFragmentManager() 即可。

在具体应用中,本人使用的是后一种,即 直接由 Fragment 发起流程,因为被嵌套的流程往往和主流程有关联,即嵌套流程的结果有可能改变主流程的流转分支,所以直接由 Fragment 发起流程并处理结果比较方便一点,如果交给宿主 Activity 可能需要额外多写一些代码进行 Activity - Fragment 的通信才能实现相同效果。

首先,在没有嵌套流程的情况下,登录流程的第一个步骤登录步骤(用户名、密码验证),代码应该如下:

public class LoginFragment extends Fragment {
// UI references.
private EditText mPhoneView;
private EditText mPasswordView;

LoginCallback mCallback;

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_login_by_pwd, container, false);
// Set up the login form.
mPhoneView = view.findViewById(R.id.phone);
mPasswordView = view.findViewById(R.id.password);

Button signInButton = view.findViewById(R.id.sign_in);
signInButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String phone = mPhoneView.getText().toString();
String pwd = mPasswordView.getText().toString();

if (mCallback != null) {
// mock login ok
mCallback.onLoginOk(true, new User(
UUID.randomUUID().toString(),
“Jack”,
mPhoneView.getText().toString()
));
}
}
});

return view;
}

public void setLoginCallback(LoginCallback callback) {
this.mCallback = callback;
}

public interface LoginCallback {
void onLoginOk(boolean needSmsVerify, User user);
}
}

在上面的代码中,LoginCallback 这个接口作用是,登录这个步骤,收集完信息,与服务器交互完毕后,把结果回传给宿主 Activity,由 Activity 决定后续步骤的流转。上面的例子中做了一部分简化,在 onClick 处理函数里没有发起和服务端的交互,而是直接 Mock 了一个请求成功的结果。

现在的需求是,在登录这个步骤里,嵌入两个步骤:

  1. 一个是注册流程,而且注册成功后直接视为登录成功,不需要再走剩余的登录流程步骤;
  2. 另一个是忘记密码流程,忘记密码流程本质是重置密码,但是即使密码重置成功还是需要用户使用新密码登录,不会直接在重置密码后自动登录。

根据需求,我们在上述代码中加入嵌入这两个流程的代码:

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_login_by_pwd, container, false);
// Set up the login form.
mPhoneView = view.findViewById(R.id.phone);
mPasswordView = view.findViewById(R.id.password);

Button signInButton = view.findViewById(R.id.sign_in);
Button mPwdResetBtn = view.findViewById(R.id.pwd_reset);
Button mRegisterBtn = view.findViewById(R.id.register);

// 直接登录
signInButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String phone = mPhoneView.getText().toString();
String pwd = mPasswordView.getText().toString();

if (mCallback != null) {
// mock login ok
mCallback.onLoginOk(true, new User(
UUID.randomUUID().toString(),
“Jack”,
mPhoneView.getText().toString()
));
}
}
});

// 发起注册流程
RxView.clicks(mRegisterBtn)
.compose(RegisterActivity.startRegister(this))
.subscribe(user -> {
if (mCallback != null) {
mCallback.onRegisterOk(user);
}
});

// 发起忘记密码流程
RxView.clicks(mPwdResetBtn)
.compose(PwdResetActivity.startPwdReset(this))
.subscribe();

return view;
}

public interface LoginCallback {
void onLoginOk(boolean needSmsVerify, User user);
void onRegisterOk(User user);
}

在上面的代码里,RegisterActivity.startRegisterPwdResetActivity.startPwdReset 两个方法即为使用了 ObservableTransformer 封装的注册流程和忘记密码流程。同时可以看到,LoginCallback 这个接口里多了一个方法 onRegisterOk,也就是说登录这个步骤不再只有 onLoginOk 这一种情况通知宿主 Activity 了,在内嵌注册流程成功的情况下,也可以通知宿主 Activity,然后让宿主 Activity 决定后续流转,当然这种情况,根据需求注册成功也是属于登录成功的一种,宿主 Activity 通过 setResult 方法把整个登录流程的状态标记为登录成功,finish 自己,同时把用户信息传递给发起登录流程的地方。

但是为什么内嵌的注册流程需要把流程的结果回传给登录流程的宿主 Activity,而内嵌的忘记密码流程没有设置一个类似的方法回调登录流程的宿主 Activity 呢?因为注册成功这件事影响了登录流程的走向(注册成功直接视为登录成功,登录流程状态置为成功,并通知发起登录流程的地方本次登录结果为成功),而忘记密码流程最后的重置密码成功并不影响登录流程走向(即使重置密码成功依然需要在登录界面使用新密码登录)。

按照上面的分析,登录流程的宿主 Activity,负责分发流程步骤的逻辑的相关代码如下所示:

public class LoginActivity extends Activity {

// …

@Override
protected void onCreate(Bundle savedInstanceState) {

//…
// 用户名密码验证步骤
loginFragment.setLoginCallback(new LoginFragment.LoginCallback() {
@Override
public void onLoginOk(boolean needSmsVerify, User user) {
// 用户名密码验证成功
if (needSmsVerify) {
// 需要短信验证码的两步验证
push(loginBySmsFragment);
} else {
// 登录成功
setResult(
RESULT_OK,
IntentBuilder.newInstance().putExtra(“user”, user).build()
);
finish();
}
}

@Override
public void onRegisterOk(User user) {
// 注册成功, 直接登录
setResult(
RESULT_OK,
IntentBuilder.newInstance().putExtra(“user”, user).build()
);
finish();
}
}

// 短信验证码两步验证步骤
loginBySmsFragment.setSmsLoginCallback(new LoginBySmsFragment.LoginBySmsCallback() {
@Override
public void onSmsVerifyOk(User user) {
// 短信验证成功
setResult(
RESULT_OK,
IntentBuilder.newInstance().putExtra(“user”, user).build()
);
finish();
}
});
}

// …
}

可以看到,即使是流程嵌套的情况下,使用 RxJava 封装的流程依然不会使流程跳转的代码显得十分混乱,这点十分可贵,因为这意味着今后流程相关代码不会成为项目中难以维护的模块,而是清晰且高内聚的。

文末

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

[外链图片转存中…(img-EtHi6QaU-1715241492128)]

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-Vh9OPFGl-1715241492129)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值