在2017的Google I/O大会上,Google宣布,这门诞生于俄罗斯的年轻语言,即日起成为最新的一级安卓编程语言,并在Android Studio 3.0 已加入对其的支持。Kotlin是JetBrains设计并开源(最新开源版本为1.1.4)的一门静态编程语言,由于设计者是IDE著名开发商JetBrains公司,Kotlin从一开始就自带IDE 支持。在Intellij IDEA 15和Android Studio3.0之前的版本需要安装Kotlin插件,之后的版本自带Kotlin插件。
Kotlin想给我们展现什么?
1. 互操作与互兼容
谈起Kotlin与Java,很多人估计会联想起Swift与OC,Swift是苹果于2014年WWDC(苹果开发者大会)发布的新开发语言,可与Objective-C共同运行于Mac OS和iOS平台。根据了解,现在国内一些大公司依然使用OC或者Swift与OC混编的方式开发iOS。这其中涉及很多原因:
- 早期版本的Swift编译速度和运行速度慢,导致用户觉得应用卡;
- 用Swift开发打包后的安装包比用OC的大;
- 再比如Swift的版本更新太快,不太稳定,开发者不得不花时间去适配到最新的Swift。
- Swift与OC并不能完全互操作,存在兼容性问题,除此以外虽然Swift调用OC比较简单,但OC里用Swift比较麻烦。(源自简书作者LingoGuo)
但是Kotlin,却与Java有着100%的互操作和互兼容性,并在编译速度和运行速度上,与Java相比并未有劣势可言:
- 兼容性(Compatibility)—— Kotlin能兼容JDK 6,确保Kotlin的应用程序在老版本的Android设备上运行
- 运行速度(Performance)—— Kotlin 应用程序的运行速度与 Java 差不多,但是随着Kotlin对内联函数的支持,使用 lambda 表达式的代码通常比用 Java 写的代码运行得更快
- 互操作性(Interoperability)—— 用Java写的类库和代码可以继续在Kotlin的代码中继续沿用,并支持Kotlin和Java两种语言的混合编程
- 占用空间(Footprint)—— Kotlin拥有一个紧密的runtime library,在ProGuard的作用下减小了更多内存的占用,在实际应用程序中,Kotlin 开发的apk比Java开发的apk增加不到 100K 的大小。
- 编译时间(Compilation Time)—— Kotlin 支持高效的增量编译,所以对于清理构建会有额外的开销,增量构建通常与 Java 一样快或者更快(增量编译只重新编译代码中更改的部分)
2. 易表现(简洁)
- 常量与变量
在Kotlin中,变量用var声明,常量用val声明,val声明的对象意味着它在实例化之后就不能再去改变它的状态了。如果你需要一个这个对象修改之后的版本,那就会再创建一个新的对象。这个让编程更加具有健壮性和预估性:
val s = "Example" // A String
val actionBar = supportActionBar // An ActionBar in an Activity context
var i = 23 // An Int
var m = 23.4 // An Double
而在Java中,声明一个对象不可变需要加final属性,间接性一目明了:
private final String s = "Example"
- 基本类型
在Kotlin中,基本类型自带转化方法:
val i:Int = 7
val d:Double = i.toDouble()
val c:Char = 'c'
val i:Int = c.toInt()
- 函数
Kotlin的函数可以给参数指定一个默认值使得它们变得可选,如下,第二个参数( length) 指定了一个默认值,意味着调用的时候可以传入第二个值或者不传,这样可以避免你需要的重载函数:
fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, length).show()
}
toast("Hello")
toast("Hello", Toast.LENGTH_LONG)
这个与下面的Java代码是一样的,明显Kotlin更加易于表现:
void toast(String message){
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
void toast(String message, int length){
Toast.makeText(this, message, length).show();
}
- 类
在Java中,如果我们要典型的数据类,我们需要去编写(至少生成) 这些代码:
public class Artist {
private long id;
private String name;
private String url;
private String mbid;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public String toString() {
return "Artist{" + "id=" + id + ", name='" + name + '\'' + ", url='" + url + '\'' + '}';
}
}
使用Kotlin,我们只需要通过数据类,这个数据类,它会自动生成所有属性和它们的访问器,以及一些有用的方法,比如toString():
data class Artist(
var id: Long,
var name: String,
var url: String,
var mbid: String)
如果我们使用不可修改的对象,假如我们需要修改这个对象状态,必须要创建一个新的一个或者多个属性被修改的实例。这个任务在java里是非常重复且不简洁的,然后Kotlin可以这样实现:
val f1 = Forecast(Date(), 27.5f, "Shiny day")
val f2 = f1.copy(temperature = 30f)
而且Kotlin还支持映射对象的每一个属性到一个变量中:
val f1 = Forecast(Date(), 27.5f, "Shiny day")
val (date, temperature, details) = f1
在Java中我们需要这样去实现:
Forecast f1 = new Forecast(Date(), 27.5f, "Shiny day");
Date date = f1.getDate();
float temperature = f1.getTemperature();
String details = f1.getDetails();
- 操作符重载
在Kotlin中,每个操作符都有对应的操作方法,如:
操作符 | 对应方法 |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
正如这两个表达式是一样的意思:
var num:Int = 1
num = num.plus(1) // 与num = num + 1效果一样
这也就提供了重载操作符(扩展操作符)的方法:
fun main(args: Array<String>){
var num1 = Number(1, 1)
var num2 = Number(1, 1)
println((num1 + num2).toString())
}
operator fun Number.plus(other: Number):Number{
this.one = this.one + other.one
this.two = this.two + other.two
return this
}
data class Number(
var one: Int,
var two: Int
)
输出结果为
Number(one=2, two=2)
- 强大的list
val list = listOf(1, 2, 3, 4, 5, 6)
list.any { it % 2 == 0 } // 如果至少有一个元素符合给出的判断条件,则返回true
list.all { it % 2 == 0 } // 如果全部的元素符合给出的判断条件,则返回true
list.count { it % 2 == 0 } // 返回符合给出判断条件的元素总数
list.forEach { println(it) } // 遍历所有元素,并执行给定的操作
list.forEachIndexed { index, value -> println("position $index contains a $value") }
// 与 forEach ,但是我们同时可以得到元素的index
list.max() // 返回最大的一项,如果没有则返回null
list.maxBy { -it } // 根据给定的函数返回最大的一项,如果没有则返回null
list.min() // 返回最小的一项,如果没有则返回null
list.minBy { -it } // 根据给定的函数返回最小的一项,如果没有则返回null
list.sumBy { it % 2 } // 返回所有每一项通过函数转换之后的数据的总和
list.drop(4) // 返回包含去掉前n个元素的所有元素的列表
list.filter { it % 2 == 0 } // 过滤所有符合给定函数条件的元素
list.contains(2) // 如果指定元素可以在集合中找到,则返回true
list.elementAt(1) // 返回给定index对应的元素
list.first { it % 2 == 0 } // 返回符合给定函数条件的第一个元素
list.indexOf(4) // 返回指定元素的第一个index,如果不存在,则返回 -1
list.indexOfFirst { it % 2 == 0 }
// 返回第一个符合给定函数条件的元素的index,如果没有符合则返回 -1
list.indexOfLast{ it % 2 == 0 } // 返回最后一个符合给定函数条件的元素的index,如果没有符合则返回 -1
list.reverse() // 返回一个与指定list相反顺序的list
list.sort() // 返回一个自然排序后的list
list..sortBy { it % 3 } // 返回一个根据指定函数排序后的list
......
- 流程控制
if表达式可实现赋值操作:
val z = if (condition) x else y
when表达式代替switch/case
when (x){
1 -> print("x == 1")
2 -> print("x == 2")
else -> {
print("I'm a block")
print("x is neither 1 nor 2")
}
}
val result = when (x) {
0, 1 -> "binary"
else -> "error"
}
when(view) {
is TextView -> view.setText("I'm a TextView")
is EditText -> toast("EditText value: ${view.getText()}")
is ViewGroup -> toast("Number of children: ${view.getChildCount()} ")
else -> view.visibility = View.GONE
}
val cost = when(x) {
in 1..10 -> "cheap"
in 10..100 -> "regular"
in 100..1000 -> "expensive"
in specialValues -> "special value!"
else -> "not rated"
}
val res = when{
x in 1..10 -> "cheap"
s.contains("hello") -> "it's a welcome!"
v is ViewGroup -> "child count: ${v.getChildCount()}"
else -> ""
}
Range 表达式使用一个 .. 操作符,它是被定义实现了一个 RangTo 方法。Ranges 帮助我们使用很多富有创造性的方式去简化我们的代码。比如我们可以把它:
if(i >= 0 && i <= 10) println(i)
转化成:
if (i in 0..10) println(i)
3. 空安全
我们有时候的确需要去定义一个变量包不包含一个值。在Java中尽管注解和IDE在这方面帮了我们很多,但是我们仍然可以这么做:
Forecast forecast = null;
forecast.toString();
这个代码可以被完美地编译( 你可能会从IDE上得到一个警告) ,然后正常地执行,但是显然它会抛一个NullPointerException 。这个相当不安全的。当然,在Kotlin中,也可以有一个可null的对象(用?标记):
val a: Int? = null
然而一个可null类型,你在没有进行检查之前你是不能直接使用它,这个代码不能被编译:
val a: Int? = null
a.toString() // 编译失败
a?.toString() // 编译成功
if(a!=null){ // 编译成功
a.toString()
}
4. 可扩展方法
Kotlin允许我们给任何类添加函数。它比那些我们项目中典型的工具类更加具有可读性。举个例子,我们可以给fragment增加一个显示toast的函数:
fun Fragment.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(getActivity(), message, duration).show()
}
我们现在可以这么做:
fragment.toast("Hello world!")
注:扩展函数并不是真正地修改了原来的类,它是以静态导入的方式来实现的。扩展函数可以被声明在任何文件中,因此有个通用的实践是把一系列有关的函数放在一个新建的文件里。
5. 函数式支持(lambda)
一个lambda表达式通过参数的形式被定义在箭头的左边( 被圆括号包围) ,然后在箭头的右边返回结果值。在这个例子中,我们接收一个View,然后返回一个Unit( 没有东西) 。所以根据这种思想,我们可以把前面的代码简化成这样:
view.setOnClickListener({ view -> toast("Click")})
这是非常棒的简化!当我们定义了一个方法,我们必须使用大括号包围,然后在箭头的左边指定参数,在箭头的右边返回函数执行的结果。如果左边的参数没有使用到,我们甚至可以省略左边的参数:
view.setOnClickListener({ toast("Click") })
如果这个函数的最后一个参数是一个函数,我们可以把这个函数移动到圆括号外面:
view.setOnClickListener() { toast("Click") }
并且,最后,如果这个函数只有一个参数,我们可以省略这个圆括号:
view.setOnClickListener { toast("Click") }
比原始的Java代码简短了5倍多,并且更加容易理解它所做的事情,非常让人影响深刻。
Kotlin Android Extensions有什么用?
Kotlin Android Extensions是Kotlin团队研发的可以让开发更简单的插件,Kotlin Android Extensions自动创建了属性让开发者直接访问XML中的view,而不需要在开始使用之前明确地从布局中去找到这些views。
注:属性的名字就是来自对应view的id,所以取id的时候要十分小心,因为它们将会是我们类中非常重要的一部分。这些属性的类型也是来自XML中的,所以不需要去进行额外的类型转换。
在使用的时候需要我们需要使用import 语句,以 kotlin.android.synthetic 开头,然后加上需要绑定到Activity的布局XML的名字:
import kotlinx.android.synthetic.activity_main.*
然后就可以直接使用View对象了:
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView.setText("Hello, world!")
// Instead of findViewById(R.id.textView) as TextView
}
}
那它背后是怎么工作的?其实正如Kotlin支持扩展方法一样,Kotlin也支持扩展属性,通过获取布局文件的控件id,以其为名添加相应控件作为该Activity的扩展属性,并与布局中的控件通过findViewById等方式获取控件实例(映射)。
该插件会代替任何属性调用函数,比如获取到view,并具有缓存功能,以免每次属性被调用都会去重新获取这个view。需要注意的是这个缓存装置只会在 Activity 或者 Fragment 中才有效。如果它是在一个扩展函数中增加的,这个缓存就会被跳过,因为它可以被用在 Activity 中但是插件不能被修改,所以不需要再去增加一个缓存功能。
Anko!强大?方便?
Anko是JetBrains开发的一个强大的库。它主要的目的是用来替代以前XML的方式来使用代码生成UI布局。如:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
linearLayout {
button("Login") {
textSize = dip(16)
onclick{
clickButton()
}
}.lparams(width = wrapContent) {
horizontalMargin = dip(5)
topMargin = dip(10)
}
}
}
以上代码相当于:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Login"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="10dp"/>
</LinearLayout>
这种通过代码生成UI布局的方式虽然方便,但是,这种由代码生成布局的方式,不利于控制器(Controller)与视图(View)的分离,处理不当可能会造成Activity代码臃肿;而且这种方式不支持视图的预览,必须运行之后才能看清楚效果。从个人的UI绘制经验出发,使用XML会更容易一些。
但是,Anko还是有其优势的,最重要的一点就是上方提及的扩展函数(方法),扩展函数数是指在一个类上增加一种新的行为,甚至我们没有这个类代码的访问权限。另外,Anko也支持扩展属性:
public var TextView.text: CharSequence
get() = getText()
set(v) = setText(v)
所以,当你在Fragment中看见activity的引用时不要惊讶,那其实就是使用getActivity()的扩展属性,再如当给ListView设置适配器时:
listview.adapter = mAdapter; // .adapter等同于set/getAdapter
还有,当一个Listener有多个方法时,Anko就显得很方便了, 看下面的代码(没有使用Anko):
seekBar.setOnSeekBarChangeListener(object: OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
// Something
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
// Just an empty method
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
// Another empty method
}
})
使用了Anko之后:
seekBar {
onSeekBarChangeListener {
onProgressChanged { seekBar, progress, fromUser ->
// Something
}
}
}
我相信,当越来越多的Android开发者认识到Kotlin的魅力后,Kotlin会真正成为Android开发的主流语言,以上的分享也只是简单的认识,如果读者有发现Kotlin更加诱人的魅力,希望能够多多分享,小牧在此先谢过啦(真诚脸)。