从Android开发的角度去认识Kotlin

在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。这其中涉及很多原因:

  1. 早期版本的Swift编译速度和运行速度慢,导致用户觉得应用卡;
  2. 用Swift开发打包后的安装包比用OC的大;
  3. 再比如Swift的版本更新太快,不太稳定,开发者不得不花时间去适配到最新的Swift。
  4. 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更加诱人的魅力,希望能够多多分享,小牧在此先谢过啦(真诚脸)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容