標籤:

實戰kotlin@android(三): 擴展變數與其它技巧

這是Kotlin開發系列文章的第三篇,也是最後一篇。在前面的兩篇文章中,我們探索了如何使用Kotlin來進行部分實用Android開發工作。如果你沒有看過,建議可以先看一下:

實戰Kotlin@Android(一):項目配置和語言轉換

實戰Kotlin@Andorid(二):界面構建與擴展方法

版權聲明

原作者:Doug Stevenson

譯者:程大治

本文由原作者授權翻譯並發布,首發於移動開發前線公眾號,未經允許禁止轉載。

到現在,我們已經可以使用比XML更少的代碼完成View的構建,更別說Java了。Kotlin的語法為聲明式,View之間的嵌套也十分清晰,而且我們還可以給類很方便地添加實用方法。

但在上一篇的結尾我們提到要給View設置左內邊距並不容易實現。如果硬是要用Kotlin做這件事,就需要如下編碼,注意其中我們需要調用setPadding()並傳入四個參數,而不是給一個由JavaBean風格的getter/setter生成的模擬屬性賦值。

為了使構建View的代碼風格一致,我們更願意直接給左內邊距賦值,而不是調用有四個參數的方法。你可能想到給View類添加一個擴展方法setleftPadding(int)。這樣做OK,但是你無法通過lambda with receiver這樣寫:leftPadding = dp_i(16),原因是由Kotlin生成的長相類似JavaBean的方法不會生成模擬變數,只有Java類中的成員方法可以。

但是,Kotlin可以定義擴展屬性,長得和Java類與方法中的變數差不多。一個擴展變數會像Kotlin擴展方法一樣被移植到存在的類中,所以可以直接通過該類實例訪問,只需你在代碼中導入擴展變數。

我們可以定義一個擴展變數使得對於左內邊距的設置代碼能和其他屬性設置代碼風格一致。下面的代碼設置了左內邊距,其他類似:

所以TextView就可以這樣創建了:

所有屬性的設置代碼風格變一致了,棒!

對於padLeft擴展屬性有如下幾點需要注意:

  • 擴展屬性的註解是class dot property,注意與擴展方法的class dot function註解區別開。

  • 擴展屬性的類型在冒號後指定。

  • 通過var關鍵字聲明,這也是Kotlin中聲明可變變數的關鍵字,也就是可以直接賦值。如果要聲明不可變的只讀變數則使用val關鍵字。

  • 一個可變的擴展屬性需要我們同時提供getter和setter的實現。

padLeft的setter方法的實現基於View的setPadding()方法,它接收賦值語句右邊的值作為第一個參數傳入,並傳入TextView的其他內邊距屬性的值。getter方法的實現只不過將View內部的左內邊距屬性返回。

小吐槽:Android的View類給內邊距屬性提供了JavaBean風格的getter方法,卻沒有提供setter方法,現在通過Kotlin我們化解了這個尷尬。

如果你覺得現有的哪個不可修改的API所提供的功能還不夠,就可以通過Kotlin的擴展方法和擴展屬性完善它。

中場

以上說的種種風格與語法都有助於以聲明的方式動態構建View層級,使Kotlin成為一個「域確切」(domain specific)的語言。比如,綜合使用上述所有功能,我們可以寫一個設置了左內邊距屬性的兩個TextView:

就這一段代碼而言還是不錯的,而且肯定比傳統方式強一些。不過,語法還能更加緊湊,還有提升的空間。

如何給特定的layout參數屬性賦值?

從上面的代碼中我們不容易發現,想給一個LinearLayout設置某個特定的layout參數是很難的。到現在為止,我們都在通過把給定LinearLayout.LayoutParams對象賦值給View對象的layoutParams虛擬屬性來給View設置寬高。但當我們需要設置gravity屬性呢?可能你想這麼寫:

編譯器不同意的原因是,View的所有layoutParams屬性都是ViewGroup.LayoutParams類型,也就是所有其他LayoutParams的超類,為了設置LinearLayout的屬性,就需要向下轉型。

在Java中我們需要將LayoutParams轉型或是重新賦值,但在Kotlin我們可以利用一個名叫with的標準庫函數來處理LayoutParams。with方法接收一個對象和一個用於處理該對象的lambda with receiver。描述起來可能沒什麼厲害的,但它的語法是這樣的:

請注意這裡我們可以同時將layoutParams轉型為LinearLayout.LayoutParams並將其作為receiver通過lambda快速訪問其屬性。

還有一點需要知道的就是Android View容器會在子View生成時自動生成一個layout參數對象並賦值給子View。在這裡,當把一個TextView添加進LinearLayout時,會自動生成一個LinearLayout.LayoutParams並賦值給TextView的layoutParams屬性。也就是說,我們並不需要自己創建一個LayoutParams對象,直接用父LinearLayout提供的就可以。記住這種情況只在將一個子View添加進一個父ViewGroup時才會發生,所以最外層的ViewGroup並不會獲得這類對象,因為其沒有父ViewGroup。

對於layout參數來講,這種語法可能還有改善的控制項,但已經很直白了。

現在,我要讓「v」方法消失

伴隨我們這麼久的v方法已經很方便了,但其語法還有簡化的控制項。如果能像下面這樣構建一個View層級豈不是更好?

linearLayout {n textView {n // 各屬性...n }n textView {n // 各屬性...n }n}n

這樣寫顯得更加自然,且可讀性有很大的提升。(長得挺像Gradle)實現這種語法的技巧就是給每一種View類型定義一個縮寫形式。所以為了實現上述代碼,我們需要一個叫linearLayout的方法和v等價,還需要一個textView方法和v等價。在Kotlin中這不難實現:

在這裡我為每一種View類型寫了兩個方法來適配各種被調用的情況,因為在前面v方法可以被Context或ViewGroup調用。Kotlin中支持這種聲明的語言特點叫做單一表述方法single expression function。這是一種針對方法的特殊語法,可以讓你:

  1. 省略方法體的大括弧。

  2. 通過表述的返回類型猜測方法的返回類型。

  3. 省略return關鍵字。

現在我們通過這些方法來重新構建上面的View層級:

所以只要你願意給每一種要用到的View類型定義一個縮寫函數,這還是挺有用的。

Kotlin實用嗎?

到現在為止,我們通過type-safe builder模式、擴展方法與擴展屬性、lambda with receiver寫了一個快速構建View層級的方法。

所以你現在可能要問:我到底要不要以這種方式在我自己的APP中構建View?到現在為止,我一直在說這樣做可以很簡單很方便,但只與在Java中進行同樣工作進行了比較,結果不言自明。不過在Android中我們一般通過XML資源來描述View,所以我們來對比一下通過XML與通過Kotlin的type-safe builder來構建View哪種更強一些。

當Activity配置改變

Android設備中配置隨時會改變,最簡單的一種改變就是屏幕方向。當然還有其他各種改變,具體參見官方文檔。方向的改變對Android View的影響極大,因為我們經常會針對豎向和橫向寫兩套布局。

對於XML布局而言,無需自己更新UI來應對配置改變,只需要寫兩套布局,一套放在res/layout-land下,另一套放在res/layout-port下就好。當在這兩個目錄中給文件相同的命名時,Android就會針對各種情況自己找到正確的文件來填充。總而言之,無需自己寫代碼來處理方向改變。

但當處理動態生成的layout時,就需要自己處理配置信息的改變了。如果你想要對各種配置有不同的布局,就需要自己寫邏輯來判斷要生成何種View。如果你總是需要這麼做,代碼就會變得笨重。

所以,如果對於每種配置信息要填充不同的UI,XML布局更加方便。

當需要處理RelativeLayout等複雜布局

RelativeLayout讓我們以一種非常靈活的方式進行相對布局。你可以很簡單地聲明一個View要放在另一個錨View的上面下面左邊右邊,做這件事時需要將錨View的ID傳入前者。View ID還用於在代碼中對特定View進行配置修改。

在Android中,最好的方式就是讓編譯器自己給View ID賦值,你不應自己寫ID,也就是說,你需要使用Android工具來指定這些ID以備後面使用。

在XML布局中,創建一個View ID很簡單,只需要這樣:

<TextView android:id="@+id/tv" ... />n

@+id註解會告訴Android定義一個叫做tv的新ID,或是重用有同樣名稱的已存在ID。代碼不能更簡單。

當處理動態生成的布局時,無法通過代碼動態創建ID,為了創建新ID,你需要在XML中定義一個新ID,然後再在代碼中通過編譯後的R類來獲取ID的引用。所以,動態操作View就要牽扯到另一個文件,而且在Android中,即使一個ID不再被引用,也不會被刪除。

總的來說,在XML不居中更容易操作View的ID,因為有工具來支持ID的動態管理。

如果需要在給View屬性賦值時進行計算

計算View的屬性很常見,比如設置其內部文本時,或是背景色、尺寸時。

Android XML布局語言完全是聲明性的,你不能在賦值字元串中進行計算。也就是說這些屬性必須在View被填充後動態修改。這樣一來布局文件中的定義與代碼中的修改會被分開。Android開發者對這一點可並不陌生。

但當你動態構建View時,你可以直接將計算結果賦值給View屬性,也可以將某個屬性設置的與另外一個屬性一樣,畢竟我們經常需要給marginLeft和marginRight設置同樣的值。

既然當動態構建View時可以享受代碼帶來的所有靈活性,在賦值方面肯定是動態創建View更加方便,尤其是配合Kotlin的type-safe builder。

如何進行布局微調

除非你能看到代碼就能想像出圖形界面,你肯定需要使用Android Studio的布局預覽來對XML布局進行微調,而不需要構建項目並在終端上跑起來。

當你動態編寫布局時,你不能快速預覽。不過隨著Android Studio2.0中Instant Run功能的推出,這一點不再那麼困擾人了。但是就現在而言,修改布局的最簡單的方式還是Android Studio預覽功能。

兩種方式的性能如何?

性能是考量填充View與動態構造View優劣的重要指標。在我一開始測試的時候,我發現構建一個簡單的布局比填充幾乎快一倍!但當我開始使用更多的Kotlin語法特色,並構建更複雜的View時,差別就不那麼大了。在我的Nexus 6P上,我發現動態構建所花的時間約是填充的四分之三。我猜測這是因為填充時需要先解析布局資源然後構建View。

順帶說一句,在兩種情況中,構建一個有5個View的View層級所花的時間均小於1秒,所以除非你要構建很多View,性能上不需要擔心太多。

如果你想自己測試一下,源碼可以在我的Github上找到(CodingDoug/kotlin-view-builder)。

使用Kotlin的大小上限是多少?

在第一部分中我們提及了有關在Android中使用Kotlin的大小需求問題。你需要聲明一個運行時和一個標準庫的依賴。每當你想給一個Android app添加一個依賴,都應該考慮一下大小,尤其當你不能使用multidex時。

在Kotlin1.0.0中,運行時+標準庫的大小總共是210k+636k=846k,這對一個庫來說真的不小了。使用dex-method-counts統計出的方法數為6584,這隻包含kotlin包下的,沒有算裡面引用的Java runtime方法。這一下就佔了一個dex的10%的方法數。但當在我的測試工程上使用了ProGuard後,數目一下減到42個方法。最終的數目自然會增加,因為還需要用到Kotlin中的其他方法。在包含Kotlin的應用中使用ProGuard可以有效控制其大小。

總結

Kotlin用起來還是很愉快的,它可以直接應用在Android開發中。對於構建View來講,它不是特別的厲害,因為使用XML布局有諸多優勢,就現在而言是最佳的方式。但在某些情況下動態的構建View更符合需求,此時Kotlin就能很大程度上簡化代碼、優化風格。

你可能在思考使用Kotlin有沒有其他的坑。這個問題問得好,在Reddit上有討論(Can we talk about the downsides of using Kotlin? ? r/androiddev)。

如果你想看一看我的測試項目,並將XML布局與Kotlin進行比較,可以clone我的這個repo(github.com/CodingDoug/k

希望你一路讀下來能有所收穫!

原文鏈接:實戰kotlin@android(三):擴展變數與其它技巧

推薦閱讀:

如果通過一根OTG線和一根普通MICRO USB數據線把兩台安卓手機連起來,會出現什麼現象?
2013 年 99% 的手機惡意軟體以 Android 設備為目標,此事會有何影響?
讓這款壁紙應用來發現你隱藏已久的藝術氣息

TAG:Kotlin | Android |