用Kotlin開發Android項目是一種什么樣的感受?
前言
從初學 Kotlin,到嘗試性的寫一點體驗代碼,再到實驗性的做一些封裝工作,到***摸爬滾打著寫了一個項目。不得不說過程中還是遇上了不少的問題,盡管有不少坑是源于我自己的選擇,比如使用了 anko 布局放棄了 xml,但是總體來說,這門語言帶給我的驚喜是完全足以讓我忽略路上的坎坷。
這篇文章僅僅是想整理一下這一路走過來的一些感想和驚喜,隨著我對 Kotlin 的學習和使用,會長期修改。
正文
1.有了空安全,再也不怕服務端返回空對象了
簡單一點的例子,那就是 String 和 String?是兩種不同的類型。String 已經確定是不會為空,一定有值;而 String?則是未知的,也許有值,也許是空。在使用對象的屬性和方法的時候,String 類型的對象可以毫無顧忌的直接使用,而 String?類型需要你先做非空判斷。
- fun demo() {
- val string1: String = "string1"
- val string2: String? = null
- val string3: String? = "string3"
- println(string1.length)
- println(string2?.length)
- println(string3?.length)
- }
輸出結果為:
- 7
- null
- 7
盡管 string2 是一個空對象,也并沒有因為我調用了它的屬性/方法就報空指針。而你所需要做的,僅僅是加一個"?"。
如果說這樣還體現不出空安全的好處,那么看下面的例子:
- val a: A? = A()
- println(a?.b?.c)
試想一下當每一級的屬性皆有可能為空的時候,JAVA 中我們需要怎么處理?
2.轉型與智能轉換,省力又省心
我寫過這樣子的 JAVA 代碼
- if(view instanceof TextView) {
- TextView textView = (TextView) view;
- textView.setText("text");
- }
而在 Kotlin 中的寫法則有所不同
- if(view is TextView) {
- TextView textView = view as TextView
- textView.setText("text")
- }
縮減代碼之后對比更加明顯
- JAVA
- if(view instanceof TextView) {
- ((TextView) view).setText("text");
- }
- Kotlin
- if(view is TextView) {
- (view as TextView).setText("text")
- }
相比于 JAVA 在對象前加 (Class) 這樣子的寫法,Kotlin 是在對象之后添加 as Class 來實現轉型。至少我個人而言,在習慣了 as Class 順暢的寫法之后,是再難以忍受 JAVA 中前置的寫法,哪怕有 cast 快捷鍵的存在,仍然很容易打斷我寫代碼的順序和思路
事實上,Kotlin 此處可以更簡單:
- if(view is TextView) {
- view.setText("text")
- }
因為當前上下文已經判明 view 就是 TextView,所以在當前代碼塊中 view 不再是 View 類,而是 TextView 類。這就是 Kotlin 的智能轉換。
接著上面的空安全來舉個例子,常規思路下,既然 String 和 String? 是不同的類型,是不是我有可能會寫出這樣的代碼?
- val a: A? = A()
- if (a != null) {
- println(a?.b)
- }
這樣子寫,Kotlin 反而會給你顯示一個高亮的警告,說這是一個不必要的 safe call。至于為什么,因為你前面已經寫了 a != null 了啊,于是 a 在這個代碼塊里不再是 A? 類型, 而是 A 類型。
- val a: A? = A()
- if (a != null) {
- println(a.b)
- }
智能轉換還有一個經常出現的場景,那就是 switch case 語句中。在 Kotlin 中,則是 when 語法。
- fun testWhen(obj: Any) {
- when(obj) {
- is Int -> {
- println("obj is a int")
- println(obj + 1)
- }
- is String -> {
- println("obj is a string")
- println(obj.length)
- }
- else -> {
- println("obj is something i don't care")
- }
- }
- }
- fun main(args: Array<String>) {
- testWhen(98)
- testWhen("98")
- }
輸出如下:
- obj is a int
- 99
- obj is a string
- 2
可以看出在已經判斷出是 String 的條件下,原本是一個 Any 類的 obj 對象,我可以直接使用屬于 String 類的 .length 屬性。而在 JAVA 中,我們需要這樣做:
- System.out.println("obj is a string")
- String string = (String) obj;
- System.out.println(string.length)
或者
- System.out.println("obj is a string")
- System.out.println(((String) obj).length)
前者打斷了編寫和閱讀的連貫性,后者嘛。。
Kotlin 的智能程度遠不止如此,即便是現在,在編寫代碼的時候還會偶爾蹦一個高亮警告出來,這時候我才知道原來我的寫法是多余的,Kotlin 已經幫我處理了好了。此處不再一一贅述。
3.比 switch 更強大的 when
通過上面智能轉化的例子,已經展示了一部分 when 的功能。但相對于 JAVA 的 switch,Kotlin 的 when 帶給我的驚喜遠遠不止這么一點。
例如:
- fun testWhen(int: Int) {
- when(int) {
- in 10 .. Int.MAX_VALUE -> println("${int} 太大了我懶得算")
- 2, 3, 5, 7 -> println("${int} 是質數")
- else -> println("${int} 不是質數")
- }
- }
- fun main(args: Array<String>) {
- (0..10).forEach { testWhen(it) }
- }
輸出如下:
- 不是質數
- 不是質數
- 是質數
- 是質數
- 不是質數
- 是質數
- 不是質數
- 是質數
- 不是質數
- 不是質數
- 太大了我懶得算
和 JAVA 中死板的 switch-case 語句不同,在 when 中,我既可以用參數去匹配 10 到 Int.MAX_VALUE 的區間,也可以去匹配 2, 3, 5, 7 這一組值,當然我這里沒有列舉所有特性。when 的靈活、簡潔,使得我在使用它的時候變得相當開心(和 JAVA 的 switch 對比的話)
4.容器的操作符
自從迷上 RxJava 之后,我實在很難再回到從前,這其中就有 RxJava 中許多方便的操作符。而 Kotlin 中,容器自身帶有一系列的操作符,可以非常簡潔的去實現一些邏輯。
例如:
- (0 until container.childCount)
- .map { container.getChildAt(it) }
- .filter { it.visibility == View.GONE }
- .forEach { it.visibility = View.VISIBLE }
上述代碼首先創建了一個 0 到 container.childCount - 1 的區間;再用 map 操作符配合取出 child 的代碼將這個 Int 的集合轉化為了 childView 的集合;然后在用 filter 操作符對集合做篩選,選出 childView 中所有可見性為 GONE 的作為一個新的集合;最終 forEach 遍歷把所有的 childView 都設置為 VISIBLE。
這里再貼上 JAVA 的代碼作為對比。
- for(int i = 0; i < container.childCount - 1; i++) {
- View childView = container.getChildAt(i);
- if(childView.getVisibility() == View.GONE) {
- childView.setVisibility(View.VISIBLE);
- }
- }
這里就不詳細的去描述這種鏈式的寫法有什么優點了。
5.線程切換,so easy
既然上面提到了 RxJava,不得不想起 RxJava 的另一個優點——線程調度。Kotlin 中有一個專為 Android 開發量身打造的庫,名為 anko,其中包含了許多可以簡化開發的代碼,其中就對線程進行了簡化。
- async {
- val response = URL("https://www.baidu.com").readText()
- uiThread {
- textView.text = response
- }
- }
上面的代碼很簡單,通過 async 方法將代碼實現在一個異步的線程中,在讀取到 http 請求的響應了之后,再通過 uiThread 方法切換回 ui 線程將 response 顯示在 textView 上。
拋開內部的實現,你再也不需要為了一個簡簡單單的異步任務去寫一大堆的無效代碼。按照慣例,這里似乎應該貼上 JAVA 的代碼做對比,但請原諒我不想刷屏(啊哈哈)
6.一個關鍵字實現單例
沒錯,就是一個關鍵字就可以實現單例:
- object Log {
- fun i(string: String) {
- println(string)
- }
- }
- fun main(args: Array<String>) {
- Log.i("test")
- }
再見,單例模式
7.自動 getter、setter 及 class 簡潔聲明
JAVA 中有如下類
- class Person {
- private String name;
- public Person(String name) {
- this.name = name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public void getName() {
- return name;
- }
- }
- Person person = new Person("張三");
Person person = new Person("張三");
可以看出,標準寫法下,一個屬性對應了 get 和 set 兩個方法,需要手動寫的代碼量相當大。當然有快捷鍵幫助我們生成這些代碼,但是考慮到各種復雜情形總歸不***。
而 Kotlin 中是這樣的:
- class Person(var name: String)
- val person = Person("張三");
還可以添加默認值:
- class Person(var name: String = "張三")
- val person = Person()
再附上我項目中一個比較復雜的數據類:
- data class Column(
- var subId: String?,
- var subTitle: String?,
- var subImg: String?,
- var subCreatetime: String?,
- var subUpdatetime: String?,
- var subFocusnum: Int?,
- var lastId: String?,
- var lastMsg: String?,
- var lastType: String?,
- var lastMember: String?,
- var lastTIme: String?,
- var focus: String?,
- var subDesc: String?,
- var subLikenum: Int?,
- var subContentnum: Int?,
- var pushSet: String?
- )
一眼望去,沒有多余代碼。這是為什么我認為 Kotlin 代碼比 JAVA 代碼要更容易寫得干凈的原因之一。
8. DSL 式編程
說起 dsl ,Android 開發者接觸的最多的或許就是 gradle 了
例如:
- android {
- compileSdkVersion 23
- buildToolsVersion "23.0.2"
- defaultConfig {
- applicationId "com.zll.demo"
- minSdkVersion 15
- targetSdkVersion 23
- versionCode 1
- versionName "1.0"
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
- }
這就是一段 Groovy 的 DSL,用來聲明編譯配置
那么在 Android 項目的代碼中使用 DSL 是一種什么樣的感覺呢?
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val homeFragment = HomeFragment()
- val columnFragment = ColumnFragment()
- val mineFragment = MineFragment()
- setContentView(
- tabPages {
- backgroundColor = R.color.white
- dividerColor = R.color.colorPrimary
- behavior = ByeBurgerBottomBehavior(context, null)
- tabFragment {
- icon = R.drawable.selector_tab_home
- body = homeFragment
- onSelect { toast("home selected") }
- }
- tabFragment {
- icon = R.drawable.selector_tab_search
- body = columnFragment
- }
- tabImage {
- imageResource = R.drawable.selector_tab_photo
- onClick { showSheet() }
- }
- tabFragment {
- icon = R.drawable.selector_tab_mine
- body = mineFragment
- }
- }
- )
- }
沒錯,上面的代碼就是用來構建這個主界面的 viewPager + fragments + tabBar 的。以 tabPages 作為開始,設置背景色,分割線等屬性;再用 tabFrament 添加 fragment + tabButton,tabImage 方法則只添加 tabButton。所見的代碼都是在做配置,而具體的實現則被封裝了起來。
前面提到過 anko 這個庫,其實也可以用來替代 xml 做布局用:
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- verticalLayout {
- textView {
- text = "這是標題"
- }.lparams {
- width = matchParent
- height = dip(44)
- }
- textView {
- text = "這是內容"
- gravity = Gravity.CENTER
- }.lparams {
- width = matchParent
- height = matchParent
- }
- }
- }
相比于用 JAVA 代碼做布局,這種 DSL 的方式也是在做配置,把布局的實現代碼封裝在了背后,和 xml 布局很接近。
關于 DSL 和 anko 布局,以后會有專門的文章做介紹,這里就此打住。
9.委托/代理,SharedPreference 不再麻煩
通過 Kotlin 中的委托功能,我們能輕易的寫出一個 SharedPreference 的代理類
- class Preference<T>(val context: Context, val name: String?, val default: T) : ReadWriteProperty<Any?, T> {
- val prefs by lazy {
- context.getSharedPreferences("xxxx", Context.MODE_PRIVATE)
- }
- override fun getValue(thisRef: Any?, property: KProperty<*>): T = with(prefs) {
- val res: Any = when (default) {
- is Long -> {
- getLong(name, 0)
- }
- is String -> {
- getString(name, default)
- }
- is Float -> {
- getFloat(name, default)
- }
- is Int -> {
- getInt(name, default)
- }
- is Boolean -> {
- getBoolean(name, default)
- }
- else -> {
- throw IllegalArgumentException("This type can't be saved into Preferences")
- }
- }
- res as T
- }
- override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = with(prefs.edit()) {
- when (value) {
- is Long -> putLong(name, value)
- is String -> putString(name, value)
- is Float -> putFloat(name, value)
- is Int -> putInt(name, value)
- is Boolean -> putBoolean(name, value)
- else -> {
- throw IllegalArgumentException("This type can't be saved into Preferences")
- }
- }.apply()
- }
- }
暫且跳過原理,我們去看怎么使用
- class EntranceActivity : BaseActivity() {
- private var userId: String by Preference(this, "userId", "")
- override fun onCreate(savedInstanceState: Bundle?) {
- testUserId()
- }
- fun testUserId() {
- if (userId.isEmpty()) {
- println("userId is empty")
- userId = "default userId"
- } else {
- println("userId is $userId")
- }
- }
- }
重復啟動 app 輸出結果:
- userId is empty
- userId is default userId
- userId is default userId
- ...
***次啟動 app 的時候從 SharedPreference 中取出來的 userId 是空的,可是后面卻不為空。由此可見,userId = "default userId" 這句代碼成功的將 SharedPreference 中的值修改成功了。
也就是說,在這個 Preference 代理的幫助下,SharedPreference 存取操作變得和普通的對象調用、賦值一樣的簡單。
10.擴展,和工具類說拜拜
很久很久以前,有人和我說過,工具類本身就是一種違反面向對象思想的東西。可是當時我就想了,你不讓我用工具類,那有些代碼我該怎么寫呢?直到我知道了擴展這個概念,我才豁然開朗。
- fun ImageView.displayUrl(url: String?) {
- if (url == null || url.isEmpty() || url == "url") {
- imageResource = R.mipmap.ic_launcher
- } else {
- Glide.with(context)
- .load(ColumnServer.SERVER_URL + url)
- .into(this)
- }
- }
- ...
- val imageView = findViewById(R.id.avatarIv) as ImageView
- imageView.displayUrl(url)
上述代碼可理解為:
- 我給 ImageView 這個類擴展了一個名為 displayUrl 的方法,這個方法接收一個名為 url 的 String?類對象。如不出意外,會通過 Glide 加載這個 url 的圖片,顯示在當前的 imageView 上;
- 我在另一個地方通過 findViewById 拿到了一個 ImageView 類的實例,然后調用這個 imageView 的displayUrl 方法,試圖加載我傳入的 url
通過擴展來為 ImageView 添加方法,相比于通過繼承 ImageView 來寫一個 CustomImageView,再添加方法而言,侵入性更低,不需要在代碼中全寫 CustomImageView,也不需要在 xml 布局中將包名寫死,造成移植的麻煩。
這事用工具類當然也可以做,比如做成 ImageUtil.displayUrl(imageView, url),但是工具類閱讀起來并沒有擴展出來的方法讀起來更自然更流暢。
擴展是 Kotlin 相比于 JAVA 的一大殺器