從Android開(kāi)發(fā)的角度去認(rèn)識(shí)Kotlin

在2017的Google I/O大會(huì)上,Google宣布,這門(mén)誕生于俄羅斯的年輕語(yǔ)言,即日起成為最新的一級(jí)安卓編程語(yǔ)言,并在Android Studio 3.0 已加入對(duì)其的支持。Kotlin是JetBrains設(shè)計(jì)并開(kāi)源(最新開(kāi)源版本為1.1.4)的一門(mén)靜態(tài)編程語(yǔ)言,由于設(shè)計(jì)者是IDE著名開(kāi)發(fā)商JetBrains公司,Kotlin從一開(kāi)始就自帶IDE 支持。在Intellij IDEA 15和Android Studio3.0之前的版本需要安裝Kotlin插件,之后的版本自帶Kotlin插件。

Kotlin想給我們展現(xiàn)什么?

1. 互操作與互兼容

談起Kotlin與Java,很多人估計(jì)會(huì)聯(lián)想起Swift與OC,Swift是蘋(píng)果于2014年WWDC(蘋(píng)果開(kāi)發(fā)者大會(huì))發(fā)布的新開(kāi)發(fā)語(yǔ)言,可與Objective-C共同運(yùn)行于Mac OS和iOS平臺(tái)。根據(jù)了解,現(xiàn)在國(guó)內(nèi)一些大公司依然使用OC或者Swift與OC混編的方式開(kāi)發(fā)iOS。這其中涉及很多原因:

  1. 早期版本的Swift編譯速度和運(yùn)行速度慢,導(dǎo)致用戶(hù)覺(jué)得應(yīng)用卡;
  2. 用Swift開(kāi)發(fā)打包后的安裝包比用OC的大;
  3. 再比如Swift的版本更新太快,不太穩(wěn)定,開(kāi)發(fā)者不得不花時(shí)間去適配到最新的Swift。
  4. Swift與OC并不能完全互操作,存在兼容性問(wèn)題,除此以外雖然Swift調(diào)用OC比較簡(jiǎn)單,但OC里用Swift比較麻煩。(源自簡(jiǎn)書(shū)作者LingoGuo

但是Kotlin,卻與Java有著100%的互操作和互兼容性,并在編譯速度和運(yùn)行速度上,與Java相比并未有劣勢(shì)可言:

  • 兼容性(Compatibility)—— Kotlin能兼容JDK 6,確保Kotlin的應(yīng)用程序在老版本的Android設(shè)備上運(yùn)行
  • 運(yùn)行速度(Performance)—— Kotlin 應(yīng)用程序的運(yùn)行速度與 Java 差不多,但是隨著Kotlin對(duì)內(nèi)聯(lián)函數(shù)的支持,使用 lambda 表達(dá)式的代碼通常比用 Java 寫(xiě)的代碼運(yùn)行得更快
  • 互操作性(Interoperability)—— 用Java寫(xiě)的類(lèi)庫(kù)和代碼可以繼續(xù)在Kotlin的代碼中繼續(xù)沿用,并支持Kotlin和Java兩種語(yǔ)言的混合編程
  • 占用空間(Footprint)—— Kotlin擁有一個(gè)緊密的runtime library,在ProGuard的作用下減小了更多內(nèi)存的占用,在實(shí)際應(yīng)用程序中,Kotlin 開(kāi)發(fā)的apk比Java開(kāi)發(fā)的apk增加不到 100K 的大小。
  • 編譯時(shí)間(Compilation Time)—— Kotlin 支持高效的增量編譯,所以對(duì)于清理構(gòu)建會(huì)有額外的開(kāi)銷(xiāo),增量構(gòu)建通常與 Java 一樣快或者更快(增量編譯只重新編譯代碼中更改的部分)
2. 易表現(xiàn)(簡(jiǎn)潔)
  • 常量與變量

在Kotlin中,變量用var聲明,常量用val聲明,val聲明的對(duì)象意味著它在實(shí)例化之后就不能再去改變它的狀態(tài)了。如果你需要一個(gè)這個(gè)對(duì)象修改之后的版本,那就會(huì)再創(chuàng)建一個(gè)新的對(duì)象。這個(gè)讓編程更加具有健壯性和預(yù)估性:

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中,聲明一個(gè)對(duì)象不可變需要加final屬性,間接性一目明了:

private final String s = "Example"
  • 基本類(lèi)型

在Kotlin中,基本類(lèi)型自帶轉(zhuǎn)化方法:

val i:Int = 7
val d:Double = i.toDouble()
val c:Char = 'c'
val i:Int = c.toInt()
  • 函數(shù)

Kotlin的函數(shù)可以給參數(shù)指定一個(gè)默認(rèn)值使得它們變得可選,如下,第二個(gè)參數(shù)( length) 指定了一個(gè)默認(rèn)值,意味著調(diào)用的時(shí)候可以傳入第二個(gè)值或者不傳,這樣可以避免你需要的重載函數(shù):

fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, message, length).show()
}
toast("Hello")
toast("Hello", Toast.LENGTH_LONG)

這個(gè)與下面的Java代碼是一樣的,明顯Kotlin更加易于表現(xiàn):

void toast(String message){
    Toast.makeText(this, message, Toast.LENGTH_LONG).show();
} 

void toast(String message, int length){
    Toast.makeText(this, message, length).show();
}
  • 類(lèi)

在Java中,如果我們要典型的數(shù)據(jù)類(lèi),我們需要去編寫(xiě)(至少生成) 這些代碼:

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,我們只需要通過(guò)數(shù)據(jù)類(lèi),這個(gè)數(shù)據(jù)類(lèi),它會(huì)自動(dòng)生成所有屬性和它們的訪(fǎng)問(wèn)器,以及一些有用的方法,比如toString():

data class Artist(
    var id: Long,
    var name: String,
    var url: String,
    var mbid: String)

如果我們使用不可修改的對(duì)象,假如我們需要修改這個(gè)對(duì)象狀態(tài),必須要?jiǎng)?chuàng)建一個(gè)新的一個(gè)或者多個(gè)屬性被修改的實(shí)例。這個(gè)任務(wù)在java里是非常重復(fù)且不簡(jiǎn)潔的,然后Kotlin可以這樣實(shí)現(xiàn):

val f1 = Forecast(Date(), 27.5f, "Shiny day")
val f2 = f1.copy(temperature = 30f)

而且Kotlin還支持映射對(duì)象的每一個(gè)屬性到一個(gè)變量中:

val f1 = Forecast(Date(), 27.5f, "Shiny day")
val (date, temperature, details) = f1

在Java中我們需要這樣去實(shí)現(xiàn):

Forecast f1 = new Forecast(Date(), 27.5f, "Shiny day");
Date date = f1.getDate();
float temperature = f1.getTemperature();
String details = f1.getDetails();
  • 操作符重載

在Kotlin中,每個(gè)操作符都有對(duì)應(yīng)的操作方法,如:

操作符 對(duì)應(yīng)方法
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)

正如這兩個(gè)表達(dá)式是一樣的意思:

var num:Int = 1
num = num.plus(1)      // 與num = num + 1效果一樣

這也就提供了重載操作符(擴(kuò)展操作符)的方法:

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
)

輸出結(jié)果為

Number(one=2, two=2)

  • 強(qiáng)大的list
val list = listOf(1, 2, 3, 4, 5, 6)
list.any { it % 2 == 0 }         // 如果至少有一個(gè)元素符合給出的判斷條件,則返回true
list.all { it % 2 == 0 }         // 如果全部的元素符合給出的判斷條件,則返回true
list.count { it % 2 == 0 }       // 返回符合給出判斷條件的元素總數(shù)
list.forEach { println(it) }     // 遍歷所有元素,并執(zhí)行給定的操作
list.forEachIndexed { index, value -> println("position $index contains a $value") }
                                 // 與 forEach ,但是我們同時(shí)可以得到元素的index
list.max()                       // 返回最大的一項(xiàng),如果沒(méi)有則返回null
list.maxBy { -it }               // 根據(jù)給定的函數(shù)返回最大的一項(xiàng),如果沒(méi)有則返回null
list.min()                       // 返回最小的一項(xiàng),如果沒(méi)有則返回null
list.minBy { -it }               // 根據(jù)給定的函數(shù)返回最小的一項(xiàng),如果沒(méi)有則返回null
list.sumBy { it % 2 }            // 返回所有每一項(xiàng)通過(guò)函數(shù)轉(zhuǎn)換之后的數(shù)據(jù)的總和

list.drop(4)                     // 返回包含去掉前n個(gè)元素的所有元素的列表
list.filter { it % 2 == 0 }      // 過(guò)濾所有符合給定函數(shù)條件的元素

list.contains(2)                 // 如果指定元素可以在集合中找到,則返回true
list.elementAt(1)                // 返回給定index對(duì)應(yīng)的元素
list.first { it % 2 == 0 }       // 返回符合給定函數(shù)條件的第一個(gè)元素
list.indexOf(4)                  // 返回指定元素的第一個(gè)index,如果不存在,則返回 -1 
list.indexOfFirst { it % 2 == 0 }             
                                 // 返回第一個(gè)符合給定函數(shù)條件的元素的index,如果沒(méi)有符合則返回 -1 
list.indexOfLast{ it % 2 == 0 }  // 返回最后一個(gè)符合給定函數(shù)條件的元素的index,如果沒(méi)有符合則返回 -1

list.reverse()                   // 返回一個(gè)與指定list相反順序的list
list.sort()                      // 返回一個(gè)自然排序后的list
list..sortBy { it % 3 }          // 返回一個(gè)根據(jù)指定函數(shù)排序后的list

......
  • 流程控制

if表達(dá)式可實(shí)現(xiàn)賦值操作:

val z = if (condition) x else y

when表達(dá)式代替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 表達(dá)式使用一個(gè) .. 操作符,它是被定義實(shí)現(xiàn)了一個(gè) RangTo 方法。Ranges 幫助我們使用很多富有創(chuàng)造性的方式去簡(jiǎn)化我們的代碼。比如我們可以把它:

if(i >= 0 && i <= 10) println(i)

轉(zhuǎn)化成:

if (i in 0..10) println(i)
3. 空安全

我們有時(shí)候的確需要去定義一個(gè)變量包不包含一個(gè)值。在Java中盡管注解和IDE在這方面幫了我們很多,但是我們?nèi)匀豢梢赃@么做:

Forecast forecast = null;
forecast.toString();

這個(gè)代碼可以被完美地編譯( 你可能會(huì)從IDE上得到一個(gè)警告) ,然后正常地執(zhí)行,但是顯然它會(huì)拋一個(gè)NullPointerException 。這個(gè)相當(dāng)不安全的。當(dāng)然,在Kotlin中,也可以有一個(gè)可null的對(duì)象(用?標(biāo)記):

val a: Int? = null

然而一個(gè)可null類(lèi)型,你在沒(méi)有進(jìn)行檢查之前你是不能直接使用它,這個(gè)代碼不能被編譯:

val a: Int? = null
a.toString()        // 編譯失敗
a?.toString()       // 編譯成功
if(a!=null){        // 編譯成功
    a.toString()
}
4. 可擴(kuò)展方法

Kotlin允許我們給任何類(lèi)添加函數(shù)。它比那些我們項(xiàng)目中典型的工具類(lèi)更加具有可讀性。舉個(gè)例子,我們可以給fragment增加一個(gè)顯示toast的函數(shù):

fun Fragment.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(getActivity(), message, duration).show()
}

我們現(xiàn)在可以這么做:

fragment.toast("Hello world!")

注:擴(kuò)展函數(shù)并不是真正地修改了原來(lái)的類(lèi),它是以靜態(tài)導(dǎo)入的方式來(lái)實(shí)現(xiàn)的。擴(kuò)展函數(shù)可以被聲明在任何文件中,因此有個(gè)通用的實(shí)踐是把一系列有關(guān)的函數(shù)放在一個(gè)新建的文件里。

5. 函數(shù)式支持(lambda)

一個(gè)lambda表達(dá)式通過(guò)參數(shù)的形式被定義在箭頭的左邊( 被圓括號(hào)包圍) ,然后在箭頭的右邊返回結(jié)果值。在這個(gè)例子中,我們接收一個(gè)View,然后返回一個(gè)Unit( 沒(méi)有東西) 。所以根據(jù)這種思想,我們可以把前面的代碼簡(jiǎn)化成這樣:

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

這是非常棒的簡(jiǎn)化!當(dāng)我們定義了一個(gè)方法,我們必須使用大括號(hào)包圍,然后在箭頭的左邊指定參數(shù),在箭頭的右邊返回函數(shù)執(zhí)行的結(jié)果。如果左邊的參數(shù)沒(méi)有使用到,我們甚至可以省略左邊的參數(shù):

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

如果這個(gè)函數(shù)的最后一個(gè)參數(shù)是一個(gè)函數(shù),我們可以把這個(gè)函數(shù)移動(dòng)到圓括號(hào)外面:

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

并且,最后,如果這個(gè)函數(shù)只有一個(gè)參數(shù),我們可以省略這個(gè)圓括號(hào):

view.setOnClickListener { toast("Click") }

比原始的Java代碼簡(jiǎn)短了5倍多,并且更加容易理解它所做的事情,非常讓人影響深刻。

Kotlin Android Extensions有什么用?

Kotlin Android Extensions是Kotlin團(tuán)隊(duì)研發(fā)的可以讓開(kāi)發(fā)更簡(jiǎn)單的插件,Kotlin Android Extensions自動(dòng)創(chuàng)建了屬性讓開(kāi)發(fā)者直接訪(fǎng)問(wèn)XML中的view,而不需要在開(kāi)始使用之前明確地從布局中去找到這些views。

注:屬性的名字就是來(lái)自對(duì)應(yīng)view的id,所以取id的時(shí)候要十分小心,因?yàn)樗鼈儗?huì)是我們類(lèi)中非常重要的一部分。這些屬性的類(lèi)型也是來(lái)自XML中的,所以不需要去進(jìn)行額外的類(lèi)型轉(zhuǎn)換。

在使用的時(shí)候需要我們需要使用import 語(yǔ)句,以 kotlin.android.synthetic 開(kāi)頭,然后加上需要綁定到Activity的布局XML的名字:

import kotlinx.android.synthetic.activity_main.*

然后就可以直接使用View對(duì)象了:

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
    }
}

那它背后是怎么工作的?其實(shí)正如Kotlin支持?jǐn)U展方法一樣,Kotlin也支持擴(kuò)展屬性,通過(guò)獲取布局文件的控件id,以其為名添加相應(yīng)控件作為該Activity的擴(kuò)展屬性,并與布局中的控件通過(guò)findViewById等方式獲取控件實(shí)例(映射)。

該插件會(huì)代替任何屬性調(diào)用函數(shù),比如獲取到view,并具有緩存功能,以免每次屬性被調(diào)用都會(huì)去重新獲取這個(gè)view。需要注意的是這個(gè)緩存裝置只會(huì)在 Activity 或者 Fragment 中才有效。如果它是在一個(gè)擴(kuò)展函數(shù)中增加的,這個(gè)緩存就會(huì)被跳過(guò),因?yàn)樗梢员挥迷?Activity 中但是插件不能被修改,所以不需要再去增加一個(gè)緩存功能。

Anko!強(qiáng)大?方便?

Anko是JetBrains開(kāi)發(fā)的一個(gè)強(qiáng)大的庫(kù)。它主要的目的是用來(lái)替代以前XML的方式來(lái)使用代碼生成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)
        }
    }
}

以上代碼相當(dāng)于:

<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>

這種通過(guò)代碼生成UI布局的方式雖然方便,但是,這種由代碼生成布局的方式,不利于控制器(Controller)與視圖(View)的分離,處理不當(dāng)可能會(huì)造成Activity代碼臃腫;而且這種方式不支持視圖的預(yù)覽,必須運(yùn)行之后才能看清楚效果。從個(gè)人的UI繪制經(jīng)驗(yàn)出發(fā),使用XML會(huì)更容易一些。

但是,Anko還是有其優(yōu)勢(shì)的,最重要的一點(diǎn)就是上方提及的擴(kuò)展函數(shù)(方法),擴(kuò)展函數(shù)數(shù)是指在一個(gè)類(lèi)上增加一種新的行為,甚至我們沒(méi)有這個(gè)類(lèi)代碼的訪(fǎng)問(wèn)權(quán)限。另外,Anko也支持?jǐn)U展屬性:

public var TextView.text: CharSequence
    get() = getText()
    set(v) = setText(v)

所以,當(dāng)你在Fragment中看見(jiàn)activity的引用時(shí)不要驚訝,那其實(shí)就是使用getActivity()的擴(kuò)展屬性,再如當(dāng)給ListView設(shè)置適配器時(shí):

listview.adapter = mAdapter;          // .adapter等同于set/getAdapter

還有,當(dāng)一個(gè)Listener有多個(gè)方法時(shí),Anko就顯得很方便了, 看下面的代碼(沒(méi)有使用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
        }
    }
}

我相信,當(dāng)越來(lái)越多的Android開(kāi)發(fā)者認(rèn)識(shí)到Kotlin的魅力后,Kotlin會(huì)真正成為Android開(kāi)發(fā)的主流語(yǔ)言,以上的分享也只是簡(jiǎn)單的認(rèn)識(shí),如果讀者有發(fā)現(xiàn)Kotlin更加誘人的魅力,希望能夠多多分享,小牧在此先謝過(guò)啦(真誠(chéng)臉)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容