代码灾区?
在我们 Android 开发中,不可避免的一个情景是在 Activity 跳转的时候传数据。比如从一个 FromActivity 界面,跳转到一个 OtherActivity 界面,需要传 username
和 password
,一般我们的写法是这样的:
val intent = Intent(this, OtherActivity::class.java)
.putExtra("username", username)
.putExtra("password", password)
this.startActivity(intent)
但当随着应用的迭代,需要跳转的界面增加之后,上面的一些问题就会显露出来。比如有多个界面会跳转到这里的 OtherActivity 来,并且有些界面会需要再传个 mobile
,或者有的界面还需要一个 sms_code
,如果每个界面都把跳转所需要传的数据写在自己页面上,那么就很易发生两个问题:
一是调用 putExtra(key, value)
时由于 key 是硬编写在各自的文件上,写错了就会导致另一个界面拿不到数据的问题。二是当以后需求变化页面重构,有些数据不需要传或者是想修改统一一下key的时候,就要查到其他跳转到这个界面的代码,去修改这些 putExtra
的代码了,而且也不利于尽快熟悉都有哪些界面跳转到这个界面上来。
尝试的改进
对于第一个问题,比较好解决。我们可以将这个硬编码的字符串抽出进行常量声明。而第二个问题,从决定方的角度来思考,我们从 FromActivity 跳转到 OtherActivity,所传的数据,那是 OtherActivity 所期望及能够处理的,而不是 FromActivity 想怎样传就怎样传。也就是能传什么数据,key 是什么,类型是什么,应该是由 OtherActivity 来决定的,而不是由跳转到它的界面。既然这样,那么我们就应该把跳转的逻辑实现在 OtherActivity。所以后来,我们会把代码改善为如下:
public static void start(Context context, String username, String password) {
final Intent intent = new Intent(context, OtherActivity.class)
.putExtra(Const.USERNAME, username)
.putExtra(Const.PASSWORD, password);
context.startActivity(intent);
}
public static void start(Context context, String username, String password, String mobile) {
final Intent intent = new Intent(context, OtherActivity.class)
.putExtra(Const.USERNAME, username)
.putExtra(Const.USERNAME, password)
.putExtra(Const.MOBILE, mobile)
context.startActivity(intent)
}
这里的静态方法是声明在 OtherActivity 中的。上面是 Java 写的例子,如果需要的参数不同,方法重载还是有些啰嗦的,用 kotlin 就会相对简洁一点,但是类里的静态方法就需要声明在 companion object
下,如下所示:
companion object {
fun startActivity(context: Context, username: String, password: String, mobile: String? = null) {
val intent = Intent(context, OtherActivity::class.java)
.putExtra(Const.USERNAME, username)
.putExtra(Const.PASSWORD, password)
.putExtra(Const.MOBILE, mobile)
context.startActivity(intent)
}
}
但是对于每个接收数据跳转的 Activity,就都需要声明 companion object
,然后写一个跳转方法,方法体里面都是诸如 putExtra(key, value)
方法的调用,这其实也是一种重复性工作。它还能不能再简洁一些呢?
Dinny
从 Retrofit 那里得到思路,能不能把这些静态方法的定义变成像 Retrofit 那样的接口声明呢?比如变成下面这样:
interface Protocol {
@ToActivity(OtherActivity::class)
fun mainIntent(from: Context, @Extra("key") extra: Type): Intent
@ToActivity(OtherActivity::class)
fun startMain(from: Context, @Extra("key") extra: Type)
}
我们声明一个接口,接口里的方法用 @ToActivity
注解指定所要跳转的界面,参数用 @Extra
指定 putExtra(key, value)
中的 key,并且约定第一个参数为 Context
或其子类。而如果需要得到一个 Intent
来进行其他操作比如添加 flag 或者添加一些转场动画,就声明返回类型为 Intent
,如果要直接跳转就让返回类型为空 void
,那么代码就简洁多了。
基于以上设计,我实现了 Dinny 。
基于 Dinny 的做法是,把一个功能模块下接收数据进行界面跳转的方法都声明在这个功能模块下的一个接口中,然后由 Dinny 代理实现。此时,这个接口就成了这个模块向外开放的界面跳转的一个协议,它声明了从其他界面跳转到这个模块里的界面所要传的数据类型。因此,我们不但减少了一些重复性代码,而且使得一个模块下的界面跳转得到了集中管理,便于快速了解这个模块内哪些界面跳转需要数据并且需要怎样的数据类型。
使用 Dinny,前面的代码可以演变为:
interface UserProtocol {
@ToAcTivity(OtherActivity::class)
fun toOther(context: Context, @Extra(Const.USERNAME) username: String,
@Extra(Const.PASSWORD) password: String, @Extra(Const.MOBILE) mobile: String? = null)
// 其他接口 ...
}
object UserModule : UserProtocol by Dinny.create(UserProtocol::class.java)
而当我们需要跳转的时候,只需要调用:
UserModule.toOther(this, username, password)
下面是实际项目的一个模块声明:
internal interface RecordProtocol {
/* payment */
@ToActivity(PayDetailActivity::class)
fun showPayDetail(context: Context, @Extra(Extras.DATA) record: PayRecord, @Extra(Extras.PARK_CODE) parkCode: String)
@ToActivity(SearchPayRecordActivity::class)
fun startSearchPayRecord(context: Context, @Extra(Extras.PARK_CODE) parkCode: String)
/* tracking */
@ToActivity(VehicleDetailActivity::class)
fun showVehicleDetail(context: Context, @Extra(Extras.PARK_CODE) parkCode: String, @Extra(Extras.DATA) vehicle: String)
/* traffic */
@ToActivity(AbnormalRecordsActivity::class)
fun showAbnormalRecords(context: Context, @Extra(Extras.PARK_CODE) parkCode: String, @Extra(Extras.DATE) date: String)
@ToActivity(RecordDetailActivity::class)
fun showParkRecordDetail(context: Context, @Extra(Extras.DATA) record: ParkRecord)
@ToActivity(SearchTrafficActivity::class)
fun startSearchTrafficRecord(context: Context)
}
object RecordModule : RecordProtocol by Dinny.create(RecordProtocol::class.java)
可以看到一个模块下的界面跳转都得到了简洁清晰的体现。
Dinny 项目地址:https://github.com/msdx/dinny
Gradle 依赖:
implementation 'com.githang:dinny:0.1.1'