作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Ivan Kušt's profile image

Ivan Kušt

Ivan是一个移动爱好者,他完善了移动应用程序的开发过程和架构.

Previously At

Infinum
Share

Introduction

A while ago, Tomasz介绍了Kotlin在Android上的开发. 提醒您:Kotlin是由 Jetbrains该公司是最流行的Java ide之一的背后, IntelliJ IDEA. 与Java一样,Kotlin也是一种通用语言. 因为它符合Java虚拟机(JVM)字节码, 它可以与Java并行使用, 而且它不会带来性能开销.

在本文中,我将介绍促进Android开发的十大有用特性.

Note在撰写本文时,实际版本是Android Studio 2.1.1. and Kotlin 1.0.2.

Kotlin

Tired of never-ending Java code? 试试Kotlin,节省你的时间和理智.

Kotlin Setup

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

The first step is to install Kotlin plugin. 成功这样做之后,将有新的操作可用于将Java转换为Kotlin. Two new options are:

  1. 创建一个新的Android项目,并在项目中设置Kotlin.
  2. 为现有的Android项目添加Kotlin支持.

要了解如何创建一个新的Android项目,请查看 official step by step guide. 要向新创建的或现有的项目添加Kotlin支持,请打开 find action dialog using Command + Shift + A on Mac or Ctrl + Shift + A on Windows/Linux, and invoke the Configure Kotlin in Project action.

要创建一个新的Kotlin类,选择:

  • File > New > Kotlin file/class, or
  • File > New > Kotlin activity

或者,您可以创建一个Java类,并使用上面提到的操作将其转换为Kotlin. Remember, 您可以使用它来转换任何类, interface, enum or annotation, 这可以用来比较Java和Kotlin代码.

另一个节省大量输入的有用元素是Kotlin扩展. 要使用它们,你必须在你的模块中应用另一个插件 build.gradle file:

应用插件:'kotlin-android-extensions'

Caveat:如果你正在使用Kotlin插件动作来设置你的项目, 它将把以下代码放在顶层 build.gradle file:

buildscript {
   ext.kotlin_version = '1.0.2'
   repositories {
       jcenter()
   }
   dependencies {
       classpath "org.jetbrains.芬兰湾的科特林:kotlin-gradle-plugin: $ kotlin_version”

       // NOTE: Do not place your application dependencies here; they belong
       // in the individual module build.gradle files
   }
}

这将导致扩展不工作. 要解决这个问题,只需将该代码复制到希望使用Kotlin的每个项目模块中.

If you setup everything correctly, 你应该能够像在标准Android项目中那样运行和测试你的应用程序, but now using Kotlin.

Saving Time with Kotlin

So, 让我们从描述Kotlin语言的一些关键方面开始,并提供一些如何使用Kotlin代替Java来节省时间的技巧.

Feature #1: Static Layout Import

Android中最常见的样板代码之一是使用 findViewById() 函数来获取对活动或片段中视图的引用.

There are solutions, such as the Butterknife library, that save some typing, 但是Kotlin又迈出了一步,允许您通过一次导入从布局中导入对视图的所有引用.

例如,考虑以下活动XML布局:




    

以及附带的活动代码:

package co.ikust.kotlintest

import android.support.v7.app.AppCompatActivity
import android.os.Bundle

import kotlinx.android.synthetic.main.activity_main.*

类MainActivity: AppCompatActivity() {

    重载onCreate(savedInstanceState: Bundle)?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        helloWorldTextView.text = "Hello World!"
    }
}

获取具有已定义ID的布局中所有视图的引用, use the Android Kotlin extension Anko. 记住输入这个import语句:

import kotlinx.android.synthetic.main.activity_main.*

注意,在Kotlin中不需要在行尾写分号,因为分号是可选的.

The TextView from layout is imported as a TextView 实例的名称等于视图的ID. 不要被语法弄糊涂了,它是用来设置标签的:

helloWorldTextView.text = "Hello World!"

We will cover that shortly.

Caveats:

  • 确保导入了正确的布局,否则导入的视图引用将具有 null value.
  • 当使用片段时,请确保使用了导入的视图引用 after the onCreateView() function call. Import the layout in onCreateView() 函数,并使用View引用来设置UI onViewCreated(). 参考文献不会在 onCreateView() method has finished.

特性#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 saves a lot of typing

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

  • Mutable variables, defined by var keyword.
  • Immutable variables, defined by val keyword.

The next thing to note is the syntax differs a bit from Java; first, 先声明变量名,然后再声明类型. 此外,默认情况下,属性是非空类型,这意味着它们不能接受 null value. To define a variable to accept a null 值时,必须在类型后面加上问号. 稍后我们将在Kotlin中讨论这个和null-safety.

Another important thing to note is that Kotlin doesn’t have the ability to declare fields for the class; only properties can be defined. So, in this case, firstName and 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"

Note that the new property fullName is read only (defined by val keyword) and has a custom getter; it simply appends first and last name.

Kotlin中的所有属性都必须在声明时或在构造函数中赋值. There are some cases when that isn’t convenient; for example, 对于将通过依赖注入初始化的属性. In that case, a lateinit modifier can be used. Here is an example:

class MyClass {
    lateinit var firstName : String;

    fun inject() {
        firstName = "John";
    }
}

有关属性的更多详细信息,请参见 official documentation.

特性#3:类继承和构造函数

Kotlin在构造函数方面也有更简洁的语法.

Constructors

Kotlin类有一个主构造函数和一个或多个辅助构造函数. 定义主构造函数的一个例子:

类用户构造函数(firstName: String, lastName: String) {
}

主构造函数位于类定义中的类名之后. 如果主构造函数没有任何注释或可见性修饰符, 可以省略constructor关键字:

class Person(firstName: String) {
}

Note that a primary constructor cannot have any code; any initialization must be done in the init code block:

class Person(firstName: String) {
    init {
         //在这里执行主构造函数初始化
    }
}

此外,主构造函数可用于定义和初始化属性:

类用户(var firstName:字符串,var lastName:字符串){
  // ...
}

与普通构造函数一样,从主构造函数定义的属性可以是不可变的(val) or mutable (var).

Classes may have secondary constructors as well; the syntax for defining one is as follows:

类用户(var firstName: String, var lastName) {
    构造函数(name: String, parent: Person): this(name) {
        parent.children.add(this)
    }
}

注意,每个辅助构造函数都必须委托给主构造函数. 这与Java类似,Java使用 this keyword:

类用户(val firstName: String, val lastName: String) {
    构造函数(firstName: String): this(firstName, "") {
       //...
    }
}

在实例化类时,请注意Kotlin没有 new keywords, as does Java. To instantiate the aforementioned User class, use:

val user = User("John", "Doe)

Introducing Inheritance

在Kotlin中,所有类都从 Any, which is similar to Object in Java. 默认情况下,类是封闭的,就像Java中的final类一样. 因此,为了扩展一个类,必须将它声明为 open or abstract:

打开类User(val firstName, val lastName)
类管理员(val firstName, val lastName):用户(firstName, lastName)

注意,您必须委托给扩展类的默认构造函数, which is similar to calling super() 方法在Java的新类的构造函数中.

有关课程的更多详细信息,请查看 the official documentation.

Feature #4: Lambda Expressions

Java 8引入的Lambda表达式是它最喜欢的特性之一. However, Android的情况就不那么乐观了, as it still only supports Java 7, 看起来Java 8短期内不会被支持. So, workarounds, such as Retrolambda,将lambda表达式引入Android.

使用Kotlin,不需要额外的库或解决方案.

Functions in Kotlin

让我们先快速浏览一下Kotlin中的函数语法:

fun add(x: Int, y: Int) : Int {
    return x + y
}

函数的返回值可以省略,在这种情况下,函数将返回 Int. 值得重复的是,Kotlin中的所有东西都是对象,从 Any,并且没有原始类型.

函数的参数可以有一个默认值,例如:

add(x: Int, y: Int = 1): Int {
    return x + y;
}

In that case, the add() 函数可以通过只传递 x argument. 等效的Java代码是:

int add(int x) {
   Return add(x, 1);
}

int add(int x, int y) {
    return x + y;
}

调用函数的另一个好处是可以使用命名参数. For example:

add(y = 12, x = 5)

有关函数的更多详细信息,请查看 official documentation.

在Kotlin中使用Lambda表达式

Kotlin中的Lambda表达式可以看作是Java中的匿名函数, but with a more concise syntax. 作为示例,让我们展示如何在Java和Kotlin中实现单击侦听器.

In Java:

view.setOnClickListener(new OnClickListener()) {
    @Override
    public void onClick(View v) {
        Toast.makeText(v.getContext(), " click on view", Toast.LENGTH_SHORT).show();
    }
};

In Kotlin:

view.setOnClickListener({ view -> toast("Click") })

Wow! Just one line of code! 我们可以看到,lambda表达式被花括号包围. 首先声明形参,然后声明体 -> sign. 对于click listener,视图参数的类型没有指定,因为它可以被推断出来. The body is simply a call to toast() Kotlin提供的显示toast的函数.

同样,如果不使用参数,我们可以省略它们:

view.setOnClickListener({toast("Click")})

Kotlin优化了Java库, 任何接收到带有一个方法作为参数的接口的函数都可以用函数参数调用(而不是interface)。.

此外,如果函数是最后一个参数,则可以将其移出括号:

view.setOnClickListener() {toast("Click")}

最后,如果函数只有一个函数形参,括号可以省略:

view.setOnClickListener {toast("Click")}

For more information, check Kotlin for Android developers book by Antonio Leiva and the official documentation.

Extension Functions

Kotlin, similar to C#, 提供通过使用扩展函数扩展具有新功能的现有类的能力. 例如,一个扩展方法可以计算a的MD5哈希值 String:

fun String.md5(): ByteArray {
    val digester = MessageDigest.getInstance("MD5")
    digester.update(this.toByteArray(Charset.defaultCharset()))
    return digester.digest()
}

请注意,函数名前面是扩展类的名称(在本例中, String),并且扩展类的实例可以通过 this keyword.

扩展函数相当于Java实用程序函数. Java中的示例函数是这样的:

public static int number (String instance) {
	return Integer.valueOf(instance);
}

示例函数必须放在Utility类中. 这意味着扩展函数不会修改原始的扩展类, 而是一种编写实用程序方法的方便方法.

Feature #5: Null-safety

在Java中你最忙的一件事可能是 NullPointerException. null安全是集成到Kotlin语言中的一个特性,它是隐式的,您通常不必担心. The official documentation 说明唯一可能的原因 NullPointerExceptions are:

  • An explicit call to throw NullPointerException.
  • Using the !! 操作符(我将在后面解释).
  • External Java code.
  • If the lateinit 属性在初始化之前在构造函数中访问UninitializedPropertyAccessException will be thrown.

默认情况下,会考虑Kotlin中的所有变量和属性 non-null (unable to hold a null 值),如果它们没有显式声明为可空的话. 如前所述,定义一个变量来接受 null 值时,必须在类型后面加上问号. For example:

val number: Int? = null

但是,请注意以下代码将无法编译:

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

这是因为编译器执行 null checks. To compile, a null check must be added:

val number: Int? = null

if(number != null) {
    number.toString();
}

此代码将成功编译. 在本例中,Kotlin在后台所做的是 number becomes nun-null (Int instead of Int?) inside the if block.

The null check can be simplified using safe call operator (?.):

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

只有当编号不是时才执行第二行 null. You can even use the famous Elvis operator (?:):

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

If the expression on the left of ?: is not null, it is evaluated and returned. 否则,返回右边表达式的结果. 你还可以使用 throw or return 在猫王运算符的右边,因为它们是Kotlin中的表达式. For example:

fun sendMailToUser(user: User) {
    val email = user?.email ?抛出新的IllegalArgumentException("User email is null")
    //...
}

The !! Operator

If you want a NullPointerException 抛出的方式与Java中相同,您可以使用 !! operator. The following code will throw a NullPointerException:

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

Casting

Casting in done by using an as keyword:

val x: String = y as String

这被认为是“不安全的”cast,因为它会抛出 ClassCastException 如果不可能强制转换,就像Java一样. 有一个“安全”强制转换操作符返回 null 值,而不是抛出异常:

val x: String = y as? String

有关铸造的更多细节,请查看 Type Casts and Casts 部分的官方文件,以及更多的详细信息 null safety check the Null-Safety section.

lateinit properties

There is a case in which using lateinit 属性可能导致类似的异常 NullPointerException. Consider the following class:

class InitTest {
    lateinit var s: String;

    init {
        val len = this.s.length
    }

}

此代码将在没有警告的情况下编译. 然而,一旦一个实例 TestClass is created, an UninitializedPropertyAccessException will be thrown because property s 是否在初始化之前被访问.

Feature #6: Function with()

Function with() 是有用的,并随Kotlin标准库一起提供的. 如果需要访问对象的许多属性,可以使用它来节省一些输入. For example:

with(helloWorldTextView) {
    text = "Hello World!"
    visibility = View.VISIBLE
}

它接收一个对象和一个扩展函数作为参数. 代码块(在花括号中)是一个lambda表达式,用于作为第一个参数指定的对象的扩展函数.

Feature #7: Operator Overloading

使用Kotlin,可以为一组预定义的操作符提供自定义实现. To implement an operator, 必须提供具有给定名称的成员函数或扩展函数.

For example, 实现乘法运算符, 成员函数或扩展函数, with the name times(argument), must be provided:

operator fun String.times(b: Int): String {
    val buffer = StringBuffer()

    for (i in 1..b) {
        buffer.append(this)
    }

    return buffer.toString()
}

上面的示例显示了二进制的实现 * operator on the String. 例如,下面的表达式将值" TestTestTestTest "赋值给a newString variable:

val newString = "Test" * 4

因为可以使用扩展函数, 这意味着可以更改所有对象的操作符的默认行为. 这是一把双刃剑,应该谨慎使用. 有关可重载的所有操作符的函数名列表,请查看 official documentation.

与Java相比,另一个很大的不同是 == and != operators. Operator == translates to:

a?.equals(b) ?: b === null 

While operator != translates to:

!(a?.equals(b) ?:

What that means, is that using == 不像Java那样进行身份检查(比较对象的实例是否相同), but behaves the same way as equals() method along with null checks.

要执行身份检查,操作符 === and !== must be used in Kotlin.

Feature #8: Delegated Properties

某些属性共享一些共同的行为. For instance:

  • 在第一次访问时初始化的延迟初始化属性.
  • 在Observer模式中实现Observable的属性.
  • 属性存储在映射中,而不是作为单独的字段.

为了使这样的情况更容易实现,Kotlin支持 Delegated Properties:

class SomeClass {
    var p: String by Delegate()
}

这意味着属性的getter和setter函数 p 由另一个类的实例处理, Delegate.

An example of a delegate for the String property:

class Delegate {
  操作符getValue(thisRef: Any)?, property: KProperty<*>): String {
    $thisRef,感谢您委托${属性.name}' to me!"
  }
 
  operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    Println ("$value已被赋值给${属性.name} in $thisRef.'")
  }
}

上面的示例在分配或读取属性时打印一条消息.

可以为可变的(var) and read-only (val) properties.

For a read-only property, getValue method must be implemented. 它接受两个参数(取自 offical documentation):

  • 接收者-必须是属性所有者的相同或超类型(对于扩展属性), it is the type being extended).
  • metadata - must be of type KProperty<*> or its supertype.

此函数必须返回与属性或其子类型相同的类型.

对于可变属性,委托必须另外提供一个名为 setValue 它接受以下参数:

  • receiver - same as for getValue().
  • metadata - same as for getValue().
  • 新值——必须与属性或其超类型具有相同的类型.

Kotlin提供了一些标准委托,涵盖了最常见的情况:

  • Lazy
  • Observable
  • Vetoable

Lazy

Lazy是一个接受lambda表达式作为参数的标准委托. 传递的lambda表达式第一次执行 getValue() method is called.

默认情况下,惰性属性的计算是同步的. 如果您不关心多线程,您可以使用 lazy(LazyThreadSafetyMode.NONE) { … } to get extra performance.

Observable

The Delegates.observable() 是为属性,应该表现为观察者模式中的可观察对象. It accepts two parameters, 初始值和一个有三个参数的函数(属性, old value, and new value).

每次都会执行给定的lambda表达式 setValue() method is called:

class User {
    var email: String by Delegates.observable("") {
        prop, old, new ->
        //处理从旧值到新值的更改
    }
}

Vetoable

这个标准委托是一种特殊的可观察对象,它让你决定是否存储分配给属性的新值. 它可以用来在赋值之前检查一些条件. As with Delegates.observable(),它接受两个参数:初始值和一个函数.

不同之处在于该函数返回一个布尔值. If it returns true,则分配给该属性的新值将被存储,否则将被丢弃.

var positiveNumber = Delegates.vetoable(0) {
    d, old, new ->
    new >= 0
}

给定的示例将只存储分配给该属性的正数.

For more details, check the official documentation.

特性#9:将对象映射到映射

一个常见的用例是将属性的值存储在映射中. 这经常发生在使用RESTful api和解析JSON对象的应用程序中. 在这种情况下,映射实例可以用作委托属性的委托. An example from the official documentation:

class User(val map: Map) {
    val name: String by map
    val age: Int     by map
}

In this example, User 有一个接受映射的主构造函数. 这两个属性将从映射中获取值,这些值映射在等于属性名称的键下:

val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))

新用户实例的name属性将被赋值为“John Doe”,age属性的值为25.

这适用于var属性与 MutableMap as well:

class MutableUser(val map: MutableMap) {
    var name: String by map
    var age: Int     by map
}

特性#10:集合和函数操作

有了Kotlin中对lambdas的支持,集合可以被利用到一个新的高度.

首先,Kotlin区分了可变集合和不可变集合. 例如,有两个版本的 Iterable interface:

  • Iterable
  • MutableIterable

The same goes for Collection, List, Set and Map interfaces.

For example, this any operation returns true 如果至少有一个元素与给定谓词匹配:

val list = listOf(1,2,3,4,5,6)
assertTrue(list.any { it % 2 == 0 })

有关可以在集合上执行的函数操作的广泛列表,请查看此列表 blog post.

Conclusion

我们只是触及了Kotlin提供的服务的表面. 对于那些有兴趣进一步阅读和学习更多内容的人,请查看:

总而言之,Kotlin为您提供了在编写本机代码时节省时间的能力 Android 应用程序通过使用直观和简洁的语法. 它仍然是一种年轻的编程语言, but in my opinion, 它现在足够稳定,可以用于构建生产应用程序.

The benefits of using Kotlin:

  • Android Studio的支持是无缝的和优秀的.
  • 将现有的Java项目转换为Kotlin很容易.
  • Java和Kotlin代码可以共存于同一个项目中.
  • 在应用程序中没有速度开销.

The downsides:

  • Kotlin将把它的库添加到生成的 .apk, so the final .apk size will be about 300KB larger.
  • 如果滥用,操作符重载可能导致代码不可读.
  • IDE和自动完成在使用Kotlin时比使用纯Java Android项目要慢一些.
  • 编译时间可能会稍长一些.
就这一主题咨询作者或专家.
Schedule a call
Ivan Kušt's profile image
Ivan Kušt

Located in Zagreb, Croatia

Member since April 14, 2016

About the author

Ivan是一个移动爱好者,他完善了移动应用程序的开发过程和架构.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

Infinum

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.