用Kotlin開發(fā)Android項(xiàng)目是一種什么樣的感受?
前言
從初學(xué) Kotlin,到嘗試性的寫一點(diǎn)體驗(yàn)代碼,再到實(shí)驗(yàn)性的做一些封裝工作,到***摸爬滾打著寫了一個(gè)項(xiàng)目。不得不說過程中還是遇上了不少的問題,盡管有不少坑是源于我自己的選擇,比如使用了 anko 布局放棄了 xml,但是總體來說,這門語言帶給我的驚喜是完全足以讓我忽略路上的坎坷。
這篇文章僅僅是想整理一下這一路走過來的一些感想和驚喜,隨著我對(duì) Kotlin 的學(xué)習(xí)和使用,會(huì)長(zhǎng)期修改。
正文
1.有了空安全,再也不怕服務(wù)端返回空對(duì)象了
簡(jiǎn)單一點(diǎn)的例子,那就是 String 和 String?是兩種不同的類型。String 已經(jīng)確定是不會(huì)為空,一定有值;而 String?則是未知的,也許有值,也許是空。在使用對(duì)象的屬性和方法的時(shí)候,String 類型的對(duì)象可以毫無顧忌的直接使用,而 String?類型需要你先做非空判斷。
- fun demo() {
- val string1: String = "string1"
- val string2: String? = null
- val string3: String? = "string3"
- println(string1.length)
- println(string2?.length)
- println(string3?.length)
- }
輸出結(jié)果為:
- 7
- null
- 7
盡管 string2 是一個(gè)空對(duì)象,也并沒有因?yàn)槲艺{(diào)用了它的屬性/方法就報(bào)空指針。而你所需要做的,僅僅是加一個(gè)"?"。
如果說這樣還體現(xiàn)不出空安全的好處,那么看下面的例子:
- val a: A? = A()
- println(a?.b?.c)
試想一下當(dāng)每一級(jí)的屬性皆有可能為空的時(shí)候,JAVA 中我們需要怎么處理?
2.轉(zhuǎn)型與智能轉(zhuǎn)換,省力又省心
我寫過這樣子的 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")
- }
縮減代碼之后對(duì)比更加明顯
- JAVA
- if(view instanceof TextView) {
- ((TextView) view).setText("text");
- }
- Kotlin
- if(view is TextView) {
- (view as TextView).setText("text")
- }
相比于 JAVA 在對(duì)象前加 (Class) 這樣子的寫法,Kotlin 是在對(duì)象之后添加 as Class 來實(shí)現(xiàn)轉(zhuǎn)型。至少我個(gè)人而言,在習(xí)慣了 as Class 順暢的寫法之后,是再難以忍受 JAVA 中前置的寫法,哪怕有 cast 快捷鍵的存在,仍然很容易打斷我寫代碼的順序和思路
事實(shí)上,Kotlin 此處可以更簡(jiǎn)單:
- if(view is TextView) {
- view.setText("text")
- }
因?yàn)楫?dāng)前上下文已經(jīng)判明 view 就是 TextView,所以在當(dāng)前代碼塊中 view 不再是 View 類,而是 TextView 類。這就是 Kotlin 的智能轉(zhuǎn)換。
接著上面的空安全來舉個(gè)例子,常規(guī)思路下,既然 String 和 String? 是不同的類型,是不是我有可能會(huì)寫出這樣的代碼?
- val a: A? = A()
- if (a != null) {
- println(a?.b)
- }
這樣子寫,Kotlin 反而會(huì)給你顯示一個(gè)高亮的警告,說這是一個(gè)不必要的 safe call。至于為什么,因?yàn)槟闱懊嬉呀?jīng)寫了 a != null 了啊,于是 a 在這個(gè)代碼塊里不再是 A? 類型, 而是 A 類型。
- val a: A? = A()
- if (a != null) {
- println(a.b)
- }
智能轉(zhuǎn)換還有一個(gè)經(jīng)常出現(xiàn)的場(chǎng)景,那就是 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
可以看出在已經(jīng)判斷出是 String 的條件下,原本是一個(gè) Any 類的 obj 對(duì)象,我可以直接使用屬于 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 的智能程度遠(yuǎn)不止如此,即便是現(xiàn)在,在編寫代碼的時(shí)候還會(huì)偶爾蹦一個(gè)高亮警告出來,這時(shí)候我才知道原來我的寫法是多余的,Kotlin 已經(jīng)幫我處理了好了。此處不再一一贅述。
3.比 switch 更強(qiáng)大的 when
通過上面智能轉(zhuǎn)化的例子,已經(jīng)展示了一部分 when 的功能。但相對(duì)于 JAVA 的 switch,Kotlin 的 when 帶給我的驚喜遠(yuǎn)遠(yuǎn)不止這么一點(diǎn)。
例如:
- fun testWhen(int: Int) {
- when(int) {
- in 10 .. Int.MAX_VALUE -> println("${int} 太大了我懶得算")
- 2, 3, 5, 7 -> println("${int} 是質(zhì)數(shù)")
- else -> println("${int} 不是質(zhì)數(shù)")
- }
- }
- fun main(args: Array<String>) {
- (0..10).forEach { testWhen(it) }
- }
輸出如下:
- 不是質(zhì)數(shù)
- 不是質(zhì)數(shù)
- 是質(zhì)數(shù)
- 是質(zhì)數(shù)
- 不是質(zhì)數(shù)
- 是質(zhì)數(shù)
- 不是質(zhì)數(shù)
- 是質(zhì)數(shù)
- 不是質(zhì)數(shù)
- 不是質(zhì)數(shù)
- 太大了我懶得算
和 JAVA 中死板的 switch-case 語句不同,在 when 中,我既可以用參數(shù)去匹配 10 到 Int.MAX_VALUE 的區(qū)間,也可以去匹配 2, 3, 5, 7 這一組值,當(dāng)然我這里沒有列舉所有特性。when 的靈活、簡(jiǎn)潔,使得我在使用它的時(shí)候變得相當(dāng)開心(和 JAVA 的 switch 對(duì)比的話)
4.容器的操作符
自從迷上 RxJava 之后,我實(shí)在很難再回到從前,這其中就有 RxJava 中許多方便的操作符。而 Kotlin 中,容器自身帶有一系列的操作符,可以非常簡(jiǎn)潔的去實(shí)現(xiàn)一些邏輯。
例如:
- (0 until container.childCount)
- .map { container.getChildAt(it) }
- .filter { it.visibility == View.GONE }
- .forEach { it.visibility = View.VISIBLE }
上述代碼首先創(chuàng)建了一個(gè) 0 到 container.childCount - 1 的區(qū)間;再用 map 操作符配合取出 child 的代碼將這個(gè) Int 的集合轉(zhuǎn)化為了 childView 的集合;然后在用 filter 操作符對(duì)集合做篩選,選出 childView 中所有可見性為 GONE 的作為一個(gè)新的集合;最終 forEach 遍歷把所有的 childView 都設(shè)置為 VISIBLE。
這里再貼上 JAVA 的代碼作為對(duì)比。
- for(int i = 0; i < container.childCount - 1; i++) {
- View childView = container.getChildAt(i);
- if(childView.getVisibility() == View.GONE) {
- childView.setVisibility(View.VISIBLE);
- }
- }
這里就不詳細(xì)的去描述這種鏈?zhǔn)降膶懛ㄓ惺裁磧?yōu)點(diǎn)了。
5.線程切換,so easy
既然上面提到了 RxJava,不得不想起 RxJava 的另一個(gè)優(yōu)點(diǎn)——線程調(diào)度。Kotlin 中有一個(gè)專為 Android 開發(fā)量身打造的庫(kù),名為 anko,其中包含了許多可以簡(jiǎn)化開發(fā)的代碼,其中就對(duì)線程進(jìn)行了簡(jiǎn)化。
- async {
- val response = URL("https://www.baidu.com").readText()
- uiThread {
- textView.text = response
- }
- }
上面的代碼很簡(jiǎn)單,通過 async 方法將代碼實(shí)現(xiàn)在一個(gè)異步的線程中,在讀取到 http 請(qǐng)求的響應(yīng)了之后,再通過 uiThread 方法切換回 ui 線程將 response 顯示在 textView 上。
拋開內(nèi)部的實(shí)現(xiàn),你再也不需要為了一個(gè)簡(jiǎn)簡(jiǎn)單單的異步任務(wù)去寫一大堆的無效代碼。按照慣例,這里似乎應(yīng)該貼上 JAVA 的代碼做對(duì)比,但請(qǐng)?jiān)徫也幌胨⑵?啊哈哈)
6.一個(gè)關(guān)鍵字實(shí)現(xiàn)單例
沒錯(cuò),就是一個(gè)關(guān)鍵字就可以實(shí)現(xiàn)單例:
- object Log {
- fun i(string: String) {
- println(string)
- }
- }
- fun main(args: Array<String>) {
- Log.i("test")
- }
再見,單例模式
7.自動(dòng) getter、setter 及 class 簡(jiǎn)潔聲明
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("張三");
可以看出,標(biāo)準(zhǔn)寫法下,一個(gè)屬性對(duì)應(yīng)了 get 和 set 兩個(gè)方法,需要手動(dòng)寫的代碼量相當(dāng)大。當(dāng)然有快捷鍵幫助我們生成這些代碼,但是考慮到各種復(fù)雜情形總歸不***。
而 Kotlin 中是這樣的:
- class Person(var name: String)
- val person = Person("張三");
還可以添加默認(rèn)值:
- class Person(var name: String = "張三")
- val person = Person()
再附上我項(xiàng)目中一個(gè)比較復(fù)雜的數(shù)據(jù)類:
- 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?
- )
一眼望去,沒有多余代碼。這是為什么我認(rèn)為 Kotlin 代碼比 JAVA 代碼要更容易寫得干凈的原因之一。
8. DSL 式編程
說起 dsl ,Android 開發(fā)者接觸的最多的或許就是 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 項(xiàng)目的代碼中使用 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
- }
- }
- )
- }
沒錯(cuò),上面的代碼就是用來構(gòu)建這個(gè)主界面的 viewPager + fragments + tabBar 的。以 tabPages 作為開始,設(shè)置背景色,分割線等屬性;再用 tabFrament 添加 fragment + tabButton,tabImage 方法則只添加 tabButton。所見的代碼都是在做配置,而具體的實(shí)現(xiàn)則被封裝了起來。
前面提到過 anko 這個(gè)庫(kù),其實(shí)也可以用來替代 xml 做布局用:
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- verticalLayout {
- textView {
- text = "這是標(biāo)題"
- }.lparams {
- width = matchParent
- height = dip(44)
- }
- textView {
- text = "這是內(nèi)容"
- gravity = Gravity.CENTER
- }.lparams {
- width = matchParent
- height = matchParent
- }
- }
- }
相比于用 JAVA 代碼做布局,這種 DSL 的方式也是在做配置,把布局的實(shí)現(xiàn)代碼封裝在了背后,和 xml 布局很接近。
關(guān)于 DSL 和 anko 布局,以后會(huì)有專門的文章做介紹,這里就此打住。
9.委托/代理,SharedPreference 不再麻煩
通過 Kotlin 中的委托功能,我們能輕易的寫出一個(gè) 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()
- }
- }
暫且跳過原理,我們?nèi)タ丛趺词褂?/p>
- 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")
- }
- }
- }
重復(fù)啟動(dòng) app 輸出結(jié)果:
- userId is empty
- userId is default userId
- userId is default userId
- ...
***次啟動(dòng) app 的時(shí)候從 SharedPreference 中取出來的 userId 是空的,可是后面卻不為空。由此可見,userId = "default userId" 這句代碼成功的將 SharedPreference 中的值修改成功了。
也就是說,在這個(gè) Preference 代理的幫助下,SharedPreference 存取操作變得和普通的對(duì)象調(diào)用、賦值一樣的簡(jiǎn)單。
10.擴(kuò)展,和工具類說拜拜
很久很久以前,有人和我說過,工具類本身就是一種違反面向?qū)ο笏枷氲臇|西。可是當(dāng)時(shí)我就想了,你不讓我用工具類,那有些代碼我該怎么寫呢?直到我知道了擴(kuò)展這個(gè)概念,我才豁然開朗。
- 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 這個(gè)類擴(kuò)展了一個(gè)名為 displayUrl 的方法,這個(gè)方法接收一個(gè)名為 url 的 String?類對(duì)象。如不出意外,會(huì)通過 Glide 加載這個(gè) url 的圖片,顯示在當(dāng)前的 imageView 上;
- 我在另一個(gè)地方通過 findViewById 拿到了一個(gè) ImageView 類的實(shí)例,然后調(diào)用這個(gè) imageView 的displayUrl 方法,試圖加載我傳入的 url
通過擴(kuò)展來為 ImageView 添加方法,相比于通過繼承 ImageView 來寫一個(gè) CustomImageView,再添加方法而言,侵入性更低,不需要在代碼中全寫 CustomImageView,也不需要在 xml 布局中將包名寫死,造成移植的麻煩。
這事用工具類當(dāng)然也可以做,比如做成 ImageUtil.displayUrl(imageView, url),但是工具類閱讀起來并沒有擴(kuò)展出來的方法讀起來更自然更流暢。
擴(kuò)展是 Kotlin 相比于 JAVA 的一大殺器