促进 Android 开发的 10 个 Kotlin 特性
已发表: 2022-03-11介绍
前段时间,Tomasz 介绍了 Android 上的 Kotlin 开发。 提醒您:Kotlin 是由 Jetbrains 开发的一种新的编程语言,Jetbrains 是最受欢迎的 Java IDE 之一 IntelliJ IDEA 背后的公司。 与 Java 一样,Kotlin 是一种通用语言。 由于它符合 Java 虚拟机 (JVM) 字节码,因此可以与 Java 并行使用,并且不会带来性能开销。
在本文中,我将介绍促进 Android 开发的 10 大有用功能。
注意:在撰写本文时,实际版本为 Android Studio 2.1.1。 和 Kotlin 1.0.2。
Kotlin 设置
由于 Kotlin 是由 JetBrains 开发的,因此它在 Android Studio 和 IntelliJ 中都得到了很好的支持。
第一步是安装 Kotlin 插件。 成功执行此操作后,将可以使用新操作将您的 Java 转换为 Kotlin。 两个新选项是:
- 创建一个新的 Android 项目并在项目中设置 Kotlin。
- 将 Kotlin 支持添加到现有的 Android 项目。
要了解如何创建新的 Android 项目,请查看官方分步指南。 要将 Kotlin 支持添加到新创建或现有项目中,请在 Mac 上使用Command + Shift + A
或在 Windows/Linux 上使用Ctrl + Shift + A
打开查找操作对话框,然后调用Configure Kotlin in Project
操作。
要创建一个新的 Kotlin 类,请选择:
-
File
>New
>Kotlin file/class
,或 File
>New
>Kotlin activity
或者,您可以创建一个 Java 类并使用上述操作将其转换为 Kotlin。 请记住,您可以使用它来转换任何类、接口、枚举或注解,这可以用来轻松地将 Java 与 Kotlin 代码进行比较。
另一个节省大量输入的有用元素是 Kotlin 扩展。 要使用它们,您必须在模块build.gradle
文件中应用另一个插件:
apply plugin: 'kotlin-android-extensions'
警告:如果您使用 Kotlin 插件操作来设置您的项目,它会将以下代码放入您的顶级build.gradle
文件中:
buildscript { ext.kotlin_version = '1.0.2' repositories { jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } }
这将导致扩展程序不起作用。 要解决这个问题,只需将该代码复制到您希望使用 Kotlin 的每个项目模块中。
如果您正确设置了所有内容,您应该能够像在标准 Android 项目中一样运行和测试您的应用程序,但现在使用的是 Kotlin。
使用 Kotlin 节省时间
因此,让我们从描述 Kotlin 语言的一些关键方面开始,并提供有关如何通过使用它而不是 Java 来节省时间的提示。
功能 #1:静态布局导入
Android 中最常见的样板代码之一是使用findViewById()
函数来获取对活动或片段中视图的引用。
有一些解决方案,例如 Butterknife 库,可以节省一些输入,但 Kotlin 又迈出了一步,允许您通过一次导入从布局中导入对视图的所有引用。
例如,考虑以下活动 XML 布局:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:andro xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="co.ikust.kotlintest.MainActivity"> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> </RelativeLayout>
以及随附的活动代码:
package co.ikust.kotlintest import android.support.v7.app.AppCompatActivity import android.os.Bundle import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) helloWorldTextView.text = "Hello World!" } }
要获取布局中具有定义 ID 的所有视图的引用,请使用 Android Kotlin 扩展 Anko。 请记住输入此导入语句:
import kotlinx.android.synthetic.main.activity_main.*
请注意,您不需要在 Kotlin 中的行尾写分号,因为它们是可选的。
布局中的TextView
被导入为TextView
实例,其名称等于视图的 ID。 不要被用于设置标签的语法所迷惑:
helloWorldTextView.text = "Hello World!"
我们将很快介绍这一点。
注意事项:
- 确保导入正确的布局,否则导入的视图引用将具有
null
值。 - 使用片段时,请确保在
onCreateView()
函数调用之后使用导入的视图引用。 在onCreateView()
函数中导入布局,并使用视图引用在onViewCreated()
中设置 UI。 在onCreateView()
方法完成之前不会分配引用。
特性 #2:使用 Kotlin 编写 POJO 类
使用 Kotlin 可以节省最多时间的方法是编写用于保存数据的 POJO(Plain Old Java Object)类。 例如,在 RESTful API 的请求和响应正文中。 在依赖 RESTful API 的应用程序中,会有很多这样的类。
在 Kotlin 中,为您做了很多事情,而且语法简洁。 例如,考虑 Java 中的以下类:
public class User { private String firstName; private String lastName; public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
使用 Kotlin 时,您不必再次编写 public 关键字。 默认情况下,一切都是公共范围的。 例如,如果要声明一个类,只需编写:
class MyClass { }
上面 Kotlin 中的 Java 代码的等价物:
class User { var firstName: String? = null var lastName: String? = null }
嗯,这样可以节省很多打字时间,不是吗? 让我们来看看 Kotlin 代码。
在 Kotlin 中定义变量时,有两种选择:
- 可变变量,由
var
关键字定义。 - 不可变变量,由
val
关键字定义。
接下来要注意的是语法与 Java 有点不同。 首先,声明变量名,然后跟上类型。 此外,默认情况下,属性是非空类型,这意味着它们不能接受null
值。 要定义一个接受null
值的变量,必须在类型之后添加一个问号。 稍后我们将在 Kotlin 中讨论这一点和零安全性。
另一个需要注意的重要事情是 Kotlin 没有为类声明字段的能力。 只能定义属性。 因此,在这种情况下, firstName
和lastName
是已分配默认 getter/setter 方法的属性。 如前所述,在 Kotlin 中,默认情况下它们都是公共的。
可以编写自定义访问器,例如:
class User { var firstName: String? = null var lastName: String? = null val fullName: String? get() firstName + " " + lastName }
从外部看,在语法方面,属性的行为类似于 Java 中的公共字段:
val userName = user.firstName user.firstName = "John"
请注意,新属性fullName
是只读的(由val
关键字定义)并具有自定义 getter; 它只是附加名字和姓氏。
Kotlin 中的所有属性都必须在声明时或在构造函数中时赋值。 在某些情况下不方便; 例如,对于将通过依赖注入初始化的属性。 在这种情况下,可以使用lateinit
修饰符。 这是一个例子:
class MyClass { lateinit var firstName : String; fun inject() { firstName = "John"; } }
有关属性的更多详细信息可以在官方文档中找到。
特性#3:类继承和构造函数
Kotlin 在构造函数方面也有更简洁的语法。
构造函数
Kotlin 类有一个主构造函数和一个或多个辅助构造函数。 定义主构造函数的示例:
class User constructor(firstName: String, lastName: String) { }
主构造函数位于类定义中的类名之后。 如果主构造函数没有任何注释或可见性修饰符,则可以省略构造函数关键字:
class Person(firstName: String) { }
请注意,主构造函数不能有任何代码; 任何初始化都必须在init
代码块中完成:
class Person(firstName: String) { init { //perform primary constructor initialization here } }
此外,主构造函数可用于定义和初始化属性:
class User(var firstName: String, var lastName: String) { // ... }
就像常规属性一样,从主构造函数定义的属性可以是不可变的 ( val
) 或可变的 ( var
)。
类也可能有辅助构造函数; 定义一个的语法如下:
class User(var firstName: String, var lastName) { constructor(name: String, parent: Person) : this(name) { parent.children.add(this) } }
请注意,每个辅助构造函数都必须委托给主构造函数。 这类似于使用this
关键字的 Java:
class User(val firstName: String, val lastName: String) { constructor(firstName: String) : this(firstName, "") { //... } }
实例化类时,请注意 Kotlin 没有new
关键字,Java 也是如此。 要实例化上述User
类,请使用:
val user = User("John", "Doe)
介绍继承
在 Kotlin 中,所有类都从Any
扩展而来,这类似于 Java 中的Object
。 默认情况下,类是关闭的,就像 Java 中的最终类一样。 因此,为了扩展一个类,它必须被声明为open
或abstract
:
open class User(val firstName, val lastName) class Administrator(val firstName, val lastName) : User(firstName, lastName)
请注意,您必须委托给扩展类的默认构造函数,这类似于在 Java 中的新类的构造函数中调用super()
方法。
有关类的更多详细信息,请查看官方文档。
功能 #4:Lambda 表达式
随 Java 8 引入的 Lambda 表达式是它最喜欢的特性之一。 然而,Android 上的情况并不那么乐观,因为它仍然只支持 Java 7,而且看起来 Java 8 不会很快得到支持。 因此,Retrolambda 等变通方法将 lambda 表达式引入 Android。
使用 Kotlin,不需要额外的库或变通方法。
Kotlin 中的函数
让我们先快速回顾一下 Kotlin 中的函数语法:
fun add(x: Int, y: Int) : Int { return x + y }
函数的返回值可以省略,在这种情况下,函数将返回Int
。 值得重申的是,Kotlin 中的一切都是对象,从Any
扩展而来,并且没有原始类型。
函数的参数可以有一个默认值,例如:
fun add(x: Int, y: Int = 1) : Int { return x + y; }
在这种情况下,可以通过仅传递x
参数来调用add()
函数。 等效的 Java 代码将是:
int add(int x) { Return add(x, 1); } int add(int x, int y) { return x + y; }
调用函数时的另一个好处是可以使用命名参数。 例如:
add(y = 12, x = 5)
有关函数的更多详细信息,请查看官方文档。
在 Kotlin 中使用 Lambda 表达式
Kotlin 中的 Lambda 表达式可以被视为 Java 中的匿名函数,但语法更简洁。 作为一个例子,让我们展示如何在 Java 和 Kotlin 中实现点击监听器。
在 Java 中:
view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(v.getContext(), "Clicked on view", Toast.LENGTH_SHORT).show(); } };
在科特林:
view.setOnClickListener({ view -> toast("Click") })
哇! 只需一行代码! 我们可以看到 lambda 表达式被花括号包围。 参数首先声明,主体在->
符号之后。 使用单击侦听器,未指定视图参数的类型,因为它可以被推断出来。 主体只是对用于显示 Toast 的toast()
函数的调用,这是 Kotlin 提供的。

此外,如果不使用参数,我们可以将它们排除在外:
view.setOnClickListener({ toast("Click") })
Kotlin 优化了 Java 库,任何接收接口的函数都可以使用函数参数(而不是接口)调用参数的一种方法。
此外,如果函数是最后一个参数,则可以将其移出括号:
view.setOnClickListener() { toast("Click") }
最后,如果函数只有一个参数是函数,则可以省略括号:
view.setOnClickListener { toast("Click") }
有关更多信息,请查看 Antonio Leiva 的 Kotlin for Android 开发人员一书和官方文档。
扩展功能
Kotlin 与 C# 类似,提供了通过使用扩展函数来扩展现有类的能力。 例如,将计算String
的 MD5 哈希的扩展方法:
fun String.md5(): ByteArray { val digester = MessageDigest.getInstance("MD5") digester.update(this.toByteArray(Charset.defaultCharset())) return digester.digest() }
请注意,函数名称前面是扩展类的名称(在本例中为String
),并且扩展类的实例可通过this
关键字获得。
扩展函数等价于 Java 实用函数。 Java 中的示例函数如下所示:
public static int toNumber(String instance) { return Integer.valueOf(instance); }
示例函数必须放在 Utility 类中。 这意味着扩展函数不会修改原始扩展类,而是一种编写实用方法的便捷方式。
功能 #5:空安全
在 Java 中你最忙的事情之一可能是NullPointerException
。 Null-safety 是一项已集成到 Kotlin 语言中的功能,并且非常隐含,您通常不必担心。 官方文档指出NullPointerExceptions
的唯一可能原因是:
- 显式调用 throw
NullPointerException
。 - 使用
!!
运算符(我稍后会解释)。 - 外部 Java 代码。
- 如果在
UninitializedPropertyAccessException
之前在构造函数中访问了lateinit
属性,则会抛出 UninitializedPropertyAccessException。
默认情况下,如果 Kotlin 中的所有变量和属性没有显式声明为可空,则它们都被视为non-null
(无法保存null
值)。 如前所述,要定义一个接受null
值的变量,必须在类型后添加一个问号。 例如:
val number: Int? = null
但是,请注意以下代码不会编译:
val number: Int? = null number.toString()
这是因为编译器执行null
检查。 要编译,必须添加一个null
检查:
val number: Int? = null if(number != null) { number.toString(); }
此代码将成功编译。 在这种情况下,Kotlin 在后台所做的是,该number
在 if 块内变为nun-null
( Int
而不是Int?
)。
可以使用安全调用运算符( ?.
) 简化null
检查:
val number: Int? = null number?.toString()
仅当数字不为null
时才会执行第二行。 您甚至可以使用著名的Elvis 运算符( ?:
):
val number Int? = null val stringNumber = number?.toString() ?: "Number is null"
如果?:
左侧的表达式不为null
,则计算并返回。 否则,返回右侧表达式的结果。 另一个巧妙的事情是,您可以在 Elvis 运算符的右侧使用throw
或return
,因为它们是 Kotlin 中的表达式。 例如:
fun sendMailToUser(user: User) { val email = user?.email ?: throw new IllegalArgumentException("User email is null") //... }
这 !! 操作员
如果你想像在 Java 中一样抛出NullPointerException
,你可以用!!
操作员。 以下代码将抛出NullPointerException
:
val number: Int? = null number!!.toString()
铸件
使用as
关键字进行投射:
val x: String = y as String
这被认为是“不安全”的强制转换,因为如果无法进行强制转换,它将抛出ClassCastException
,就像 Java 那样。 有一个“安全”转换运算符返回null
值而不是抛出异常:
val x: String = y as? String
有关转换的更多详细信息,请查看官方文档的 Type Casts 和 Casts 部分,有关null
安全性的更多详细信息,请查看 Null-Safety 部分。
lateinit
属性
在某些情况下,使用lateinit
属性可能会导致类似于NullPointerException
的异常。 考虑以下类:
class InitTest { lateinit var s: String; init { val len = this.s.length } }
此代码将在没有警告的情况下编译。 但是,一旦创建了TestClass
的实例,就会抛出UninitializedPropertyAccessException
,因为属性s
在初始化之前就被访问了。
功能 #6:函数with()
with()
函数很有用,它随 Kotlin 标准库提供。 如果您需要访问对象的许多属性,它可以用来节省一些输入。 例如:
with(helloWorldTextView) { text = "Hello World!" visibility = View.VISIBLE }
它接收一个对象和一个扩展函数作为参数。 代码块(在花括号中)是指定为第一个参数的对象的扩展函数的 lambda 表达式。
功能 #7:运算符重载
使用 Kotlin,可以为一组预定义的运算符提供自定义实现。 要实现运算符,必须提供具有给定名称的成员函数或扩展函数。
例如,要实现乘法运算符,必须提供名称为times(argument)
的成员函数或扩展函数:
operator fun String.times(b: Int): String { val buffer = StringBuffer() for (i in 1..b) { buffer.append(this) } return buffer.toString() }
上面的示例显示了String
上二元*
运算符的实现。 例如,以下表达式会将值“TestTestTestTest”分配给newString
变量:
val newString = "Test" * 4
由于可以使用扩展函数,这意味着可以更改所有对象的运算符的默认行为。 这是一把双刃剑,应谨慎使用。 有关可以重载的所有运算符的函数名称列表,请查看官方文档。
与 Java 相比的另一个重大区别是==
和!=
运算符。 运算符==
转换为:
a?.equals(b) ?: b === null
而运算符!=
转换为:
!(a?.equals(b) ?:
这意味着,使用==
不会像在 Java 中那样进行身份检查(比较对象的实例是否相同),但其行为方式与equals()
方法以及null
检查相同。
要执行身份检查,必须在 Kotlin 中使用运算符===
和!==
。
特性 #8:委托属性
某些属性有一些共同的行为。 例如:
- 首次访问时初始化的延迟初始化属性。
- 在观察者模式中实现 Observable 的属性。
- 存储在地图中的属性,而不是作为单独的字段。
为了使这种情况更容易实现,Kotlin 支持Delegated Properties :
class SomeClass { var p: String by Delegate() }
这意味着属性p
的 getter 和 setter 函数由另一个类的实例Delegate
处理。
String
属性的委托示例:
class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, thank you for delegating '${property.name}' to me!" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$value has been assigned to '${property.name} in $thisRef.'") } }
上面的示例在分配或读取属性时打印一条消息。
可以为可变 ( var
) 和只读 ( val
) 属性创建委托。
对于只读属性,必须实现getValue
方法。 它有两个参数(取自官方文档):
- 接收者 - 必须是属性所有者的相同类型或超类型(对于扩展属性,它是被扩展的类型)。
- 元数据 - 必须是
KProperty<*>
类型或其超类型。
此函数必须返回与属性或其子类型相同的类型。
对于可变属性,委托必须另外提供一个名为setValue
的函数,该函数采用以下参数:
- 接收者 - 与
getValue()
相同。 - 元数据 - 与
getValue()
相同。 - 新值 - 必须与属性或其超类型具有相同的类型。
Kotlin 附带了一些标准代表,涵盖了最常见的情况:
- 懒惰的
- 可观察的
- 可否决
懒惰的
Lazy 是一个标准委托,它接受一个 lambda 表达式作为参数。 传递的 lambda 表达式在第一次调用getValue()
方法时执行。
默认情况下,惰性属性的评估是同步的。 如果你不关心多线程,你可以使用lazy(LazyThreadSafetyMode.NONE) { … }
来获得额外的性能。
可观察的
Delegates.observable()
用于在观察者模式中表现得像 Observables 的属性。 它接受两个参数,初始值和一个具有三个参数(属性、旧值和新值)的函数。
每次调用setValue()
方法时,都会执行给定的 lambda 表达式:
class User { var email: String by Delegates.observable("") { prop, old, new -> //handle the change from old to new value } }
可否决
这个标准委托是一种特殊的 Observable,可以让您决定是否存储分配给属性的新值。 它可用于在分配值之前检查某些条件。 与Delegates.observable()
一样,它接受两个参数:初始值和一个函数。
不同之处在于该函数返回一个布尔值。 如果它返回true
,则分配给该属性的新值将被存储或以其他方式丢弃。
var positiveNumber = Delegates.vetoable(0) { d, old, new -> new >= 0 }
给定的示例将仅存储分配给属性的正数。
更多详细信息,请查看官方文档。
功能 #9:将对象映射到地图
一个常见的用例是将属性的值存储在地图中。 这经常发生在使用 RESTful API 并解析 JSON 对象的应用程序中。 在这种情况下,地图实例可以用作委托属性的委托。 官方文档中的一个例子:
class User(val map: Map<String, Any?>) { val name: String by map val age: Int by map }
在这个例子中, User
有一个带地图的主构造函数。 这两个属性将从映射中获取与属性名称相同的键映射的值:
val user = User(mapOf( "name" to "John Doe", "age" to 25 ))
新用户实例的 name 属性将被赋值为“John Doe”,age 属性值为 25。
这也适用于结合MutableMap
的 var 属性:
class MutableUser(val map: MutableMap<String, Any?>) { var name: String by map var age: Int by map }
功能 #10:集合和功能操作
借助 Kotlin 中对 lambdas 的支持,可以将集合利用到一个新的水平。
首先,Kotlin 区分了可变集合和不可变集合。 例如, Iterable接口有两个版本:
- 可迭代
- 可变可迭代
Collection 、 List 、 Set和Map接口也是如此。
例如,如果至少一个元素与给定的谓词匹配,则any
操作都会返回true
:
val list = listOf(1, 2, 3, 4, 5, 6) assertTrue(list.any { it % 2 == 0 })
有关可以对集合执行的功能操作的详细列表,请查看此博客文章。
结论
我们刚刚触及了 Kotlin 所提供功能的皮毛。 对于那些有兴趣进一步阅读和了解更多信息的人,请查看:
- Antonio Leiva 的 Kotlin 博客文章和书籍。
- 来自 JetBrains 的官方文档和教程。
总而言之,Kotlin 通过使用直观和简洁的语法为您提供了在编写原生 Android 应用程序时节省时间的能力。 它仍然是一种年轻的编程语言,但在我看来,它现在足够稳定,可以用于构建生产应用程序。
使用 Kotlin 的好处:
- Android Studio 的支持是无缝且出色的。
- 将现有的 Java 项目转换为 Kotlin 很容易。
- Java 和 Kotlin 代码可以共存于同一个项目中。
- 应用程序中没有速度开销。
缺点:
- Kotlin 会将其库添加到生成的
.apk
中,因此最终的.apk
大小将大约大 300KB。 - 如果滥用,运算符重载会导致代码不可读。
- 使用 Kotlin 时,IDE 和自动完成的行为比使用纯 Java Android 项目时要慢一些。
- 编译时间可能会更长一些。