实战 kotlin@android(三):扩展变量与其它技巧

阅读数:2307 2016 年 5 月 19 日 17:20

这是 Kotlin 开发系列文章的第三篇,也是最后一篇。在前面的两篇文章中,我们探索了如何使用 Kotlin 来进行部分实用 Android 开发工作。如果你没有看过,建议可以先看一下:

到现在,我们已经可以使用比 XML 更少的代码完成 View 的构建,更别说 Java 了。Kotlin 的语法为声明式,View 之间的嵌套也十分清晰,而且我们还可以给类很方便地添加实用方法。

但在上一篇的结尾我们提到要给 View 设置左内边距并不容易实现。如果硬是要用 Kotlin 做这件事,就需要如下编码,注意其中我们需要调用 setPadding() 并传入四个参数,而不是给一个由 JavaBean 风格的 getter/setter 生成的模拟属性赋值。

为了使构建 View 的代码风格一致,我们更愿意直接给左内边距赋值,而不是调用有四个参数的方法。你可能想到给 View 类添加一个扩展方法 setleftPadding(int)。这样做 OK,但是你无法通过 lambda with receiver 这样写:leftPadding = dp_i(16),原因是由 Kotlin 生成的长相类似 JavaBean 的方法不会生成模拟变量,只有 Java 类中的成员方法可以。

但是,Kotlin 可以定义扩展属性,长得和 Java 类与方法中的变量差不多。一个扩展变量会像 Kotlin 扩展方法一样被移植到存在的类中,所以可以直接通过该类实例访问,只需你在代码中导入扩展变量。

我们可以定义一个扩展变量使得对于左内边距的设置代码能和其他属性设置代码风格一致。下面的代码设置了左内边距,其他类似:

所以 TextView 就可以这样创建了:

所有属性的设置代码风格变一致了,棒!

对于 padLeft 扩展属性有如下几点需要注意:

  • 扩展属性的注解是 class dot property,注意与扩展方法的 class dot function 注解区别开。

  • 扩展属性的类型在冒号后指定。

  • 通过 var 关键字声明,这也是 Kotlin 中声明可变变量的关键字,也就是可以直接赋值。如果要声明不可变的只读变量则使用 val 关键字。

  • 一个可变的扩展属性需要我们同时提供 getter 和 setter 的实现。

padLeft 的 setter 方法的实现基于 View 的 setPadding() 方法,它接收赋值语句右边的值作为第一个参数传入,并传入 TextView 的其他内边距属性的值。getter 方法的实现只不过将 View 内部的左内边距属性返回。

小吐槽:Android 的 View 类给内边距属性提供了 JavaBean 风格的 getter 方法,却没有提供 setter 方法,现在通过 Kotlin 我们化解了这个尴尬。

如果你觉得现有的哪个不可修改的 API 所提供的功能还不够,就可以通过 Kotlin 的扩展方法和扩展属性完善它。

中场

以上说的种种风格与语法都有助于以声明的方式动态构建 View 层级,使 Kotlin 成为一个“域确切”(domain specific)的语言。比如,综合使用上述所有功能,我们可以写一个设置了左内边距属性的两个 TextView:

就这一段代码而言还是不错的,而且肯定比传统方式强一些。不过,语法还能更加紧凑,还有提升的空间。

如何给特定的 layout 参数属性赋值?

从上面的代码中我们不容易发现,想给一个 LinearLayout 设置某个特定的 layout 参数是很难的。到现在为止,我们都在通过把给定 LinearLayout.LayoutParams 对象赋值给 View 对象的 layoutParams 虚拟属性来给 View 设置宽高。但当我们需要设置 gravity 属性呢?可能你想这么写:

编译器不同意的原因是,View 的所有 layoutParams 属性都是 ViewGroup.LayoutParams 类型,也就是所有其他 LayoutParams 的超类,为了设置 LinearLayout 的属性,就需要向下转型。

在 Java 中我们需要将 LayoutParams 转型或是重新赋值,但在 Kotlin 我们可以利用一个名叫 with 的标准库函数来处理 LayoutParams。with 方法接收一个对象和一个用于处理该对象的 lambda with receiver。描述起来可能没什么厉害的,但它的语法是这样的:

请注意这里我们可以同时将 layoutParams 转型为 LinearLayout.LayoutParams 并将其作为 receiver 通过 lambda 快速访问其属性。

还有一点需要知道的就是 Android View 容器会在子 View 生成时自动生成一个 layout 参数对象并赋值给子 View。在这里,当把一个 TextView 添加进 LinearLayout 时,会自动生成一个 LinearLayout.LayoutParams 并赋值给 TextView 的 layoutParams 属性。也就是说,我们并不需要自己创建一个 LayoutParams 对象,直接用父 LinearLayout 提供的就可以。记住这种情况只在将一个子 View 添加进一个父 ViewGroup 时才会发生,所以最外层的 ViewGroup 并不会获得这类对象,因为其没有父 ViewGroup。

对于 layout 参数来讲,这种语法可能还有改善的控件,但已经很直白了。

现在,我要让“v”方法消失

伴随我们这么久的 v 方法已经很方便了,但其语法还有简化的控件。如果能像下面这样构建一个 View 层级岂不是更好?

linearLayout {
    textView {
        // 各属性...
    }
    textView {
        // 各属性...
    }
}

这样写显得更加自然,且可读性有很大的提升。(长得挺像 Gradle)实现这种语法的技巧就是给每一种 View 类型定义一个缩写形式。所以为了实现上述代码,我们需要一个叫 linearLayout 的方法和 v 等价,还需要一个 textView 方法和 v 等价。在 Kotlin 中这不难实现:

在这里我为每一种 View 类型写了两个方法来适配各种被调用的情况,因为在前面 v 方法可以被 Context 或 ViewGroup 调用。Kotlin 中支持这种声明的语言特点叫做单一表述方法 single expression function。这是一种针对方法的特殊语法,可以让你:

  1. 省略方法体的大括号。

  2. 通过表述的返回类型猜测方法的返回类型。

  3. 省略 return 关键字。

现在我们通过这些方法来重新构建上面的 View 层级:

所以只要你愿意给每一种要用到的 View 类型定义一个缩写函数,这还是挺有用的。

Kotlin 实用吗?

到现在为止,我们通过 type-safe builder 模式、扩展方法与扩展属性、lambda with receiver 写了一个快速构建 View 层级的方法。

所以你现在可能要问:我到底要不要以这种方式在我自己的 APP 中构建 View?到现在为止,我一直在说这样做可以很简单很方便,但只与在 Java 中进行同样工作进行了比较,结果不言自明。不过在 Android 中我们一般通过 XML 资源来描述 View,所以我们来对比一下通过 XML 与通过 Kotlin 的 type-safe builder 来构建 View 哪种更强一些。

当 Activity 配置改变

Android 设备中配置随时会改变,最简单的一种改变就是屏幕方向。当然还有其他各种改变,具体参见官方文档。方向的改变对 Android View 的影响极大,因为我们经常会针对竖向和横向写两套布局。

对于 XML 布局而言,无需自己更新 UI 来应对配置改变,只需要写两套布局,一套放在 res/layout-land 下,另一套放在 res/layout-port 下就好。当在这两个目录中给文件相同的命名时,Android 就会针对各种情况自己找到正确的文件来填充。总而言之,无需自己写代码来处理方向改变。

但当处理动态生成的 layout 时,就需要自己处理配置信息的改变了。如果你想要对各种配置有不同的布局,就需要自己写逻辑来判断要生成何种 View。如果你总是需要这么做,代码就会变得笨重。

所以,如果对于每种配置信息要填充不同的 UI,XML 布局更加方便。

当需要处理 RelativeLayout 等复杂布局

RelativeLayout 让我们以一种非常灵活的方式进行相对布局。你可以很简单地声明一个 View 要放在另一个锚 View 的上面下面左边右边,做这件事时需要将锚 View 的 ID 传入前者。View ID 还用于在代码中对特定 View 进行配置修改。

在 Android 中,最好的方式就是让编译器自己给 View ID 赋值,你不应自己写 ID,也就是说,你需要使用 Android 工具来指定这些 ID 以备后面使用。

在 XML 布局中,创建一个 View ID 很简单,只需要这样:

<TextView android:id="@+id/tv" ... />

@+id 注解会告诉 Android 定义一个叫做 tv 的新 ID,或是重用有同样名称的已存在 ID。代码不能更简单。

当处理动态生成的布局时,无法通过代码动态创建 ID,为了创建新 ID,你需要在 XML 中定义一个新 ID,然后再在代码中通过编译后的 R 类来获取 ID 的引用。所以,动态操作 View 就要牵扯到另一个文件,而且在 Android 中,即使一个 ID 不再被引用,也不会被删除。

总的来说,在 XML 不居中更容易操作 View 的 ID,因为有工具来支持 ID 的动态管理。

如果需要在给 View 属性赋值时进行计算

计算 View 的属性很常见,比如设置其内部文本时,或是背景色、尺寸时。

Android XML 布局语言完全是声明性的,你不能在赋值字符串中进行计算。也就是说这些属性必须在 View 被填充后动态修改。这样一来布局文件中的定义与代码中的修改会被分开。Android 开发者对这一点可并不陌生。

但当你动态构建 View 时,你可以直接将计算结果赋值给 View 属性,也可以将某个属性设置的与另外一个属性一样,毕竟我们经常需要给 marginLeft 和 marginRight 设置同样的值。

既然当动态构建 View 时可以享受代码带来的所有灵活性,在赋值方面肯定是动态创建 View 更加方便,尤其是配合 Kotlin 的 type-safe builder。

如何进行布局微调

除非你能看到代码就能想象出图形界面,你肯定需要使用 Android Studio 的布局预览来对 XML 布局进行微调,而不需要构建项目并在终端上跑起来。

当你动态编写布局时,你不能快速预览。不过随着 Android Studio2.0 中 Instant Run 功能的推出,这一点不再那么困扰人了。但是就现在而言,修改布局的最简单的方式还是 Android Studio 预览功能。

两种方式的性能如何?

性能是考量填充 View 与动态构造 View 优劣的重要指标。在我一开始测试的时候,我发现构建一个简单的布局比填充几乎快一倍!但当我开始使用更多的 Kotlin 语法特色,并构建更复杂的 View 时,差别就不那么大了。在我的 Nexus 6P 上,我发现动态构建所花的时间约是填充的四分之三。我猜测这是因为填充时需要先解析布局资源然后构建 View。

顺带说一句,在两种情况中,构建一个有 5 个 View 的 View 层级所花的时间均小于 1 秒,所以除非你要构建很多 View,性能上不需要担心太多。

如果你想自己测试一下,源码可以在我的 Github 上找到(https://github.com/CodingDoug/kotlin-view-builder)。

使用 Kotlin 的大小上限是多少?

在第一部分中我们提及了有关在 Android 中使用 Kotlin 的大小需求问题。你需要声明一个运行时和一个标准库的依赖。每当你想给一个 Android app 添加一个依赖,都应该考虑一下大小,尤其当你不能使用 multidex 时。

在 Kotlin1.0.0 中,运行时 + 标准库的大小总共是 210k+636k=846k,这对一个库来说真的不小了。使用 dex-method-counts 统计出的方法数为 6584,这只包含 kotlin 包下的,没有算里面引用的 Java runtime 方法。这一下就占了一个 dex 的 10% 的方法数。但当在我的测试工程上使用了 ProGuard 后,数目一下减到 42 个方法。最终的数目自然会增加,因为还需要用到 Kotlin 中的其他方法。在包含 Kotlin 的应用中使用 ProGuard 可以有效控制其大小。

总结

Kotlin 用起来还是很愉快的,它可以直接应用在 Android 开发中。对于构建 View 来讲,它不是特别的厉害,因为使用 XML 布局有诸多优势,就现在而言是最佳的方式。但在某些情况下动态的构建 View 更符合需求,此时 Kotlin 就能很大程度上简化代码、优化风格。

你可能在思考使用 Kotlin 有没有其他的坑。这个问题问得好,在 Reddit 上有讨论(https://www.reddit.com/r/androiddev/comments/47613n)。

如果你想看一看我的测试项目,并将 XML 布局与 Kotlin 进行比较,可以 clone 我的这个 repo

希望你一路读下来能有所收获!


感谢徐川对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

评论

发布