阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

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

  • 2016-05-19
  • 本文字数:4717 字

    阅读完需:约 15 分钟

这是 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 很简单,只需要这样:

<p><TextView android:id="@+id/tv" ... /></p>@+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 )关注我们。

2016-05-19 17:203091

评论

发布
暂无评论
发现更多内容

使用 SQL 实现同比环比分析

搞大屏的小北

数据可视化 同比环比 展示同比环比 BI大屏同比环比

1024·致敬 | 迟到的小温暖,感谢不平凡的你们

XTransfer技术

确保网路畅通,华为云CDN加速服务大有可为

路过的憨憨

Apache APISIX 3.1.0 版本正式发布

API7.ai 技术团队

开源 api 网关 APISIX apache 社区

DTT年度收官圆桌π,华为云8位技术专家的年末盘点

华为云开发者联盟

云计算 后端 华为云 12 月 PK 榜

行云管家荣膺STIF第三届国际科创节 “2022年度数字化创新典范奖”

行云管家

信息安全 数字化 国际科创节

网络拥堵影响业绩?看华为云CDN如何为企业保驾护航

路过的憨憨

记一次因 GC bug 导致 TiKV 存储占用不均的问题处理

TiDB 社区干货传送门

管理与运维 故障排查/诊断

Liga妙谈 | 找准「话事人」,高效甄别和响应用户反馈

LigaAI

产品经理 敏捷开发 PO 产品负责人 12 月 PK 榜

这个API管理工具不登录不下载就能用!

不想敲代码

API 接口管理 协同办公 协同软件

ROMA Connect: 5大联接能力+4大集成能力,推进企业数字化转型

华为云开发者联盟

云计算 后端 数字化 华为云 12 月 PK 榜

接口自动化测试不想写代码?这款工具强烈推荐

不想敲代码

自动化测试 API 自动化测试平台

重磅!XTransfer荣登InfoQ【十大开发者最向往的高价值技术团队】榜单

XTransfer技术

数益工联 x TiDB丨如何运用 HTAP 挖掘工业数据价值?

TiDB 社区干货传送门

TiDB 的“聚簇因子” -- 从 cop task 到 shard_row_id_bits

TiDB 社区干货传送门

性能测评 OLTP 场景实践

跳板攻击原理及如何追踪定位攻击者主机(下)

郑州埃文科技

IP地址 跳板攻击 攻击溯源

智能制造 | AIRIOT智慧工厂管理解决方案

AIRIOT

物联网 智慧工厂 物联网系统搭建

零信任与 K8s 环境实践

HummerCloud

k8s 零信任 kubernetes 运维

YMatrix 番外篇|透过镜头,那些不为人知的故事

YMatrix 超融合数据库

车联网 超融合数据库 C++ YMatrix 研发故事

墨芯人工智能加入龙蜥,携手打造软硬协同稀疏化计算平台

OpenAnolis小助手

人工智能 开源 龙蜥社区 CLA 墨芯

Oracle 到 TiDB (OGG)

TiDB 社区干货传送门

云网络运维必备神器:全链路故障诊断与分析

华为云开发者联盟

云计算 后端 华为云 12 月 PK 榜

NFTScan 与 Cwallet 团队达成战略合作伙伴,由 NFTScan 为其提供 NFT API 数据服务

NFT Research

NFT 数据基础设施

艺多不压身!华为云CDN成众多企业网络支撑

路过的憨憨

一文讲清「敏捷路线图」| Liga译文

LigaAI

Scrum 产品经理 敏捷开发 软件开发 12 月 PK 榜

助力企业服务体验升级,华为云CDN这样做

路过的憨憨

怎样将数据从Oracle迁移到TiDB

TiDB 社区干货传送门

等保四级适用于哪些领域?一年一次吗?

行云管家

等保 等级保护 等保四级

理解iOS端的WebView同层组件

珲少

捷报频传 | Bonree ONE获2022科技赋能金融业场景金融建设突出贡献奖

博睿数据

可观测性 智能运维 博睿数据 ONE平台 荣誉奖项

工业数据分析为什么要用FusionInsight MRS IoTDB?

华为云开发者联盟

大数据 后端 华为云 工业数据 12 月 PK 榜

实战kotlin@android(三):扩展变量与其它技巧_移动_Doug Stevenson_InfoQ精选文章