生成式AI领域的最新成果都在这里!抢 QCon 展区门票 了解详情
写点什么

实战 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:203086

评论

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

低学历并不是阻碍职业发展的绊脚石

测吧(北京)科技有限公司

软件测试

3年测试经验跳槽成功拿下30W+年薪

测吧(北京)科技有限公司

软件测试

这些小技巧,让你的前端编程更优雅

好程序员IT教育

前端

2022开源之夏|EMQ三大开源项目开发圆满收官

EMQ映云科技

开源 物联网 IoT mqtt 11月月更

自学前端技术怎么样,有必要去吗

小谷哥

好家伙!阿里P8撰写的Java微服务架构全栈笔记GitHub一夜飞到榜首

小二,上酒上酒

Java 架构 面试 微服务

万物皆可集成系列:低代码对接Web Service接口

葡萄城技术团队

极客时间架构训练营模块六作业

李晨

架构

深圳市数字经济指数发布:数字经济蓬勃发展,数字用户深度渗透

易观分析

数字经济 深圳

CSS修改单选框样式(element)

肥晨

11月月更 单选框样式修改 element单选框样式

大数据培训学习需要什么基础

小谷哥

神了!阿里P8纯手写出了这份10W字的MyBatis技术原理实战开发手册

小二,上酒上酒

学习 编程 面试 mybatis

为什么面试官狂问八股文?我已经被三家公司问到哑口无言……

程序知音

Java java面试 java架构 后端技术 Java面试八股文

爆肝了!阿里出版的这份Spring Security源码手册,狂揽GitHub榜首

小二,上酒上酒

Java 面试 spring security 大厂 大厂面试

蚌住了!这份阿里P8写的Java多线程编程实战指南就这么容易开源?

小二,上酒上酒

Java 面试 多线程 阿里 大厂面试

阿里P8偷偷把内网分享的SpringCloud微服务架构精髓手册开源了

小二,上酒上酒

架构 面试 微服务 Spring Cloud

最新出炉!开源 API 网关的性能对比:APISIX 3.0 和 Kong 3.0

API7.ai 技术团队

kong api 网关 APISIX

终于有人把这份10 万字节详细面试笔记(带完整目录) 整理出来了

钟奕礼

Java java程序员 java面试 java编程 Java 面试题

Meta Force 原力元宇宙公排系统开发详情

开发微hkkf5566

前端培训班中如何学习前端开发技术

小谷哥

【web 开发基础】PHP 自定义函数之函数的返回值-PHP 快速入门 (27)

迷彩

web开发基础 PHP基础 11月月更 return

《深入理解JavaScript特性》学习总结1-ES6基础知识点总结

肥晨

箭头函数 11月月更 ES6基础知识点总结

低门槛上手快!火山引擎VeDI这样满足数据分析新需求

字节跳动数据平台

大数据 BI

遭MQ连连干翻后的醒悟!含恨码出5本MQ学习手册助力秋招之旅

小二,上酒上酒

面试 RocketMQ 大厂 大厂面试

Meta Force 原力元宇宙dapp系统开发(智能合约部署)

开发微hkkf5566

Github上架3天星标55K,阿里最新产架构师速成手册成功颠覆了我的认知

程序员小毕

分布式 微服务 程序人生 架构师 Java后端

大数据培训和自学哪种方式更好

小谷哥

消息队列 RocketMQ 5.0:从消息服务到云原生事件流平台

阿里巴巴云原生

阿里云 RocketMQ 云原生

java培训学习该怎么做?

小谷哥

为什么晶闸管能在大电流下工作?

元器件秋姐

元器件采购 元器件电商 元器件知识 华秋商城 晶闸管

redhat运维-远程日志记录

阿柠xn

运维 日志 linux 文件权限控制 11月月更

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