用 Kotlin 開(kāi)發(fā) Android 項(xiàng)目是一種什么樣的感受(二)
前言
前面我已經(jīng)寫(xiě)了一篇名為《用 Kotlin 開(kāi)發(fā) Android 項(xiàng)目是一種什么樣的感受?》的文章。文中多數(shù)提到的還是 Kotlin 語(yǔ)言本身的特點(diǎn),而 Kotlin 對(duì)于 Android 的一些特殊支持我沒(méi)有收錄在內(nèi),已經(jīng)有朋友給我提出了建議。于是在前文的基礎(chǔ)上,這一次我們或許會(huì)說(shuō)的更詳細(xì),Kotlin 開(kāi)發(fā) Android 究竟還有一些什么讓人深感愉悅之處。
正文
1.向 findViewById 說(shuō) NO
不同于 JAVA 中,在 Kotlin 中 findViewById 本身就簡(jiǎn)化了很多,這得益于 Kotlin 的類型推斷以及轉(zhuǎn)型語(yǔ)法后置:
- val onlyTv = findViewById(R.id.onlyTv) as TextView
很簡(jiǎn)潔,但若僅僅是這樣,想必大家會(huì)噴死我:就這么點(diǎn)差距也拿出來(lái)搞事?
當(dāng)然不是。在官方庫(kù) anko 的支持下,這事又有了很多變化。
例如
- val onlyTv = find<TextView>(R.id.onlyTv)
- val onlyTv: TextView = find(R.id.onlyTv)
肯定有人會(huì)問(wèn):find 是個(gè)什么鬼?
讓我們點(diǎn)過(guò)去看看 find 的源碼:
- inline fun <reified T : View> Activity.find(id: Int): T = findViewById(id) as T
忽略掉其他細(xì)節(jié),原來(lái)和我們上面***種寫(xiě)法沒(méi)差別嘛,不就是用一個(gè)擴(kuò)展方法給 Activity 加了這么一個(gè)方法,幫我們寫(xiě)了 findViewById,再幫我們轉(zhuǎn)型了一下嘛。
其實(shí) Kotlin 中還有很多令人乍舌的實(shí)現(xiàn)其實(shí)都是在一些基礎(chǔ)特性的組合之上實(shí)現(xiàn)的,比如上面的 find 方法我結(jié)合一下原生提供的 lazy 代理:
- class MainActivity : AppCompatActivity() {
- val onlyTv by lazy { find<TextView>(R.id.onlyTv) }
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- onlyTv.text = "test"
- }
- }
以上代碼雖是筆者臨時(shí)異想天開(kāi)的一個(gè)玩法,但是經(jīng)過(guò)測(cè)試毫無(wú)問(wèn)題。
也就是說(shuō),我可以這樣子把 view 的聲明和 findViewById 一同放在聲明的地方。
而且這還只是用原生提供的 lazy 代理,如果愿意,我們完全可以達(dá)成這樣的效果:
- val onlyTv by myOwnDelegate(R.id.onlyTv)
如果我們給 myOwnDelegate 取一個(gè)名字呢?
- val onlyTv by find<TextView>(R.id.onlyTv)
- val onlyTv by findView<TextView>(R.id.onlyTv)
- val onlyTv by findViewById<TextView>(R.id.onlyTv)
挺棒的對(duì)吧?我還要啥依(zi)賴(xing)注(che)入?
有的時(shí)候,還真的要看我們腦洞夠不夠大。正如你以為這就是我想說(shuō)的全部(其實(shí)明明是我自己寫(xiě)到這里以為這一節(jié)應(yīng)該結(jié)束了)
如果我告訴你,其實(shí)你原本一句代碼都不用寫(xiě),你信嗎?
此處為了作為證據(jù),我還是上截圖吧:

毫無(wú) onlyTv 聲明痕跡,也不可能從 AppCompatActivity 繼承而來(lái)。而且當(dāng)你試圖 command/ctrl + 左鍵點(diǎn)擊 onlyTv 想要查看 onlyTv 的來(lái)源的時(shí)候,你會(huì)發(fā)現(xiàn)你跳到了 activity_main 的布局文件:

也許眼尖的朋友已經(jīng)發(fā)現(xiàn)了,唯一的真相就是:
- import kotlinx.android.synthetic.main.activity_main.*
請(qǐng)恕在下能力有限,暫時(shí)無(wú)法為大家講解其中緣由。但可以確定的就是,在 anko 的幫助下,你只需要根據(jù)布局的 id 寫(xiě)一句 import 代碼,然后你就可以把布局中的 id 作為 view 對(duì)象的名稱直接進(jìn)行使用。不僅 activity 中可以這樣玩,你甚至可以 viewA.viewB.viewC,所以大可不必?fù)?dān)心 adapter 中應(yīng)當(dāng)怎么寫(xiě)。
沒(méi)有 findViewById,也就減少了空指針;沒(méi)有 cast,則幾乎不會(huì)有類型轉(zhuǎn)換異常。
PS.也許有的朋友會(huì)發(fā)現(xiàn)這和 Google 出品的 databinding 實(shí)在是有異曲同工之妙,那如果我告訴你,databinding 庫(kù)本身就有對(duì) kotlin 的依賴呢?
2.簡(jiǎn)單粗暴的 startActivity
我們?cè)敬蠖际沁@樣子來(lái)做 Activity 跳轉(zhuǎn)的:
- Intent intent = new Intent(LoginActivity.this, MainActivity.class);
- startActivity(intent);
為了 startActivity,我不得不 new 一個(gè) Intent 出來(lái),特別是當(dāng)我要傳遞參數(shù)的時(shí)候:
- Intent intent = new Intent(LoginActivity.this, MainActivity.class);
- intent.putExtra("name", "張三");
- intent.putExtra("age", 27);
- startActivity(intent);
不知道大家有木有累覺(jué)不愛(ài)?
在 anko 的幫助下,startActivity 是這樣子的:
- startActivity<MainActivity>()
- startActivity<MainActivity>("name" to "張三", "age" to 27)
- startActivityForResult<MainActivity>(101, "name" to "張三", "age" to 27)
無(wú)參情況下,只需要在調(diào)用 startActivity 的時(shí)候加一個(gè) Activity 的 Class 泛型來(lái)告知要到哪去。有參也好說(shuō),這個(gè)方法支持你傳入 vararg params: Pair
有沒(méi)有覺(jué)得代碼寫(xiě)起來(lái)、讀起來(lái)流暢了許多?
3.玲瓏小巧的 toast
JAVA 中寫(xiě)一個(gè) toast 大概是這樣子的:
- Toast.makeText(context, "this is a toast", Toast.LENGTH_SHORT).show();
以上代碼純屬手打,如有錯(cuò)誤請(qǐng)各位指正。
不得不說(shuō)真的是又臭又長(zhǎng),雖然確實(shí)是有很多考量在里面,但是對(duì)于使用來(lái)說(shuō)實(shí)在是太不便利了,而且還很容易忘記***一個(gè) show()。我敢說(shuō)沒(méi)有任何一個(gè)一年以上的 Android 開(kāi)發(fā)者會(huì)不去封裝一個(gè) ToastUtil 的。
封裝之后大概會(huì)是這樣:
- ToastUtil.showShort(context, "this is a toast");
如果處理一下 context 的問(wèn)題,可以縮短成這樣:
- ToastUtil.showShort("this is a toast");
有那么一點(diǎn)極簡(jiǎn)的味道了對(duì)吧?
好了,是時(shí)候讓我們看看 anko 是怎么做的了:
- context.toast("this is a toast")
如果當(dāng)前已經(jīng)是在 context 上下文中(比如 activity):
- toast("this is a toast")
如果你是想要一個(gè)長(zhǎng)時(shí)間的 toast:
- longToast("this is a toast")
沒(méi)錯(cuò),就是給 Context 類擴(kuò)展了 toast 和 longToast 方法,用屁股想都知道里面干了什么。只是這樣一來(lái)比任何工具類都來(lái)得更簡(jiǎn)潔更直觀。
4.用 apply 方法進(jìn)行數(shù)據(jù)組合
假設(shè)有如下 A、B、C 三個(gè) class:
- class A(val b: B)
- class B(val c: C)
- class C(val content: String)
可以看到,A 中有 B,B 中有 C。在實(shí)際開(kāi)發(fā)的時(shí)候,我們有的時(shí)候難免會(huì)遇到比這個(gè)更復(fù)雜的數(shù)據(jù),嵌套層級(jí)很深。這種時(shí)候,用 JAVA 初始化一個(gè) A 類數(shù)據(jù)會(huì)變成一件非常痛苦的事情。例如:
- C c = new C("content");
- B b = new B(c);
- A a = new A(b);
這還是 A、B、C 的關(guān)系很單純的情況下,如果有大量數(shù)據(jù)進(jìn)行組合,那么我們會(huì)需要初始化大量的對(duì)象進(jìn)行賦值、修改等操作。如果我描述的不夠清楚的話,大家不妨想一想用 JAVA 代碼布局是一種什么樣的感覺(jué)?
當(dāng)然,在 JAVA 中也是有解決方案的,比如 Android 中常用的 Dialog,就用了 Builder 模式來(lái)進(jìn)行相應(yīng)配置。(說(shuō)到這里,其實(shí)用 Builder 模式基本上也可以說(shuō)是 JAVA 語(yǔ)言的 DSL)
但是在更為復(fù)雜的情況下,即便是有設(shè)計(jì)模式的幫助,也很難保證代碼的可讀性。那么 Kotlin 有什么好方法,或者說(shuō)小技巧來(lái)解決這個(gè)問(wèn)題嗎?
Kotlin 中有一個(gè)名為 apply 的方法,它的源碼是這樣子的:
- @kotlin.internal.InlineOnly
- public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
沒(méi)有 Kotlin 基礎(chǔ)的小伙伴看到這里一定會(huì)有點(diǎn)暈。我們先忽略一部分細(xì)節(jié),把關(guān)鍵的信息提取出來(lái),再改改格式看看:
- public fun <T> T.apply(block: T.() -> Unit): T {
- block()
- return this
- }
- 首先,我們可以看出 T 是一個(gè)泛型,而且后面沒(méi)有給 T 增加約束條件,那么這里的 T 可以理解為:我這是在給所有類擴(kuò)展一個(gè)名為『apply』的方法;
- ***行***的: T 表明,我最終是要返回一個(gè) T 類。我們也可以看到方法內(nèi)部***的 return this 也能說(shuō)明,其實(shí)***我就是要返回調(diào)用方法的這個(gè)對(duì)象自身;
- 在 return this 之前,我執(zhí)行了一句 block(),這意味著 block 本身一定是一個(gè)方法。我們可以看到,apply 方法接收的 block 參數(shù)的類型有點(diǎn)特殊,不是 String 也不是其他什么明確的類型,而是 T.() -> Unit ;
- T.() -> Unit 表示的意思是:這是一個(gè) ①上下文在 T 對(duì)象中,②返回一個(gè) Unit 類對(duì)象的方法。由于 Unit 和 JAVA 中的 Void 一致,所以可以理解為不需要返回值。那么這里的 block 的意義就清晰起來(lái)了:一個(gè)執(zhí)行在 T,即調(diào)用 apply 方法的對(duì)象自身當(dāng)中,又不需要返回值的方法。
有了上面的解析,我們?cè)賮?lái)看一下這句代碼:
- val textView = TextView(context).apply {
- text = "這是文本內(nèi)容"
- textSize = 16f
- }
這句代碼就是初始化了一個(gè) TextView,并且在將它賦值給 textView 之前,將自己的文本、字體大小修改了。
或許你會(huì)覺(jué)得這和 JAVA 比起來(lái)并沒(méi)有什么優(yōu)勢(shì)。別著急,我們慢慢來(lái):
- layout.addView(TextView(context).apply {
- text = "這是文本內(nèi)容"
- textSize = 16f
- })
這樣又如何呢?我并不需要聲明一個(gè)變量或者常量來(lái)持有這個(gè)對(duì)象才能去做修改操作。
上面的A、B、C 問(wèn)題用 Kotlin 來(lái)實(shí)現(xiàn)是可以這么寫(xiě)的:
- val a = A().apply {
- b = B().apply {
- c = C("content")
- }
- }
我只聲明了一個(gè) a 對(duì)象,然后初始化了一個(gè) A,在這個(gè)初始化的對(duì)象中先給 B 賦值,然后再提交給了 a。B 中的 C 也是如此。當(dāng)組合變得復(fù)雜的時(shí)候,我也能保持我的可讀性:
- val a = A().apply {
- b = B().apply {
- c = C("content")
- }
- d = D().apply {
- b = B().apply {
- c = C("test")
- }
- e = E("test")
- }
- }
上面的代碼用 JAVA 實(shí)現(xiàn)會(huì)是如何一番場(chǎng)景?反正我是想一想就已經(jīng)暈了。說(shuō)到底,這個(gè)小技巧也就是 ①擴(kuò)展方法 + ②高階函數(shù) 兩個(gè)特性組合在一起實(shí)現(xiàn)的效果。
5.利用高階函數(shù)搞事情
先看代碼
- inline fun debug(code: () -> Unit) {
- if (BuildConfig.DEBUG) {
- code()
- }
- }
- ...
- // Application 中
- debug {
- Timber.plant(Timber.DebugTree())
- }
上述代碼是先定義了一個(gè)全局的名為 debug 的方法,這個(gè)方法接收一個(gè)方法作為參數(shù),命名為 code。然后在方法體內(nèi)部,我先判斷當(dāng)前是不是 DEBUG 版本,如果是,再調(diào)用傳入的 code 方法。
而后我們?cè)?Application 中,debug 方法就成為了依據(jù)條件執(zhí)行代碼的關(guān)鍵字。僅當(dāng) DEBUG 版本的時(shí)候,我才初始化 Timber 這個(gè)日志庫(kù)。
如果這還不夠體現(xiàn)有點(diǎn)的話,那么可以再看看下面一段:
- supportsLollipop {
- window.statusBarColor = Color.TRANSPARENT
- window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- }
當(dāng)系統(tǒng)版本在 Lollipop 之上時(shí)才去做沉浸式狀態(tài)欄。系統(tǒng) api 經(jīng)常會(huì)有版本的限制,相對(duì)于一個(gè) supportsLollipop 關(guān)鍵字, 我想一定不是所有人都希望每次都去寫(xiě):
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- // do something
- }
諸如此類的場(chǎng)景和可以自創(chuàng)的 關(guān)鍵字/代碼塊 還有很多。
例如:
- inline fun handleException(code : () -> Unit) {
- try {
- code()
- } catch (e : Exception) {
- e.printStackTrace()
- }
- }
- ...
- handleException {
- println(Integer.parseInt("這明顯不是數(shù)字"))
- }
雖然大都可以用 if(xxxxUtil.isxxxx()) 來(lái)湊合,但是既然有了更好的方案,那還何必湊合呢?
6.用擴(kuò)展方法替代工具類
曾幾何時(shí),我做字符串判斷的時(shí)候一定會(huì)寫(xiě)一個(gè)工具類,在這個(gè)工具類里充斥著各種各樣的判斷方法。而在 Kotlin 中,可以用擴(kuò)展方法來(lái)替代。下面是我項(xiàng)目中 String 擴(kuò)展方法的一部分:
- fun String.isName(): Boolean {
- if (isEmpty() || length > 10 || contains(" ")) {
- return false
- }
- val reg = Regex("^[a-zA-Z0-9\u4e00-\u9fa5]+$")
- return reg.matches(this)
- }
- fun String.isPassword(): Boolean {
- return length in 6..12
- }
- fun String.isNumber(): Boolean {
- val regEx = "^-?[0-9]+$"
- val pat = Pattern.compile(regEx)
- val mat = pat.matcher(this)
- return mat.find()
- }
- ...
- println("張三".isName())
- println("123abc".isPassword())
- println("123456".isNumber())
7.自動(dòng) getter、setter 使得代碼更精簡(jiǎn)
以 TextView 舉例,JAVA 代碼中獲取文本、設(shè)置文本的代碼分別為:
- String text = textView.getText().toString();
- textView.setText("new text");
Kotlin 中是這樣寫(xiě)的:
- val text = textView.text
- textView.text = "new text"
如果 TextView 是一個(gè)原生的 Kotlin class,那么是沒(méi)有 getText 和 setText 兩個(gè)方法的,而是一個(gè) text 屬性。盡管此處的TextView 是 JAVA class,源碼中有g(shù)etText 和 setText 兩個(gè)方法,Kotlin 也做了類似映射的處理。當(dāng)這個(gè) text 屬性在等號(hào)右邊的時(shí)候,就是在提取 text 屬性(此處映射為 getText);當(dāng)在等號(hào)左邊的時(shí)候,就是在賦值(setText)。
說(shuō)到這里我又想起了上一篇文章中提到的 Preference 代理,其實(shí)也有一定關(guān)聯(lián),那就是當(dāng)一個(gè)屬性在等號(hào)左邊和右邊的時(shí)候,不同于 JAVA 中一定是賦值操作,在 Kotlin 中則有可能會(huì)觸發(fā)一些別的。
未完待續(xù)...
補(bǔ)充:
翻看之前的項(xiàng)目,發(fā)現(xiàn)有如下代碼可做對(duì)比:
構(gòu)建并顯示 BottomSheet
- Builder 版
- BottomSheet.Builder(this@ShareActivity, R.style.ShareSheetStyle)
- .sheet(999, R.drawable.share_circle, R.string.wXSceneTimeline)
- .sheet(998, R.drawable.share_freind, R.string.wXSceneSession)
- .listener { _, id ->
- shareTo(bitmap, target = when(id) {
- 999 -> SendMessageToWX.Req.WXSceneTimeline
- 998 -> SendMessageToWX.Req.WXSceneSession
- else -> throw Exception("it can not happen")
- })
- }
- .build()
- .show()
- DSL 版
- showBottomSheet {
- style = R.style.ShareSheetStyle
- sheet {
- icon = R.drawable.share_circle
- text = R.string.wXSceneTimeline
- selected {
- shareTo(bitmap, SendMessageToWX.Req.WXSceneTimeline)
- }
- }
- sheet {
- icon = R.drawable.share_freind
- text = R.string.wXSceneSession
- selected {
- shareTo(bitmap, SendMessageToWX.Req.WXSceneTimeline)
- }
- }
- }
apply 構(gòu)建數(shù)據(jù)實(shí)例(微信分享)
- 普通版
- val obj = WXImageObject(bitmap)
- val thumb = ......
- bitmap.recycle()
- val msg = WXMediaMessage()
- msg.mediaObject = obj
- msg.thumbData = thumb
- val req = SendMessageToWX.Req()
- req.transaction = "share"
- req.scene = target
- req.message = msg
- WxObject.api.sendReq(req)
- DSL 版
- WxObject.api.sendReq(
- SendMessageToWX.Req().apply {
- transaction = "share"
- scene = target
- message = WXMediaMessage().apply {
- mediaObject = WXImageObject(bitmap)
- thumbData = ......
- bitmap.recycle()
- }
- }
- )
要是有人能只看普通版的,3秒之內(nèi)看清結(jié)構(gòu)關(guān)系,那一定是天才。。























