本文概述
- 介绍
- Kotlin设定
- 与Kotlin节省时间
- 功能1:静态布局导入
- 功能2:使用Kotlin编写POJO类
- 功能3:类继承和构造函数
- 功能4:Lambda表达式
- 功能5:零安全
- 功能6:with()函数
- 功能7:操作员超载
- 功能8:委派的属性
- 功能9:将对象映射到地图
- 功能10:集合和功能操作
- 总结
介绍
前一段时间, Tomasz引入了Android上的Kotlin开发。提醒你:Kotlin是由Jetbrains开发的一种新编程语言, 该公司是最受欢迎的Java IDE之一IntelliJ IDEA的背后公司。像Java一样, Kotlin是一种通用语言。由于它符合Java虚拟机(JVM)字节码, 因此可以与Java并行使用, 并且不会增加性能。
在本文中, 我将介绍促进Android开发的十大有用功能。
注意:在撰写本文时, 实际版本为Android Studio 2.1.1。和Kotlin 1.0.2。
厌倦了永无止境的Java代码?尝试Kotlin, 节省时间和理智。
鸣叫
Kotlin设定
由于Kotlin由JetBrains开发, 因此在Android Studio和IntelliJ中都得到了很好的支持。
第一步是安装Kotlin插件。成功执行此操作后, 将可以使用新操作将Java转换为Kotlin。新增了两个选项:
- 创建一个新的Android项目并在项目中设置Kotlin。
- 将Kotlin支持添加到现有的Android项目中。
要了解如何创建新的Android项目, 请查看官方逐步指南。要将Kotlin支持添加到新创建或现有的项目中, 请在Mac上使用Command + Shift + A或在Windows / Linux上使用Ctrl + Shift + A打开”查找操作”对话框, 然后调用”在项目中配置Kotlin”操作。
要创建一个新的Kotlin类, 请选择:
- 文件>新建> Kotlin文件/类, 或者
- 文件>新建> Kotlin活动
另外, 你可以创建一个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:android="http://schemas.android.com/apk/res/android"
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:id="@+id/helloWorldTextView"
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作为名称等于视图ID的TextView实例导入。请勿混淆用于设置标签的语法:
helloWorldTextView.text = "Hello World!"
我们将尽快介绍。
注意事项:
- 确保导入正确的布局, 否则导入的View引用将具有空值。
- 使用片段时, 请确保在onCreateView()函数调用之后使用导入的View引用。在onCreateView()函数中导入布局, 并使用View引用在onViewCreated()中设置UI。在onCreateView()方法完成之前, 不会分配引用。
功能2:使用Kotlin编写POJO类
使用Kotlin可以节省最多时间的是编写用于保存数据的POJO(普通Java对象)类。例如, 在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有所不同。首先, 声明变量名, 然后输入type。另外, 默认情况下, 属性为非null类型, 这意味着它们不能接受null值。若要将变量定义为接受空值, 必须在类型之后添加问号。稍后我们将在Kotlin中讨论此问题和null安全。
需要注意的另一件事是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) {
}
主构造函数位于类定义中的类名称之后。如果主要构造函数没有任何注释或可见性修饰符, 则可以省略builder关键字:
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)
}
}
请注意, 每个辅助构造函数都必须委托给一个主要构造函数。这类似于Java, 它使用以下关键字:
class User(val firstName: String, val lastName: String) {
constructor(firstName: String) : this(firstName, "") {
//...
}
}
实例化类时, 请注意, Kotlin和Java一样都没有新的关键字。要实例化上述User类, 请使用:
val user = User("John", "Doe)
继承介绍
在Kotlin中, 所有类都从Any扩展, 这类似于Java中的Object。默认情况下, 类是关闭的, 就像Java中的最终类一样。因此, 为了扩展一个类, 必须将其声明为开放或抽象的:
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()函数的调用, 用于显示Kotlin提供的Toast。
另外, 如果不使用参数, 我们可以将其省略:
view.setOnClickListener({ toast("Click") })
Kotlin已经优化了Java库, 并且任何使用一种参数方法接收接口的函数都可以使用函数参数(而不是Interface)来调用。
此外, 如果函数是最后一个参数, 则可以将其移出括号:
view.setOnClickListener() { toast("Click") }
最后, 如果函数只有一个参数是函数, 则可以省略括号:
view.setOnClickListener { toast("Click") }
有关更多信息, 请查看Antonio Leiva撰写的Kotlin for Android开发人员书籍和官方文档。
扩展功能
与C#类似, Kotlin通过使用扩展功能提供了使用新功能扩展现有类的功能。例如, 一个扩展方法将计算String的MD5哈希值:
fun String.md5(): ByteArray {
val digester = MessageDigest.getInstance("MD5")
digester.update(this.toByteArray(Charset.defaultCharset()))
return digester.digest()
}
请注意, 函数名称之前是扩展类的名称(在本例中为String), 并且可以通过此关键字使用扩展类的实例。
扩展功能与Java实用程序功能等效。 Java中的示例函数如下所示:
public static int toNumber(String instance) {
return Integer.valueOf(instance);
}
该示例函数必须放在Utility类中。这意味着扩展功能不会修改原始扩展类, 而是一种方便的编写实用程序方法的方式。
功能5:零安全
在Java中最忙碌的事情之一可能就是NullPointerException。 Null-safety是已经集成到Kotlin语言中的功能, 它是如此隐式, 你通常不必担心。官方文档指出, NullPointerExceptions的唯一可能原因是:
- 显式调用以引发NullPointerException。
- 使用 !!运算符(我将在后面解释)。
- 外部Java代码。
- 如果在初始化之前在构造函数中访问了Lateinit属性, 则将引发UninitializedPropertyAccessException。
默认情况下, 如果未明确将Kotlin中的所有变量和属性声明为可为空, 则它们将被视为非空(无法保存空值)。如前所述, 要定义一个变量以接受空值, 必须在类型之后添加问号。例如:
val number: Int? = null
但是, 请注意, 以下代码将无法编译:
val number: Int? = null
number.toString()
这是因为编译器执行空检查。要进行编译, 必须添加一个空检查:
val number: Int? = null
if(number != null) {
number.toString();
}
此代码将成功编译。在这种情况下, Kotlin在后台执行的操作是该数字在if块内变为nun-null(Int而不是Int?)。
可以使用安全调用运算符(?。)简化空检查:
val number: Int? = null
number?.toString()
仅当数字不为null时, 才会执行第二行。你甚至可以使用著名的猫王运算符(?:):
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一样。有一个”安全”强制转换运算符, 它返回空值而不是引发异常:
val x: String = y as? String
有关转换的更多详细信息, 请查看官方文档的”类型转换”和”转换”部分, 有关空安全性的更多详细信息, 请查看”空安全性”部分。
堇青石性能
在某些情况下, 使用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上的binary *运算符的实现。例如, 以下表达式会将值” TestTestTestTest”分配给newString变量:
val newString = "Test" * 4
由于可以使用扩展功能, 因此意味着可以更改所有对象的运算符的默认行为。这是一把双刃剑, 应谨慎使用。有关可以重载的所有运算符的功能名称的列表, 请查看官方文档。
与Java相比, 另一个很大的区别是==和!=运算符。运算符==转换为:
a?.equals(b) ?: b === null
而运算符!=转换为:
!(a?.equals(b) ?:
这意味着使用==不会像Java中那样进行身份检查(比较一个对象的实例是否相同), 但是其行为与equals()方法相同, 并带有null检查。
要执行身份检查, 必须在Kotlin中使用运算符===和!==。
功能8:委派的属性
某些属性具有一些共同的行为。例如:
- 首次访问时初始化的惰性初始化属性。
- 在Observer模式中实现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()用于在观察者模式下应表现为可观察对象的属性。它接受两个参数, 即初始值和具有三个参数(属性, 旧值和新值)的函数。
给定的lambda表达式将在每次调用setValue()方法时执行:
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中对lambda的支持, 可以将集合发挥到一个新的水平。
首先, 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和Autocomplete的行为要比使用纯Java Android项目时的慢一些。
- 编译时间可能会更长一些。