[关闭]
@SR1s 2018-02-22T15:49:53.000000Z 字数 3097 阅读 4086

Kotlin,让startActivity用起来更顺手

Kotlin android-ktx


在Google宣布将Kotlin作为Android开发的第一语言之后,Android官方发布了android-ktx项目。这个项目利用Kotlin语言灵活的语法特性,给Android API增加了一系列的简洁好用的拓展函数,让开发者用起来更顺手、代码更加简单紧凑,同时,这个项目也在Github上开源。

社区开发者也纷纷为这个项目添砖加瓦做贡献。有一个给Intent添加拓展函数的讨论很有意思。

buildIntent

题主glidor认为Intent在Android API里很常见,但用起来不顺手。受Kotlin标准库里类似buildString拓展函数的启发,他提议,ktx里添加一个类似的buildIntent的拓展函数。

用法大致如下:

  1. val intent = buildIntent {
  2. action = "some action"
  3. data = "http://example.com".toUri()
  4. putExtra("name", value)
  5. }

实现的话也不难:

  1. inline fun buildIntent(initializer: Intent.() -> Unit): Intent {
  2. return Intent().apply(initializer)
  3. }

一般使用Intent还会指定要启动的Activity,这个场景,这个哥们给了这样的一个思路:

  1. // 用法
  2. val intent = buildIntent<MyActivity>(context) {
  3. putExtra("name", value)
  4. }
  5. // 实现
  6. inline fun <reified T : Activity> buildIntent(context: Context, initializer: Intent.() -> Unit): Intent {
  7. return Intent(context, T::class.java).apply(initializer)
  8. }

确实,相比之前的使用方式,这种方式使得代码更加简练,使用起来也顺手很多,可读性也提高了不少。

startActivity

后来,这个哥们想着不如更近一步,构造完intent之后,直接启动Activity好了,于是有了下面这个用例:

  1. // 用法
  2. activity.startActivity<MyActivity> {
  3. putExtra("name", "value")
  4. }
  5. // 实现
  6. inline fun <reified T : Activity> Activity.startActivity(initializer: Intent.() -> Unit) {
  7. startActivity(
  8. Intent(this, T::class.java).apply(initializer)
  9. )
  10. }

然后有另一个哥们dovahkiin98,觉得一直调用putExtra也很繁琐,针对这个情况,提出了使用key - value可变参数的版本:

  1. // 用法
  2. activity.startActivity<MyActivity>(
  3. "foo" to "bar",
  4. "theAnswer" to 42
  5. )
  6. // 实现
  7. inline fun <reified T : Activity> Activity.startActivity(vararg args: Pair<String, Any>) {
  8. val intent = Intent(this, T::class.java)
  9. intent.putExtras(bundleOf(*args))
  10. startActivity(intent)
  11. }

这时候,又有个叫peikedai的哥们跳出来,反对上面这种用法。倒不是说这个的实现有问题,他觉得这种用法不方便。IntentBundle有点像Map<String, Any?>,往里面存数据需要指定数据的key,业务在维护数据之外,还要维护这个key的字符串,这个就让人蛋疼了。他提议,将参数存储在Parcelable对象里进行传递:

  1. // 用法
  2. activity.startActivity<MyActivity>(Client("Alice", 15))
  3. val parameter = activity.getParameter<Client>()
  4. @Parcelize
  5. data class Client(val name: String, val age: Int): Parcelable
  6. // 实现
  7. inline fun <reified T : Activity> Activity.startActivity(parameter: Parcelable) {
  8. val intent = Intent(this, T::class.java).apply {
  9. val name = getExtraName(T::class.java)
  10. putExtra(name, parameter)
  11. }
  12. startActivity(intent)
  13. }
  14. inline fun <reified T : Parcelable> Activity.getParameter(): T {
  15. val name = getExtraName(this::class.java)
  16. return intent.getParcelableExtra<T>(name)
  17. }
  18. fun getExtraName(target: Class<out Activity>): String {
  19. return "${target.canonicalName}.Parameter"
  20. }

这种方式更符合面向对象的思想,在Java里也能这么用,这也是笔者一直以来的用法,只不过在Java里这种用法太过繁琐:实现Parcelable要写相当多的代码,在简单的场合,还不如使用key - value的方式来得方便。Kotlin的Android Plugin会自动生成Parcelable的实现,这使得使用这种方式更加方便了。

对于key - value可变参数的版本,题主glidor提了三点意见反对:
1. 相比原有的putExtra调用,可变参数的版本创建了额外的对象:每个key - value对分别创建一个对象,这些对象凑成的数组对象,还有bundleOf方法内部创建的Bundle对象,这些都会带来额外的开销。
2. 同样的,这种方式丧失了一定的灵活性。buildIntent的方式,本质上是一个代码块,可以在代码块里做运算和逻辑判断,而key - value可变参数的版本则不行。
3. 这个解决方案是类型不安全的,只有基础类型、ParcelabelSerializable这些能传递,而实现上的参数类型声明却是Any,即允许任意类型。

dovahkiin98对第一点没做反驳,他认为,这些拓展函数是可以共存的,仅仅需要传递key - value对时,可以用key - value可变参数版本,如果需要对初始化过程做更多的控制,可以用提供了初始化器的版本。至于类型不安全,他指出ktx里的bundleOf拓展方法就是这么实现的:允许传入任意类型的数据,bundleOf内部会做类型检查,遇到不合适的类型直接抛异常。

后记

至此,讨论就暂告一段落了。从上述的讨论,可以看到,在Kotlin强大的语言特性支持之下,能够帮助开发者以更简洁高效的方式来编码,同时也应该注意到,Kotlin的语言特性虽然支持我们自定义更简洁的API接口,但也可能在不经意间引入了性能问题,这点需要开发者在编码的过程中留意和权衡。

后面官方会以哪种方式实现buildIntentstartActivity目前还不得而知,如果觉得上面的方案有用,大家也可以先行在项目里用着。倘若有其他想法,不妨留言里一起探讨:-)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注