实战 Kotlin@Andorid(二):界面构建与扩展方法

阅读数:1127 2016 年 5 月 2 日

话题:移动语言 & 开发架构

版权声明

原作者:Doug Stevenson

译者:程大治

本文由原作者授权翻译并发布,首发于移动开发前线公众号,未经允许禁止转载。

这是实战 Kotlin@Android 的第二部分,如果你还没读过第一部分,建议先阅读第一部分实战 Kotlin@Android(一):项目配置和语言转换

在前面的文章中我们使用 Kotlin 中 type-safe builder 模式写了一个还算有用的 v 方法,它可以构建任意 Android View 实例。

我们可以在其他 Kotlin 代码中调用这个方法来创建并初始化任何类型的 View:

这真的实用吗?

现在我们要创建一个很简单的 layout,它包含两个 TextView。在 XML 可以这样表示:

我们可怜的 v 方法不能一下子创建这么多,不过只需借助一点帮助。我们需要再写一个能够将 View 添加至父 View(LinearLayout, RelativeLayout)的方法。我们现在写一个新的 v 方法。

这个方法与原本的 v 方法几乎一摸一样,区别只在第一个参数上:不再是 Context, 变成了 ViewGroup 类型。这个新的 v 方法需要持有父 ViewGroup,以便将新创建的 View 对象在初始化并返回之前添加进其中。而新的 v 方法又能通过 ViewGroup 来获取 Context 以初始化 View,这样就不用再传入 Context 对象了。

现在我们看一下新的 v 方法如何与旧的协作来构建上述 View 层级。

这里 Kotlin 代码会像 XML 一样嵌套,非常好看。

在上一部分中说过,Kotlin lambda with receiver 可以在 lambda 内部以 this 关键字引用 receiver 对象。在上面的例子中,在外部 v 的 lambda 中 receiver 是 LinearLayout,它作为第一个参数被传入了两个内部 v 方法(刚写的 v 方法)。因为 LinearLayout 是 ViewGroup 的子类,Kotlin 知道我们在调用新写的 v 方法,因为旧的需要传入 Context。

通过这两个兄弟 v 方法我们可以动态地、精确地创建嵌套 View,其中的 ViewGroup 和 View 的具体类型均无限制。现在我们已经可以发现这种表述性的创建方式与 XML 有些相似,而在后续的文章中,我们也将发现 Kotlin 的速度要快一些。

提升空间

Kotlin 的 type-safe builder 模式起了很大的作用,但是在很多时候,Kotlin 还是比 XML 复杂不少。比如在 Kotlin 中当我们想设置一个 TextView 的 maxWidth 属性为 120dp 时:

而在 XML 中,只需要:

<TextView android:maxWidith="120dp" />

本来是为了简化工作写的 v 方法一下变麻烦了。

这里需要将 dp 转化为 px 的简便方法

我在这里想要一个方法可以将 dp 转化为像素,然后上面的代码最好能长这样:

这里的方法可以接收一个以 dp 为单位的值,然后返回当前设备下转化成像素的值。不过为什么要叫这个方法 dp_i 而不是 dp 呢?在 Android 中有时会返回 float 而有时会返回 int,我也不想再自己进行转换,所以就给两种返回类型都写一个方法:"dp_i"和“dp_f”。

但在这里仍有问题。如果你看一下刚才很丑的那段代码,会发现计算像素值时需要 Context 对象。我可不想每次调用 dp_i 方法都传入 Context 作为参数,所以在这里要用到 Kotlin 的另一个技能:extension functions 扩展方法。让我们直接看一下扩展方法长什么样:

扩展方法如何工作?

你可能注意到的第一个点就是方法的前缀。你可能本以为第一个方法应该是 dp_f,结果是 View.dp_f。这是 Kotlin 中针对扩展方法的一个特殊语法。这里将一个类名和一个方法名以点连接,而意思就是告诉 Kotlin 我们要给 View 类添加两个新方法"dp_i","dp_f"。这样使用扩展方法有几点好处:

第一,在扩展方法内作为类的成员可以访问其成员变量和方法(只有 public 和 internal)。也就是说 dp_f 可以通过 View 内部的 context 属性来访问其 Context 引用。现在我们不需要将 Context 作为参数传入了,因为它隐含在 View 中。

第二,在导入了 (import) 这些扩展方法的代码段中可以像调用一个对象的普通方法一样调用其扩展方法。在这里,在 v 方法的 lambda with receiver 代码块中可以通过 receiver View 对象直接调用这些方法,像这样:maxWidth = dp_i(120),Kotlin 会识别出需要调用 View 类型的 receiver 对象的 dp_i 方法。

值得注意的一点是,Kotlin 在声明扩展方法时,不会修改其 class。所以在这里,View 类的其他方法不能访问扩展方法,因为扩展方法不是真正意义上的成员。

现在我们就有将 dp 转成 px 的简便方法了。

扩展方法还有其他的用处。现在我们已经看到通过扩展方法可以简化一些棘手的代码,我们利用这一点继续简化 v 方法。

现在我们有两个 v 方法,第一个用于构建根元素,接收 Context,第二个用于创建嵌套于父 View 中的子 View。

inline fun <reified TV : View> v(context: Context, init: TV.() -> Unit) : TV

inline fun <reified TV : View> v(parent: ViewGroup, init: TV.() -> Unit) : TV

如果我们不需要传入 Context 或是 ViewGroup 作为参数,岂不是很好?通过扩展方法,我们就像刚才避免将 Context 传入 dp_f 一样重构这段代码。下面使用扩展方法重新实现两个 v 方法,注释是两个方法原本的声明。

你可以看到我们去掉了两个方法的第一个参数(Context 和 ViewGroup),并通过所继承的类来获取所需实例的引用。现在这两个方法都只有一个参数:用于修改 View 的 lambda with recceiver。

修改了方法后,如果我们在 Activity(Context 子类)中写代码,那就可以将 v 添加做 Activity 对象的成员。这样我们就可以以这样更简单的方式构建嵌套 View。

这里调用 v 方法根本不像是在调用方法,因为我们不需要圆括号。在第一部分中我说过,如果方法的最后一个参数是 lambda,那就可以放在圆括号后,而在这里,只有一个参数,根本就不用写圆括号。

Kotlin 中的扩展方法帮我们在代码中很简明易懂地创建构建 View 层级。不过还是有其他问题需要注意。比如我们想设置 TextView 的左内边距为 16dp。

在这里调用 setPadding() 方法与直接修改属性放在一起真是挺丑的,之所以有这样的情况发生,是因为 setPadding() 方法有多个参数,并不是一个 JavaBean 风格的 Setter 方法。所以,Kotlin 不能为其制定一个虚拟属性。不用怕,我会在后续文章中通过 Kotlin 的另外一个功能来弥补这个问题。


感谢徐川对本文的审校。

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