充分利用 Kotlin

翻译自 Taking Advantage of Kotlin codelab。

目录

1. 介绍

你将构建什么

你会学到什么

你需要什么

2. 准备好开始

下载代码

应用概述

Contact.java

ContactsActivity.java

配置Kotlin

3. Kotlin 转换基础知识

将联系人 POJO 转换为 Kotlin 数据类

Contact 类文件的问题

使用转换器

Kotlin 数据类

将 ContactsActivity 转换为 Kotlin

lateinit 关键字

清理 ContactsActivity

4. Kotlin View Binding, Lambdas & Standard Library Extensions

Kotlin Android Extensions 视图绑定

添加 lambdas 进行验证

使用标准扩展功能添加排序并减少样板

添加排序

用标准库扩展替换 for 循环

5. 自定义扩展功能

EditText 验证扩展功能

默认参数

6. 恭喜!

我们所涵盖的内容

参考

值得注意的 Kotlin 语法更改

在哪儿了解更多


1. 介绍

在2017年的 I/O 上,谷歌宣布正式支持 Kotlin 开发 Android 应用程序。Kotlin 是由 Jetbrains 开发的一种语言,由于其简洁,优雅的语法以及与Java 的 100% 互操作性,其开发人员基础迅速增长。

你将构建什么

在此 codelab 中,您将把用 Java 语言编写的地址簿应用程序转换为 Kotlin。通过这样做,您将看到 Kotlin 如何能:

  • 帮助减少样板代码。
  • 提供易于维护、简洁易读的代码。
  • 避免常见的 Android 开发陷阱。
  • 在编写代码时强制实施最佳实践,以防止运行时错误。
 

你会学到什么

  • 如何使用 Android Studio 的 Java 到 Kotlin 转换器。
  • 如何使用 Kotlin 语法编写代码。
  • 如何使用 Kotlin lambda表达式扩展函数来减少样板代码并避免常见错误。
  • 如何使用内置标准库函数来扩展现有的 Java 类函数。
  • Kotlin 如何能帮助避免 Billion Dollar MistakeNullPointerException

你需要什么

  • 最新版本的 Android Studio
  • 用于运行应用的 Android 设备或模拟器
  • 示例代码(下面的下载链接)
  • 使用 Java 语言开发 Android 应用程序的基本知识

2. 准备好开始

下载代码

单击以下链接下载此codelab的所有代码:

下载源代码

您也可以在此 github 仓库中找到它

解压缩下载的 zip 文件。zip 文件包含一个根文件夹(android-using-kotlin-master),其中包含此 codelab 的每个步骤的文件夹。

  • MyAddressBook-starter 包含入门 app
  • 步骤 1 和步骤 2 是介绍和入门步骤,因此没有这些文件夹。
  • MyAddressBook-stepN 文件夹包含在步骤 N 之后处于完成状态的 app。
  • 最终的 app 可以在 MyAddressBook-final 文件夹中找到。

应用概述

MyAddressBook 是一个简化的地址簿应用程序,它在一个 RecyclerView 中显示联系人列表。您将把所有 Java 代码转换为 Kotlin,所以请花些时间熟悉 MyAddressBook-starter app 中的现有代码。

  1. 在 Android Studio 中打开 MyAddressBook-starter
  2. 运行它。

Contact.java

Contact 类是一个包含数据的简单 Java 类。它用于模拟单个联系人条目,包含三个变量:名字,姓氏和电子邮件。

ContactsActivity.java

ContactsActivity 显示一个 RecyclerView,这个 RecyclerView  显示 Contact 对象的 ArrayList。您可以通过两种方式添加数据:

  1. 按 Floating Action Button 将打开 New Contact 对话框,允许您输入联系人信息。
  2. 您可以通过选择 options menu 中的 Generate 选项快速生成一个有 20 个模拟联系人的列表,该选项将使用包含的 JSON 文件生成 Contact 对象。

有了一些联系人后,您可以在一行上滑动以删除该联系人,或者通过在选项菜单中选择 Clear 来清除整个列表。

您还可以点击一行来编辑该联系人条目。在编辑对话框中,名字和姓氏字段是被禁用的,因为对于本教程 app,这些字段是不可变的(仅在 Contact 类中提供了getter),但电子邮件字段是可修改的。

New Contact 对话框通过以下方式执行验证:

  • 名字和姓氏字段不得为空。
  • 电子邮件字段必须包含使用有效格式的电子邮件地址。
  • 尝试保存无效联系人将在 Toast 消息中显示错误。

配置Kotlin

在使用 Kotlin 工具和方法之前,必须在项目中配置 Kotlin。

  1. I在Android Studio中,选择 Tools > Kotlin > Configure Kotlin Plugin Updates。在窗口中选择 Stable 通道,然后单击 Check for updates now。如果有可用的更新,请单击 Install
  2. 在Android Studio中,选择 Tools > Kotlin > Configure Kotlin in Project。如果出现标题为 Choose Configurator 的窗口,请选择 Android with Gradle,确保选中 所有模块,然后单击 OK。Android Studio 将在 app 级别和 project 级别的 build.gradle 文件中添加所需的依赖项。
  3. 当 build.gradle 文件发生更改时,系统将提示您 sync the project with Gradle。同步完成后,转到下一步并编写一些 Kotlin 代码。

注意:使用 Android Studio 3.0 或更高版本并创建新项目时,您可以通过选择 Create Android Project 对话框中的 Include Kotlin support 复选框默认配置 Kotlin

3. Kotlin 转换基础知识

将联系人 POJO 转换为 Kotlin 数据类

Contact 类文件的问题

Kotlin 提示:在整个 codelab 中查找这些(提示)盒子,以找到每个步骤中使用的新 Kotlin 语法。

Contact.java 类看起来应该很熟悉,因为它包含标准的 POJO 代码:

  • 私有变量,在该例中是三个字符串。
  • 最初设置这些变量的构造函数。
  • 要从外部获取的字段的 Getters,在该例中是所有变量(的 Getters)。
  • 要从外部设置的变量的 Setters,在该例中是所有变量。

这种对象可能会给不知情的开发人员带来一些问题,因为它会留下一些问题:

  1. 可空性:这些变量中的哪一个可以为 null?First name 和 last name 只能通过构造函数设置并且没有 setter 方法这一事实,意味着它们是不可为空的,但这不并不能保证:某人仍然可以为构造函数中的一个变量传递 null。Email 确实有一个setter,所以对于这个来说,没有办法知道它是否应该是可空的。
  2. 可变性:这些变量中哪些可能会发生变化?因为只有 email 变量定义了setter,所以暗示只有该变量是可变的。

事实上,Java 语言不会强迫您考虑潜在的 null 情况,以及设置为只读的变量,这可能会导致运行时错误,例如可怕的情况 NullPointerException

Kotlin 通过强制开发人员在编写类时而不是在运行时考虑它们来帮助解决这些问题。

使用转换器

  1. 在 Project 窗格中选择 Contact.java。
  2. 选择 Code > Convert Java File to Kotlin File.
  3. 将出现一个对话框,警告您项目的其余部分中可能需要进行一些更正才能使用转换(Contact 类在 activity 中被使用)。单击 OK 进行这些更改。

转换器运行并将 Contact.java 文件扩展名更改为 .kt。Contact 类的所有代码都缩减为以下单行:

internal class Contact(val firstName: String, val lastName: String, var email: String?)

总之,此类声明执行以下操作:

  1. 声明一个名为 Contact 的类。
  2. 声明该类的三个属性:两个只读字段 名字 和 姓氏,以及一个可变变量 email 地址。
  3. FirstName 和 lastName 属性保证永远不会 null,因为它们是类型 String。转换器可以推测出这个,因为 Java 代码没有 setters 而且 secondary constructors 没有设置所有三个属性。 
  4. Email 字段可能是null,因为它是 String? 类型

Kotlin 小贴士

  • Kotlin 中的类可以具有替换 Java 语言中的字段的属性。这些字段可以使用 var 关键字声明为可变的或者使用关键字 val 声明为不可变的。
  • 可变属性(声明为 var)可以被赋值任意次数,而不可变变量(带有 val)只能被赋值一次。
  • Kotlin 属性类型位于属性名称和冒号之后 val firstName: String
  • 主构造函数是类声明的一部分:它在括号中的类名后面。您可以在类的主体中使用 constructor 关键字提供其他构造函数。
  • 属性在主构造函数中声明。
  • 默认情况下,Kotlin 中的属性类型不能为 null。如果属性可以包含空值,则使用 ? 语法可以用空类型声明它。例如, String? 可以为 null 但 String 不能。

参见 属性和字段 文档。

Kotlin 数据类

1.运行这个 app,然后使用 app 中的 Generate 菜单选项创建一些联系人。如果要重置 app 并清除联系人,请在 Options 菜单中选择 Clear

2.在您的代码中,generateContacts() 方法使用 toString() 方法记录正在创建的每个联系人。检查日志以查看此语句的输出。默认 toString() 方法使用对象的 hashcode 来识别对象,但它不会为您提供有关对象内容的任何有用信息:

generateContacts: com.example.android.myaddressbook.Contact@d293353

3。在 Contact.kt 文件中,在 internal 可见性修饰符后添加 data 关键字。

internal data class Contact
(val firstName: String, val lastName: String, var email: String?)

4.再次运行 app 并生成联系人。查看日志以获得更多信息。

Kotlin小贴士

data 关键字告诉 Kotlin 编译器该类用于存储数据,编译器将生成许多有用的方法,例如 toString() 的实现显示了对象内容,copy() 方法和用于将对象解构为其字段的 compnnent 函数。请参阅 Data类 文档。

将 ContactsActivity 转换为 Kotlin

下一步是将 ContactsActivity 转换为 Kotlin。在这个过程中,您将了解转换器的一些限制,以及一些新的 Kotlin 关键字和语法。

  1. 使用与你对 Contact.java 文件(Code > Convert Java File to Kotlin File)做的相同的过程在 ContactsActivity 上运行转换器。

lateinit 关键字

1.请注意,转换过程将 Java 成员变量,例如 mContacts 更改为 Kotlin 属性。

private var mContacts: ArrayList<Contact>? = null


除布尔值外,所有这些属性都标记为可为空,因为它们在 onCreate()执行之前不会使用任何值进行初始化,因此在 activity 创建时也是 null。
这并不理想,因为无论何时您想使用方法或设置属性,您都必须先检查引用是否为空(否则编译器将抛出错误以避免可能的 NullPointerException)。 
在 Android apps 中,您通常在 acitvity 生命周期方法中初始化成员变量,例如 onCreate(),而不是在实例化实例时。

幸运的是,Kotlin 有一个关键字正是这种情况:lateinit。如果知道属性的值在初始化后不会为  null,请使用此关键字。

2.在除初始化的 boolean 之外的所有成员变量的 private 可见性修饰符后添加 lateinit 关键字。删除 null 赋值,并通过删除 ?.,将类型更改为不可为空

private lateinit var mContacts: ArrayList<Contact>

3.将 Boolean 属性类型从 mEntryValid 成员变量(和前面的冒号)中删除,因为 Kotlin 可以从赋值中推断出类型:

private var mEntryValid = false

4.删除所有出现的 !! 操作符。您可以通过选择 Edit > Find> Select all occurrences 并按退格键来选择现有选择的所有实例。

Kotlin小贴士

  • 语句末尾的分号在 Kotlin 中是可选的。
  • 使用 fun 关键字声明函数。
  • lateinit 关键字允许您将非空变量的初始化推迟到以后的时间。
  • 如果属性类型是明确的,则可以省略属性类型,例如 val isActive = false
  • 您可以使用 !! 访问可空类型的字段和方法。这向编译器表明您知道在调用时引用不会为 null。请谨慎使用此运算符,因为它可能会导致 NullPointerException。有关实现引用可空值的其他方法,请参阅Null Safety文档。
  • 在 init {} 块内为类执行任何构造函数的初始化代码。

清理 ContactsActivity

你的 activity 现在应该按预期工作了。还剩一些更改来完成清理已转换的 actiivty :

  1. 在 ViewHolder 内部类中,nameLabel 变量有带小锯齿的下划线。选择变量并按橙色灯泡,然后选择 Join declaration and assignment
  2. 对 emailLabel 变量重复步骤 1 。

Kotlin小贴士

Kotlin 转换器进行了大量更改,使代码更具可读性。访问 codelab 末尾的参考部分,了解其中的一些变化。

4. Kotlin View Binding, Lambdas & Standard Library Extensions

Kotlin Android Extensions 视图绑定

从 activity 中访问 XML 布局文件中的各个 views 是每个 Android 开发人员都熟悉的样板代码。使用某些库可以避免无休止的 findViewById() 调用,但这些库要求对每个 view 进行注解,因为视图绑定必须在运行时发生。Kotlin Android Extension 插件将 views 转换为 acitivity 的属性,允许您直接引用它们,而不需要 findViewById() 调用。

1.打开你的 app 级 build.gradle 文件并添加一行以在 kotlin-android 插件下应用 kotlin android extension 插件:

apply plugin: 'kotlin-android-extensions'

2.在 ContactActivity 的 onCreate() 方法中,删除 Toolbar 和 Floating Action Button 的带有 findViewById() 调用的语句。如果Android Studio 未自动添加所需的导入,请添加以下 import:

import kotlinx.android.synthetic.main.activity_contacts.*

此 import 语句为 activity_contacts 布局文件中的所有 views 创建属性。

3.运行 app。Floating Action Button 和 Toolbar 不会导致任何问题,即使它们从未初始化。这怎么可能?上面的导入使用视图ID作为属性名称,在布局文件被填充时将所有 views 设置为 activity 的属性。原始 onCreate() 中的变量设计为与布局文件中的 view IDs 具有相同的名称。

重要提示:您将能够使用一个 view 的 ID 作为属性引用项目中存在的任何 view。但是,Kotlin 只能从 inflated 的 view 中创建这些属性。使用未 inflated 的布局文件中的 view IDs 将抛出 NullPointerException

4.在 setupRecyclerView() 方法中,删除 findViewById() 调用。

5.使用 contact_list 替换 recyclerView 的所有使用,因为这是布局文件中联系人列表的 ID。 请注意,activity 包含 content_contacts 布局文件的 import 语句。

6.检查Android Studio是否已自动添加以下导入。如果不存在,请添加它。

import kotlinx.android.synthetic.main.content_contacts.*

您还可以使用 Kotlin extension 来访问其他已加载视图的子视图,例如 Adapter ViewHolders 的子视图,或用于添加新联系人的 AlertDialog 的子视图。子视图成为父视图的属性,同样的,属性名引用布局文件中的 ID。

7.在 showAddContactDialog() 方法中,将 findViewById()调用 替换为以下调用:

mFirstNameEdit = dialogView.edittext_firstname
mLastNameEdit = dialogView.edittext_lastname
mEmailEdit = dialogView.edittext_email

8.在 ViewHolder 类声明中,使用相同的技术设置 nameLabel 和 emailLabel

9.请注意,对于 adapter 和 dialog 的情况,导入的类来自 <layout>.view.* 包,因为这些属性是从已加载的视图而不是 activity 派生的。

就这样!您的所有 findViewById() 调用都已被删除,并替换为更易读的从布局文件中使用 ID 的代码。

Kotlin小贴士

  1. 在 app 级 build.gradle 文件中应用 Kotlin Android Extensions 插件以使用 Kotlin 视图绑定。
  2. 使用视图的 ID 作为属性名称,将已加载布局中的任何视图作为 Kotlin 属性直接引用。
  3. 引用属于另一个已加载视图的子视图,如 ViewHolder 或 Dialog 的根视图。在这种情况下,语法是 <rootview>.id

有关详细信息,请参阅 Kotlin Extension Guide

添加 lambdas 进行验证

Kotlin提供对lambda的支持,lambda是未声明但作为表达式立即传递的函数。它们有以下语法:

  • 箭头( - >)将函数参数与函数定义分开。
  • 函数的参数(如果有的话)在 - >之前声明。如果可以推断出参数类型,则可以省略。
  • 函数体在 -> 之后定义。
{ x: Int, y: Int -> x + y }

然后,您可以将这些表达式存储在变量中并重用它们。

在此步骤中,您将修改 afterTextChanged() 方法以在添加或修改联系人时使用 lambda 表达式验证用户输入。此方法使用 setCompoundDrawablesWithIntrinsicBounds() 方法在EditText的右侧设置 pass 或 fail drawable。

您将创建两个 lambda 表达式来验证用户输入并将它们存储为变量。

1.在 afterTextChanged()方法中,删除检查三个字段有效性的三个布尔值。

2.创建一个存储名为 notEmpty 的 lambda 表达式的不可变变量。lambda 表达式将 TextView 作为参数(EditText 继承自TextView)并返回一个布尔值:  

val notEmpty: (TextView) -> Boolean

3.将一个 lambda 表达式赋值给 notEmpty,如果使用 Kotlin isNotEmpty() 方法 TextView 的 text 属性不为空就返回 true

val notEmpty: (TextView) -> Boolean = { textView -> textView.text.isNotEmpty() }

4.I如果 lambda 表达式只有一个参数,则可以省略它并替换为 it 关键字。删除 textView 参数并使用 it 在 lambda 体中替换它的引用:

val notEmpty: (TextView) -> Boolean = { it.text.isNotEmpty() }

5.创建具有相同签名的另一个 lambda,并将其赋值给 isEmail 变量。此函数检查 TextView 的 text 属性是否与 email 模式匹配:

val isEmail: (TextView) -> Boolean = { Patterns.EMAIL_ADDRESS
    .matcher(it.text).matches() }

6.在对 EditText.setCompoundDrawablesWithIntrinsicBounds()的每次调用中,删除 if/else 语句中已删除的布尔值:

mFirstNameEdit.setCompoundDrawablesWithIntrinsicBounds(null, null,
                if () passIcon else failIcon, null)

7.将其替换为相应的验证 lambda 表达式,并传入需要验证的 EditText:

mFirstNameEdit.setCompoundDrawablesWithIntrinsicBounds(null, null,
                if (notEmpty(mFirstNameEdit)) passIcon else failIcon, null)

8.将 mEntryValid 布尔值改为在名和姓 EditTexts 上调用 notEmpty(),在 email EditText 上调用 isEmail():

mEntryValid = notEmpty(mFirstNameEdit) and notEmpty(mLastNameEdit) and  isEmail(mEmailEdit)

虽然这些更改并没有大量减少代码,但可以看到如何重用这些验证器。使用 lambda 表达式与高阶函数结合使用变得更加有用,高阶函数是将其他函数作为参数的函数,这将在下一节中讨论。

Kotlin小贴士

  1. 当 lambda 表达式具有单个参数时,可以使用 it 关键字引用它。
  2. Kotlin 没有三元运算符,并使用 if / else 语句代替它。

使用标准扩展功能添加排序并减少样板

Kotlin 语言的一个主要特性是扩展,或者为任何外部类(未创建的类)添加功能的能力。这有助于避免需要包含不可修改的 Java 或 Android 框架类的 “utility” 类。例如,如果要向 ArrayList 添加一个方法来提取任何只有整数的字符串,可以在不创建任何子类的情况下向 ArrayList 添加扩展函数。

标准的 Kotlin 库包含许多常用 Java 类的扩展。

添加排序

在此步骤中,您将使用标准库扩展函数向 options 菜单添加排序选项,允许按名字和姓氏对联系人进行排序。

Kotlin 标准库包括 sortBy() 可变列表的扩展函数,包括 ArrayLists,它将 “selector” 函数作为参数。这是高阶函数的一个示例,该函数将另一个函数作为参数。在函数中传递的角色是为任意对象列表定义自然排序顺序。sortBy() 方法遍历调用它的列表中的每个条目,并对列表项执行 selector 函数以获取它知道如何排序的值。通常,selector 函数返回实现 Comparable 接口的对象的一个​​字段,例如 String 或 Int

1.在 res/menu/menu_contacts.xml中创建两个新菜单项,按 first name 和 last name 对联系人排序:

<item
    android:id="@+id/action_sort_first"
    android:orderInCategory="100"
    android:title="Sort by First Name"
    app:showAsAction="never" />
<item
    android:id="@+id/action_sort_last"
    android:orderInCategory="100"
    android:title="Sort by Last Name"
    app:showAsAction="never" />

2.在 ContactsActivity.kt 的 onOptionsItemSelected()方法中,将 IDs 用于 cases,在 when 语句中添加两个更多的 cases(类似于 JAVA 中的 switch):

R.id.action_sort_first -> {}
R.id.action_sort_last -> {}

对于 R.id.action_sort_first 在 mContacts 列表上调用  sortBy() 方法。传入一个 lambda 表达式,该表达式将 Contact 对象作为参数并返回其 first name 属性。由于 contact 是唯一的参数,因此 contact 可以被 it 来指代

mContacts.sortBy { it.firstName }

3.列表将被重新排序,因此通知 adapter 数据集已经更改,并在onOptionsItemSelected()方法的第一种 case 里返回 true:

{
    mContacts.sortBy { it.firstName }
    mAdapter.notifyDataSetChanged()
    return true
}

4.将代码复制到 “Sort by Last Name” case,更改 lambda 表达式以返回传入的联系人的姓氏:

{
    mContacts.sortBy { it.lastName }
    mAdapter.notifyDataSetChanged()
    return true
}

5.运行 app。您现在可以通过 options 菜单中的名字和姓氏对联系人进行排序!

用标准库扩展替换 for 循环

Kotlin 标准库为 Collections 例如 Lists,Sets 和 Maps 等,添加了许多扩展功能,以允许它们之间进行转换。在此步骤中,您将简化保存和加载联系人方法以使用转换扩展。

1.在 loadContacts() 方法中,将光标设置在 for 关键字上。

2.按橙色灯泡显示快速修复菜单,然后选择 Replace with mapTo(){}

Android Studio 会将 for 循环更改为 mapTo(){} 函数,这是另一个采用两个参数的高阶函数:要将 receiver 参数(要扩展的类)转换为的 collection,以及指定如何将 set 的项转换为list 项的 lambda 表达式。注意 lambda 中的 it 符号,指的是参数中传入的单体(列表中的项)。

3.将此调用内联到 return 语句中,因为 contacts 变量不再有用:

private fun loadContacts(): ArrayList<Contact> {
    val contactSet = mPrefs.getStringSet(CONTACT_KEY, HashSet())
    return contactSet.mapTo(ArrayList<Contact>()) { 
        Gson().fromJson(it, Contact::class.java) 
    }
}

4.删除第一个参数中的 <Contact> 参数化,因为编译器可以从第二个参数中的 lambda 推断出它:

return contactSet.mapTo(ArrayList()) { Gson()
    .fromJson(it, Contact::class.java) }

5.在 saveContacts() 方法中,将光标设置在带下划线的for循环定义上。

6.选择橙色灯泡图标以显示快速修复菜单,然后选择 Replace with map{}.toSet()

同样,Android Studio 用扩展函数替换循环:这次 map{},它对列表中的每个项执行传入函数(使用 GSON 将其转换为字符串),然后使用 toSet() 扩展方法将结果转换为 Set 。

Kotlin 标准库充满了扩展函数,特别是通过允许您将 lambda 表达式作为参数传递给现有数据结构添加功能的高阶函数。您还可以创建自己的高阶扩展函数,如下一节所示。

Kotlin小贴士

  1. Kotlin 标准库附带了许多扩展方法,可以为现有 Java 数据结构添加功能。
  2. 这些扩展通常是高阶函数,它将其他函数作为参数(通常作为 lambda 表达式传递)。
  3. sortBy()扩展方法扩展了许多 Java 列表类,以允许它们按照传递的 lambda 表达式排序,该表达式将列表项作为参数,并返回实现 Comparable 接口的对象,定义列表的自然排序顺序。
  4. mapTo()和 map() 方法也是扩展 Java 列表的高阶函数。传入的 lambda 表达式对列表中的每个项进行操作。

5. 自定义扩展功能

EditText 验证扩展功能

afterTextChanged() 在添加新联系人时验证值的方法仍然比需要的时间长。它重复调用以设置验证 drawables,并且对于每次调用,您必须访问 EditText 实例两次:一次设置 drawable,一次检查输入是否有效。您还必须再次检查验证 lambda 表达式以设置 mEntryValid 布尔值。

在此任务中,您将在 EditText 上创建一个执行验证的扩展函数(检查输入并设置 drawable)。

1.通过在项目浏览器中选择包目录并单击 File > New > Kotlin File/Class 来创建新的 Kotlin 文件。将它命名为 Extensions 并确保 Kind 字段显示 File

要创建 Extensions 函数,请使用与常规函数相同的语法,除了在函数名前加上要扩展的类和句点。

2.在 Extensions.kt 文件中,在 EditText 上创建一个名为  EditText.validateWith() 的 Extension 函数。它需要三个参数:输入有效情况下的 drawable,输入无效时的 drawable,以及将 TextView 作为参数并返回布尔值的验证器函数(如您创建的 notEmpty 和 isEmail 验证器)。此扩展函数还返回一个 Boolean:

internal fun EditText.validateWith
        (passIcon: Drawable?, failIcon: Drawable?, 
         validator: (TextView) -> Boolean): Boolean {
}

3.定义返回验证器函数结果的方法。您可以将 this 作为参数传递给验证程序,因为函数是 EditText 的扩展,所以 this 是调用该方法的实例:

internal fun EditText.validateWith
        (passIcon: Drawable?, failIcon: Drawable?, 
         validator: (TextView) -> Boolean): Boolean {
    return validator(this)
}

4.在 validateWith() 方法中,调用 setCompoundDrawablesWithIntrinsicBounds(),为顶部、左侧和底部的 drawables 传入 null,并将 if/else 语法与传入的验证器函数一起使用以选择 pass 或 fail drawable:

setCompoundDrawablesWithIntrinsicBounds(null, null,
            if (validator(this)) passIcon else failIcon, null)

5.回到  ContactsActivity,在 afterTextChanged() 方法中,移除所有三个对 setCompoundDrawablesWithIntrinsicBounds() 方法的调用。

6.将 mEntryValid 变量的赋值更改为在所有三个 EditTexts 上调用 validateWith(),传入  pass 和 fail 图标以及相应的验证器:

mEntryValid = mFirstNameEdit.validateWith(passIcon, failIcon, notEmpty) and
        mLastNameEdit.validateWith(passIcon, failIcon, notEmpty) and
        mEmailEdit.validateWith(passIcon, failIcon, isEmail)

您可以通过将类型 notEmpty 和 isEmail lambda 表达式更改为 TextView 类的扩展而不是传入 TextView 实例来更进一步。这样,在 lambda 表达式中,您是 TextView 实例的成员,因此可以在不引用实例的情况下调用 TextView 的方法和属性。

7.将 notEmpty 和 isEmail 声明更改为 TextView 的扩展并删除参数。删除 it 参数引用,并直接使用 text 属性:

val notEmpty: TextView.() -> Boolean = { text.isNotEmpty() }
val isEmail: TextView.() -> Boolean = { Patterns.EMAIL_ADDRESS.matcher(text).matches() }

8.在 Extensions.kt 文件中的 validateWith() 方法中,使第三个参数(验证器函数)扩展 TextView 类型而不是传入它,并删除验证器方法调用中的 this 参数:

internal fun EditText.validateWith(passIcon: Drawable?, 
                                          failIcon: Drawable?,
                                          validator: TextView.() -> Boolean): Boolean {
    setCompoundDrawablesWithIntrinsicBounds(null, null,
            if (validator()) passIcon else failIcon, null)
    return validator()
}

当您使用高阶函数时,生成的 Java 字节码会创建匿名类的实例,并将传入的函数作为匿名类的成员进行调用。 这会产生性能开销,因为需要将类加载到内存中。在上面的示例中,每次对 activity 中的 validateWith() 的调用都将创建一个包含验证器函数的匿名内部类,并在需要时调用它。

大多数情况下,使用高阶函数的主要原因是指定调用顺序或位置,如上例所示,必须调用传入函数来确定要加载哪个 drawable 并再次确定返回 Boolean。要防止创建这些匿名类实例,可以在定义高阶函数时使用 inline 关键字。 在这种情况下,内联函数的主体将被复制到调用它的位置,并且不会创建任何实例。

9.将 inline 关键字添加到 validateWith() 方法声明中。

Kotlin小贴士

  1. 通过在函数名前加上接收者类名和句点来定义类(称为接收者类)的 Extension 函数,例如:EditText.validateWith()。在函数内部,您可以调用类的成员(方法和属性),就好像函数是类本身的成员一样。
  2. 当编译器创建匿名内部类来调用传递的函数时,高阶函数会产生性能开销。在声明函数时使用 inline 关键字,通过将传入函数的内容复制到每个调用站点而不是生成匿名类来消除开销。

请参阅 Extensions 文档。

默认参数

Kotlin 提供了为函数参数声明默认值的功能。然后,在调用函数时,可以省略使用其默认值的参数,并仅使用 <variableName> = <value> 语法传递您选择的值。

1.在 Extensions.kt 文件的 validateWith() 方法中,将 pass 和 fail drawables 设置为前两个参数的默认值:

internal inline fun EditText.validateWith
        (passIcon: Drawable? = ContextCompat.getDrawable(context, R.drawable.ic_pass),
         failIcon: Drawable? = ContextCompat.getDrawable(context, R.drawable.ic_fail),
         validator: TextView.() -> Boolean): Boolean {

2.在 afterTextChanged() 方法中,删除每个调用 validateWith() 的 drawable 变量和前两个参数。 您必须在剩余的参数前面加上 validator = ,让编译器知道您传入的参数:

mEntryValid = mFirstNameEdit.validateWith(validator = notEmpty) and
                mLastNameEdit.validateWith(validator = notEmpty) and
                mEmailEdit.validateWith(validator = isEmail)

afterTextChanged() 方法现在更容易阅读:它声明两个lambda 表达式进行验证,并使用默认的 pass 和 fail drawables  validateWith() 扩展函数中传递它们。

Kotlin小贴士

  1. 您可以在函数中指定默认参数,以便可以在未传入任何参数的情况下调用它。
  2. 使用默认参数调用函数时,可以省略任何具有默认值的参数。但是,您必须使用<variableName> = <value>arguments字段中的语法指定您选择传入的参数的名称。

6. 恭喜!

我们所涵盖的内容

  • 如何使用 Android Studio 的 Java to Kotlin 转换器.
  • 如何使用 Kotlin 语法编写代码。
  • 如何使用Kotlin lambda表达式扩展函数来减少样板代码并避免常见错误。
  • 如何使用内置标准库函数来扩展现有的 Java 类函数。

参考

值得注意的 Kotlin 语法更改

Kotlin 转换器改变了许多代码以使用 Kotlin 特定语法。以下部分将指出转换器对 MyAddressBook app 所做的一些更改。

Kotlin 属性

在 Kotlin 中,您可以使用 object.property 语法直接访问对象的属性,而无需访问方法(Java 中的 getter 和 setter)。您可以在 ContactsActivity 类的许多地方看到这一点:

  • 在 setupRecyclerView() 方法中, recyclerView.setAdapter(mAdapter) 方法被替换为 recyclerView.adapter = mAdapter,viewHolder.getPosition() 被替换为 viewHolder.position。
  • EditText 的 setText( )和 getText() 方法将全部替换为 text 属性。setEnabled() 方法被替换为 isEnabled 属性。
  • mContacts 的大小全部使用 mContacts.size 访问。
  • 你可以使用 mContacts[index] 而不是 mContacts.get(index)来访问 mContacts 列表中的条目。

Lambda表达式

Kotlin 支持 lambda 表达式,该函数在使用前未声明,但可以作为表达式立即传递。

{ a, b -> a.length < b.length }

这在使用实现单个抽象方法的匿名内部类(如视图上的 setOnClickListener() 方法)时尤其有用。在这种情况下,可以直接传递 lambda 表达式,而不是匿名对象。转换器会自动为 ContactsActivity 中的每个 OnClickListener 执行此操作,例如:

fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        showAddContactDialog(-1);
    }
});

变成

fab.setOnClickListener { showAddContactDialog(-1) }

解构化声明

有时将对象解构为许多变量很方便。 例如,在适配器类的 onBindViewHolder() 方法中,使用对象的字段来使用数据填充 ViewHolder 是很常见的。

通过使用解构化声明(例如转换器在 onBindViewHolder() 声明中自动创建的声明),可以更轻松地在 Kotlin 中实现这一点。解构元素的顺序与原始类声明中的顺序相同:

val (firstName, lastName, email) = mContacts[position]

然后,您可以使用每个属性来填充视图:

val fullName = "$firstName $lastName"
holder.nameLabel.text = fullName
holder.emailLabel.text = email

Kotlin小贴士

  1. 您可以使用 object.property 语法表示法直接访问 Kotlin 属性,以获取和设置它们的值(如果某个值是不可变的 val,则无法设置该值)。
  2. Kotlin 支持使用 lambda 表达式,可以使用单个抽象方法简化匿名内部类。有关更多信息,请参阅Lambda文档。
  3. 您可以将实例解构为其组件属性。有关更多信息,请参阅 Destructuring Declarations 文档。

在哪儿了解更多

猜你喜欢

转载自blog.csdn.net/qq_33404903/article/details/88240877