opoojkk

Android 启动 Activity 的多种方式

lxx
目次

我们知道,Activity 是 Android 界面层的核心承载形式,几乎所有用户可感知的内容,最终都会落在 Activity 上。所有的内容都是基于Activity的,无论是内容、弹窗,随着官方进一步收紧安全策略,除系统应用外,普通应用更难仅仅依靠无页面的Service形式长期存在。Activity之于Android的重要性自然不言而喻。

那么,Activity是怎么启动的呢?

原生方式 #

原生方式我们通常分成两种:显示启动和隐式启动。

显示启动 #

显示启动即需要知道要启动的Activity是哪个,是最最常用的当属官方提供的方式,写起来是这样:

1
2
val intent = Intent(context, DetailActivity::class.java)
context.startActivity(intent)

当需要携带数据或是设置flag时,无非是多写两行代码,就变成了这样:

1
2
3
4
5
6
7
val intent = Intent(context, DetailActivity::class.java).apply{
  // 携带数据
  putExtra("name", "小明")
  // 设置flag
  addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)

这种方式非常灵活,每次启动添加参数、flag非常方便,编译器就能知道启动的Activity是否存在。 除了出现过的这些,可以使用的还有设置luanchMode、添加动画甚至是权限,也可以使用ActivityResultLauncher接受新Activity的结果(startActivityForResult被弃用,不推荐使用)。

如果全都好,也就不会有这篇文章了对吧。它的不足是很繁琐,即使是按照相同的方式启动一个带有相同参数的Activity也要再写一遍,违反了DRY(Don’t Repeat Yourself)原则。

隐式启动 #

隐式启动和显示启动对应,也就是不知道要启动哪个具体Activity,只关心有什么特点,使用场景通常有几种:

  1. 基础功能Activity
  2. 外部Activity(三方登录)
  3. DeepLink(Scheme)

好处是调用方不需要关心启动的是Activity名称,携带必要的参数即可。略有的不足是编译期无法校验 Intent 是否能被正确解析,只有在运行时由系统进行匹配,问题往往要到执行阶段才能暴露。

从H5中执行切携带参数时可能存在参数编码问题,需要编码,可以使用Uri.parse或是Uri.appendQueryParameter方法

封装 #

工厂方法 #

封装中用到最多的当属工厂方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class DetailActivity : AppCompatActivity() {

    companion object {
        fun start(context: Context, id: Long) {
            context.startActivity(
                Intent(context, DetailActivity::class.java).apply {
                    putExtra("id", id)
                }
            )
        }
    }
}

这里是start,也可能是launch等等,将一个Activity启动的方法收敛到一个方法中,好处是很容易找到哪里启动,不足是通常不能满足所有情况,Kotlin中支持默认参数使用起来稍微些,Java中更严重,当需要不同参数情况很多时,往往要定义很多个相同名称的方法满足业务需要,看上去很乱。另外,一旦在 start 方法中掺杂业务判断,Activity 很容易从“页面”变成承载逻辑的入口,反而增加维护成本。

虽然封装了,使用时也一样是需要有所在模块的引入才能使用。

DSL / Builder #

这是我个人最喜欢的方式,利用Kotlin的扩展方法和reified关键字写出类似模板的代码。

写法像是这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 不设置flag
context.startActivity<DetailActivity> {
    long("id", 1)
}

// 设置flag
context.startActivity<DetailActivity> (options={
  flag(Intent.FLAG_ACTIVITY_NEW_TASK)
},extra = {
  long("id", 1)
})

本质上只是封装了显示启动,写法更优雅,其他方面并无差异。一样支持编译器检查,一样需要依赖Activity所在模块。

完整定义可以参考我的Gist:opoojkk/ActivityKt.kt

三方库 #

当然可以说,本质上还是封装显示启动,不过是程度不同而已,当然是这样,要是这么说,就不存在其他的方式了,毕竟Android只支持显示启动和隐式启动🤷‍♀️。

拿使用最多的框架ARouter举例,启动上的确是封装了显示启动,写法上倒是有几分像隐式启动中的DeepLink:

1
2
3
4
ARouter.getInstance()
    .build("/picture/detail")
    .withLong("id", 123)
    .navigation()

好处是写法上足够简单清晰,一目了然,带了哪些参数路径是什么,更重要的是不需要与要启动的Activity所在的模块有耦合,只关心启动,由框架完成注册。也因此支持在组件化工程中使用。 不足嘛,硬要挑还是不能在编译器检查,传入的路径仍然是字符串。如果支持了,恐怕也没办法支持组件化了。

对比 #

方式编译期校验参数集中管理使用成本组件化友好灵活性适用场景
原生显示启动小型项目、简单页面跳转
原生隐式启动系统能力、三方应用、DeepLink
工厂方法参数固定、跳转规则稳定的页面
DSL / BuilderKotlin 项目、追求代码整洁度
ARouter 等三方库组件化、多模块、大型工程

说明:

标签:
Categories:

评论

评论由 giscus 提供;如果未加载,可能是网络环境阻止了 giscus。