促进 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。

科特林

厌倦了永无止境的 Java 代码? 尝试 Kotlin 并节省您的时间和理智。
鸣叫

Kotlin 设置

由于 Kotlin 是由 JetBrains 开发的,因此它在 Android Studio 和 IntelliJ 中都得到了很好的支持。

第一步是安装 Kotlin 插件。 成功执行此操作后,将可以使用新操作将您的 Java 转换为 Kotlin。 两个新选项是:

  1. 创建一个新的 Android 项目并在项目中设置 Kotlin。
  2. 将 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 节省了大量的打字工作

在 Kotlin 中定义变量时,有两种选择:

  • 可变变量,由var关键字定义。
  • 不可变变量,由val关键字定义。

接下来要注意的是语法与 Java 有点不同。 首先,声明变量名,然后跟上类型。 此外,默认情况下,属性是非空类型,这意味着它们不能接受null值。 要定义一个接受null值的变量,必须在类型之后添加一个问号。 稍后我们将在 Kotlin 中讨论这一点和零安全性。

另一个需要注意的重要事情是 Kotlin 没有为类声明字段的能力。 只能定义属性。 因此,在这种情况下, firstNamelastName是已分配默认 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 中的最终类一样。 因此,为了扩展一个类,它必须被声明为openabstract

 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-nullInt而不是Int? )。

可以使用安全调用运算符( ?. ) 简化null检查:

 val number: Int? = null number?.toString()

仅当数字不为null时才会执行第二行。 您甚至可以使用著名的Elvis 运算符( ?: ):

 val number Int? = null val stringNumber = number?.toString() ?: "Number is null"

如果?:左侧的表达式不为null ,则计算并返回。 否则,返回右侧表达式的结果。 另一个巧妙的事情是,您可以在 Elvis 运算符的右侧使用throwreturn ,因为它们是 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接口有两个版本:

  • 可迭代
  • 可变可迭代

CollectionListSetMap接口也是如此。

例如,如果至少一个元素与给定的谓词匹配,则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 项目时要慢一些。
  • 编译时间可能会更长一些。