做者介紹:劉高軒,美團點評Android工程師,2年Android開發經驗,如今是美團點評點餐團隊的一員。java
本文主要介紹了新晉Android官方開發語言Kotlin的語法基礎和實用特性,並加以簡單的快速實踐,給出了Kotlin相比Java的開發效率優點,很是適合對Kotlin感興趣的Android開發人員進行閱讀。
本文較長(6000字左右),建議閱讀時間: 30min+android
今年的Google I/O 2017,Kotlin正式成爲Android的官方開發語言。這門被稱爲「The Swift of Android」的語言(具體對比可參考Swift is like Kotlin),自Jetbrains推出至今已有六年時間,而首個官方穩定版本倒是去年2月才發佈的。也正是從去年開始,Kotlin的使用度迎來了爆炸式的增加,這裏有一張官方的GIF圖能夠看出其2016年的發展速度。git
Kotlin好處都有啥?誰說對了都給他。做爲一個旨在取代Android開發人員基數衆多的Java語言的新興語言,徹底與Java老大哥割裂開,那往後的推廣確定是寸步難行。Kotlin巧妙地做爲一個靜態JVM語言誕生,與Java徹底兼容,又比Java要更加簡潔,同時也處理了讓無數開發者頭疼的NullPointerException問題,顯得頗有吸引力。對於一個有經驗的Android開發者來講,學習Kotlin的曲線至關平緩,能夠幾乎零成本去改造已有Android項目的部分代碼。面對這充滿了新特性的語言,誰會不動心呢?github
Kotlin語言主要有如下幾個特色:編程
java中存在int,float,boolean等基礎類型,這些基礎類型在Kotlin裏將所有以對象的形式繼續存在。隨之而來使用上也發生一些變化,有幾點須要注意下:swift
// Int沒法自動轉換爲Double, 須要本身先作類型轉換(as Double, toDouble(), 方式不少) var a: Int = 2 var b: Double = a.toDouble() // Char不能直接等值於其對應的ASCII碼值,須要類型轉換 var c: Char = 'a' var ca: Int = c.toInt()複製代碼
Kotlin中使用var定義變量(和js很像),使用val定義常量(至關於java當中的final)。定義變量時既能夠指定類型,也能夠不指定,Kotlin支持類型推斷。設計模式
var name = "me" // 類型爲String var name: String = "me" // 類型爲String val TAG: String = "KotClassA"複製代碼
須要注意的是,你不能直接寫一個var num丟在代碼裏不初始化值,編譯器會沒法推斷它的類型android-studio
var name // 編譯器報錯 var name: String // 類型爲String複製代碼
與Java中以「;」符號區分每行的結尾不一樣,Kotlin中再也不須要在每行代碼的結束位置寫「;」。固然若是你一時改不了寫Java時的習慣,IDE也並不會報錯,只會友好地把「;」置灰並下劃線,提示你這是多餘之舉。安全
Java語言中,「:」符號主要出現於三元運算符「A ? B : C」中,for循環時遍歷列表項和switch的每種情形分支。而在Kotlin語言中,「:」被普遍用於變量類型的定義。markdown
// 定義變量類型 fun common() { var ka: KClassA var kb: KClassB } // 定義函數的參數和返回值 fun helloStr(str: String): String { var strHello: String = "hello" strHello += str return strHello }複製代碼
「:」還被用於聲明類繼承或接口實現。
interface KInterfaceA { ... } open class KClassA(n: Int): KInterfaceA { ... } class KClassB(n: Int): KClassA(n) { ... }複製代碼
此外,若是你想在Kotlin代碼中使用Java類,也須要用到「:」符號。連續兩個「:」代表對Java類的調用。
val intent = Intent(this, MainActivity::class.java)複製代碼
Java語言中使用instanceof來判斷某變量是否爲某類型,而Kotlin中使用更短小更直觀的is來進行類型檢測。
if (num instanceof Double) { ... } // Java代碼 if (num is Double) { ... } // Kotlin代碼複製代碼
Java中使用字符串模板會比較麻煩,並且不太直觀。而Kotlin裏使用則異常便捷好用。
// Java中字符串模板兩種經常使用形式 String name = "我"; int age = 25; String.format("%s今年%d歲", name, age); MessageFormat.format("{0}今年{1}歲", name, age); // Kotlin中更直觀的字符串模板形式 var name = "我" var age = 25 "${name}今年${age}歲"複製代碼
Kotlin中的函數經過關鍵字fun定義的,具體的參數和返回值定義結構以下。
fun test(para1: Int, para2: String): String { ... }複製代碼
Kotlin中的函數能夠是全局函數,成員函數或者局部函數,甚至還能夠做爲某個對象的擴展函數臨時添加,這個做爲Kotlin的一大實用特性,下文會有具體講解。
Kotlin函數參數還有默認值和可變參數的特性,分別來看一下:
對Kotlin函數中的某個參數能夠用「=」號指定其默認值,調用函數方法時可不不傳這個參數,但其餘參數須要用「=」號指定。下文例子中沒有傳遞參數para2,其實際值爲默認值"para2"
fun test(para1: Int, para2: String = "para2", para3: String): String { ... } test(22, para3 = "hello")複製代碼
可變參數值的話,須要用關鍵字vararg來定義。這裏須要注意的是,一個函數僅能有一個可變參數。該可變參數不必定得是最後一個參數,但當這種狀況下時,調用該方法,須要給其餘未指明具體值的參數傳值。
fun test(vararg para1: String, para2: String): String { ... } test("para1", "para4", "para5", para2 = "hello")複製代碼
Kotlin中也使用class關鍵字定義類,全部類都繼承於Any類,相似於Java中Object類的概念。類實例化的形式也與Java同樣,可是去掉了new關鍵字。
類的構造函數分爲primary constructor和secondary constructor,前者只能有一個,然後者能夠有多個。若是二者都未指定,則默認爲無參數的primary constructor。
primary constructor是屬於類頭的一部分,用constructor關鍵字定義,無可見性字段定義時可省略。初始化代碼須要單獨寫在init代碼塊中,方法參數只能在init代碼塊和變量初始化時使用。
secondary constructor也是用constructor關鍵字定義,必需要直接或間接代理primary constructor。
class Student(name: String) { // primary constructor var mName: String = name init { println("Student is called " + name) } constructor(age: Int, name: String):this(name) { println("Student is ${age} years old") } }複製代碼
類繼承使用符號「:」表示,接口實現也同樣,不作本來Java中的extends和implement關鍵字區分。Kotlin有一點與Java大爲不一樣,即Java中類默承認被繼承,只有被final關鍵字修飾的類纔不能被繼承。而Kotlin中直接取消了final關鍵字,全部類均默認不可被繼承。神馬?這還怎麼面向對象編程?先別急,Kotlin中新增了open關鍵字,僅有被open修飾的類才能夠被繼承。
平常開發中寫一個單例類是很常見的行爲,Kotlin中直接將這種設計模式提高到語言級別,使用關鍵詞object定義單例類。這裏須要注意,是全小寫。Kotlin中區分大小寫,Java中本來指全部類的父類Object已棄用。單例類訪問直接使用類名,無構造函數。
object Shop(name: String) { fun buySomething() { println("Bought it") } } Shop.buysomething()複製代碼
Java中使用static標識一個類裏的靜態屬性或方法,能夠被這個類的因此實現使用。Kotlin改成使用伴隨對象,用companion修飾單例類object,來實現靜態屬性或方法功能。
class Mall(name: String) { companion object Shop { val SHOP_NAME: String = "McDonald" // 等同於Java中寫public static String fun buySomething() { // 等同於Java中寫public static void println("Bought it") } } } Mall.buySomething()複製代碼
Kotlin中的if-else語句與Java一致,結構上都是if (條件A) { 條件A爲真要執行的操做 } else { 條件A爲假要執行的操做 }
這裏主要要介紹的是Kotlin的一個特色,即if-else語句能夠做爲一個邏輯表達式使用。不只如此,邏輯表達式還能夠以代碼塊的形式出現,代碼塊最後的表達式做爲該塊的值。
// 邏輯表達式的使用 fun maxNum(x: Int, y: Int): Int { var max = if (x > y) x else y return max } // 代碼塊形式的邏輯表達式 fun maxNumPlus(x: Int, y: Int): Int { var max = if (x > y) { println("Max number is x") x } else { println("Max number is y") y } return max }複製代碼
Kotlin中的when語句取代了Java中的switch-case語句,功能上要強大許多,能夠有多種形式的條件表達。與if-else同樣,Kotlin中的when也能夠做爲邏輯表達式使用。
// 邏輯表達式的使用 fun judge(obj: Any) { when (obj) { 1 -> println("是數字1") -1, 0 -> println("是數字0或-1") in 1..10 -> println("是不大於10的正整數") "abc" -> println("是字符串abc") is Double -> println("類型是雙精度浮點數") else -> println("其餘操做") } }複製代碼
Kotlin中能夠對任意表達式進行標籤標記,形式爲abc@,xyz@。而這些標籤,能夠搭配return、break、continue等跳轉行爲來使用。
fun labelTest() { la@ for (i in 1..10) { println("outer index " + i) for (j in 1..10) { println("inner index " + j ) if ( inner % 2 == 0) { break@la } } } }複製代碼
for語句、while語句、continue語句和break語句等邏輯都與Java基本一致,這裏再也不贅述。
在寫Java代碼時,最常出如今線上的crash問題大概就是NullPointerException了。技術上來講這樣的問題修復起來很快,沒什麼難度,但每每因爲平常開發中沒有寫足夠多的防護性代碼,致使此類問題一直困擾着咱們。Java做爲一個古老的語言並無空指針安全功能,這使得當對象層級套用的時候,想要獲取最裏面的某個屬性,須要從外到內依次作一遍非空判斷來避免NPE。類似的場景太多,致使這在代碼成本上是很大的。
Kotlin中,當咱們定義一個變量時,其默認就是非空類型。若是你直接嘗試給他賦值爲null,編譯器會直接報錯。Kotlin中將符號「?」定義爲安全調用操做符。變量類型後面跟?號定義,代表這是一個可空類型。一樣的,在調用子屬性和方法時,也能夠用字符?進行安全調用。Kotlin的編譯器會在寫代碼時就檢查非空狀況,所以下文例子中,當s2有前置條件判斷爲非空後,即使其自己是可空類型,也能夠安全調用子屬性或方法。對於ifelse結構的邏輯,Kotlin還提供了「?:」操做符,極大了簡化了代碼量又不失可讀性。Kotlin還提供「!!」雙感嘆號操做符來強制調用對象的屬性和方法,無視其是否非空。這是一個挺危險的操做符,除非有特殊需求,不然爲了遠離NPE,仍是少用爲妙。
var s1: String = "abc" s1 = null // 這裏編譯器會報錯 var s2: String? = "abc" s2 = null // 編譯器不會報錯 var l1 = s1.length // 可正常編譯 var l2 = s2.length // 沒有作非空判斷,編譯器檢查報錯 if (s2 != null) s2.length // Java式的判空方案 s2?.length // Kotlin的安全調用操做符?。當s2爲null時,s2?.length也爲null if (s2 != null) s2.length else -1 // Java式判空的ifelse邏輯 s2?.length ?: -1 // Kotlin的elvis操做符 s2!!.length // 可能會致使NPE複製代碼
相信做爲一個Java/Android開發者,你們都寫過不少Base類,繼承原生父類的同時,封裝一些通用方法,供子類使用。亦或是把這類通用方法,專門放置到一個XXUtils類裏,做爲工具類出現。這樣作是爲了代碼結構的清晰,但也是一種無奈。因爲沒法修改原生類的內容,咱們只能藉助繼承或者以面向方法的思惟來寫工具類。這點在Kotlin裏獲得了完美解決。Kotlin支持在包範圍內對已存在的類進行方法和屬性擴展。
咱們以Android最經常使用的showToast方法舉個擴展方法的例子,以lastIndex屬性舉個擴展屬性的例子,以下。在這樣強大的特性下,寫代碼都成了一種享受。
// 擴展方法 fun Context.showLongToast(msg: String) { Toast.makeText(this, msg, Toast.LENGTH_LONG).show() } // 擴展屬性 val <T> ArrayList<T>.lastIndex: Int get() = size -1 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) showLongToast("hello") // 給Context類擴展了showToast方法,能夠在任何有context的地方直接調用了 var mList = ArrayList<String>() mList.lastIndex // 任何ArrayList都可以調用該屬性 ... } ... }複製代碼
這裏須要注意兩點:1.擴展須要在包級範圍內進行,若是寫在class內是無效的。2.已經存在的方法或屬性,是沒法被擴展的,依舊會調用已有的方法。
相信你們都寫過數據類,或者自動化生成的數據model,一般都是由多個屬性和對應的getter、setter組成。當有大量多屬性的數據類時,不只這些類會由於大量的getter和setter方法而行數爆炸,也使整個工程方法數驟增。Kotlin中也作了這層特性優化,提供了數據類的簡單實現。不用再寫get和set方法,這些都由編譯器背後去作,你獲得的是一個清爽乾淨的數據類。具體使用參考下面的例子。
data class Student ( var name: String, var age: Int, var hobby: String?, var university: String = "NJU" ) fun printInfo() { var s: Student = Student("Ricky", 25, "playing Gwent") println("${s.name} is from ${s.university}, ${s.age} years old, and likes ${s.hobby}") }複製代碼
Anko是Jetbrains官方提供的一個讓Kotlin開發更快速簡單的類庫,旨在使代碼書寫更加清晰易懂,形式上爲DSL編程。
Android開發過程必定都寫過大量的findViewById。這自己就是個消耗資源的方法,編碼時還極爲不便,須要強制轉換爲具體的View類型才能調用其方法。經過引入支持註解的庫,可使這個過程略微簡單化一些。
// 傳統Android中的View內容初始化 TextView tvName = (TextView) this.findViewById(R.id.tv_name); tvName.setText("Ricky"); // 註解方式 @BindView(R.id.tv_name) TextView tvName; tvName.setText("Ricky");複製代碼
這樣仍是顯得不夠簡潔,而Kotlin給出了一種最爲簡便的方式。
import kotlinx.android.synthetic.main.activity_main.* // activity_main爲具體的佈局xml文件名 ... tvName.text = "Ricky";複製代碼
你只須要在具體的頁面中按照上面的格式import下,就能夠在整個頁面裏很方便的使用xml裏的view操做了。不須要類型轉換,不須要新建變量,不須要findViewById,孰優孰劣,相信你們心中都有了答案。
一般,在Android裏,當我想打開一個新頁面,並給它傳遞一些參數時,我須要按照以下的方式編碼。
Intent intent = new Intent(LoginActivity.this, MainActivity.class); intent.putExtra("userid", 10001); intent.putExtra("username", "ricky"); startActivity(intent);複製代碼
而強大的Anko給咱們提供了一種極爲方便的寫法。不管是無參仍是有參,仍是須要RequestCode,都很是簡潔易懂,是否是一看就特別心動?
startActivity<MainActivity>() startActivity<MainActivity>("userid" to 10001, "username" to "ricky") startActivityForResult<MainActivity>(101, "userid" to 10001, "username" to "ricky")複製代碼
上面說到再也不使用findViewById,仍是基於使用xml來編寫Android頁面這個基礎。假若你想完全革新寫法,換一種更直觀的DSL方式來寫Android頁面呢?Anko就提供了這樣的方案。相比之下,除了可讀性增長以外,也節約瞭解析xml文件消耗的資源。
inner class LoginAnkoUI : AnkoComponent<LoginActivity> { override fun createView(loginAnkoUI: AnkoContext<LoginActivity>): View { return with(loginAnkoUI) { verticalLayout { val textView = textView("用戶名") { textSize = sp(15).toFloat() textColor = context.resources.getColor(R.color.black) }.lparams { margin = dip(10) height = dip(40) width username= matchParent } val username = editText("輸入...") button("登陸") { onClick { view -> toast("Hello, ${username.text}!") } } } } } }複製代碼
只要最後在Activity里加一句調用,即可以使用Anko寫的頁面了。是否是看上去更直觀了呢?
LoginAnkoView().setContentView(this@LoginActivity)複製代碼
做爲官方開發語言,在Android Studio 3.0版本中,已經內嵌了對Kotlin的支持。和筆者同樣還在使用2.x版本開發的小夥伴們,也不用擔憂須要花時間升級AS才能體驗到Kotlin。JetBrains提供了完善的插件支持,直接打開Preferences去配置插件,找到Kotlin下載安裝下就好啦。
插件安裝完畢,還須要build.gradle裏添加下依賴。
若是你不清楚當前最新的kotlin版本是什麼的話,這裏也有一個更簡便的方法來給項目添加kotlin依賴。
首先你的項目中要有一份java代碼,而後在插件安裝正常的狀況下,你能夠在Code菜單看到一鍵把Java代碼轉換爲Kotlin的功能。
轉換完畢後,當你嘗試修改這個.kt代碼文件時,Android Studio便會提醒你Kotlin還沒有配置。
這個自動化配置過程,能夠看到目前插件支持到的最新的Kotlin版本。換言之插件及時最新的話,你這裏就能夠選擇到最新的Kotlin版本。這裏也能夠選擇只將某個模塊配置爲支持Kotlin,或者全局支持。
配置完畢後,咱們能夠看到build.gradle文件裏已經自動添加了依賴代碼,Sync一下就ok了。
因爲要充分發揮Kotlin的特性,將Anko的相關代碼依賴也引入進來
下面會給你們展現一個小Demo的核心代碼,完整代碼能夠查看KotlinDemo。Demo將分別用Java寫法和Kotlin寫法編寫一樣的頁面,以此對比出優劣。
Demo包含一個Java實現的公共首頁,首頁中有兩個按鈕入口,分別跳轉到用Java編寫的列表頁和用Kotlin編寫的列表頁。各自的列表頁都會實現一個Item點擊跳轉到詳情頁的功能,並在詳情頁顯示具體信息。
Java實現效果:Kotlin實現效果:
// Java實現 public class JListActivity extends AppCompatActivity implements View.OnClickListener, AdapterView.OnItemClickListener { // View private ListView listView; private Button btnClearData; private Button btnUpdateData; // Data private ArrayList<JStudent> studentList = new ArrayList<>(); private JListAdapter listAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list_java); initView(); initData(); } private void initData() { listAdapter = new JListAdapter(studentList); listView.setAdapter(listAdapter); } private void initView() { listView = (ListView) findViewById(R.id.java_list_view); btnClearData = (Button) findViewById(R.id.java_clear_btn); btnUpdateData = (Button) findViewById(R.id.java_update_btn); listView.setDividerHeight(0); listView.setOnItemClickListener(this); btnClearData.setOnClickListener(this); btnUpdateData.setOnClickListener(this); } @Override public void onClick(View view) { if (view == btnClearData) { studentList.clear(); listAdapter.notifyDataSetChanged(); showShortToast("已清空"); } else if (view == btnUpdateData) { studentList.addAll(FakeServer.randomJavaResponse()); listAdapter.notifyDataSetChanged(); showShortToast("已更新10條數據"); } } @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { Intent intent = new Intent(JListActivity.this, JInfoActivity.class); intent.putExtra("stu_name", studentList.get(i).getName()); intent.putExtra("stu_age", studentList.get(i).getAge()); intent.putExtra("stu_hobby", studentList.get(i).getHobby()); intent.putExtra("stu_university", studentList.get(i).getUniversity()); startActivity(intent); } private void showShortToast(String msg) { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); } }複製代碼
// Kotlin實現 class KListActivity : AppCompatActivity() { var studentList: ArrayList<KStudent> = ArrayList() var listAdapter: KListAdapter = KListAdapter(studentList) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ListUI().setContentView(this@KListActivity) } fun clearList() { studentList.clear() listAdapter.notifyDataSetChanged() showShortToast("已清空") } fun updateList() { studentList.addAll(FakeServer.randomKotlinResponse()) listAdapter.notifyDataSetChanged() showShortToast("已更新10條數據") } fun gotoItemInfo(position: Int) { var student = studentList[position] startActivity<KInfoActivity>("stu_name" to student.name, "stu_age" to student.age, "stu_hobby" to student.hobby, "stu_university" to student.university) } }複製代碼
// Java實現 public class JStudent { private int id; private String name; private int age; private String hobby; private String university; public JStudent(int id, String name, int age, String hobby, String university) { setId(id); setName(name); setAge(age); setHobby(hobby); setUniversity(university); } public int getId() { return id; } public String getName() { return name; } public int getAge() { return age; } public String getHobby() { return hobby; } public String getUniversity() { return university; } public void setId(int id) { this.id = id; } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } public void setHobby(String hobby) { this.hobby = hobby; } public void setUniversity(String university) { this.university = university; } }複製代碼
// Kotlin實現 data class KStudent(var id: Int, var name: String, var age: Int, var hobby: String, var university: String)複製代碼
上面的代碼對比已經能看出一些驚人的差距,咱們再統計一下用兩種方式編寫的具體代碼行數,以評判二者的開發效率。
Java Part/LOC | Kotlin Part/LOC | |
---|---|---|
ListActivity | 85 | 38 |
ListUI | 52 | 62 |
InfoActivity | 62 | 29 |
InfoUI | 114 | 120 |
ListAdapter | 71 | 33 |
ListItemUI | 48 | 67 |
Student Data Class | 64 | 1 |
Total | 496 | 350 |
能夠看到,使用了Anko編寫的Kotlin頁面UI,在代碼行數上甚至比xml編寫還要多一點。可是因爲再也不須要findViewById,對應Activity中減小了不少邏輯,代碼行也少了不少。同時,這種寫法也節約了CPU去解析xml生成頁面的資源和時間。
最終此Demo中使用Java和Kotlin編寫相同的頁面功能,Kotlin比Java少了30%的代碼開發量。再加上空指針安全等特性,表現能夠說是很是亮眼。
Kotlin做爲一個JVM上的新語言,充分兼容了老大哥Java的諸多功能,又構建了不少自身優秀特性,提供了大量便捷易懂、結構清晰的開發形式。筆者做爲一個Android開發,很是樂於在從此的開發中嘗試徹底化的Kotlin項目。本文限於篇幅,僅能展示出其諸多特色的一點皮毛,更多新特性還須要各位看官本身在實際使用中挖掘。
套用劉禹錫的一句詩來結束本文吧。「芳林新葉催陳葉,流水前波讓後波」,期待吸取了Java之長的Kotlin,能在將來有更好的表現。