Kotlin在Android 開發中的 16 個建議
譯者簡介:ASCE1885, 《Android 高級進階》作者。小密圈:Android高級進階,詳情見這篇文
Savvy Apps 在 2016 年底開始在新的 Android 項目中使用 Kotlin,就在 Kotlin 1.0.4 發布之際。最初,我們得到了在規模較小的項目中嘗試 Kotlin 的機會,當嘗試過後,我們發現了它的易用性,使用擴展函數可以很容易的將功能和業務邏輯分離開,而且它為我們節省了開發時間,因此,我們覺得它將是一門先進的語言選型。從那時開始,我們使用 Kotlin 創建了多個 Android App,同時也開發了一些內部的 Kotlin 函數庫。
注意,這些建議是基於你對 Kotlin 的熟悉程度進行排序的,所以你可以根據自身的技術水平很容易的跳過一些覺得沒必要看的建議。
初級建議延遲載入
延遲載入有幾個好處。首先由於載入時機推遲到了變數被訪問時,因此它可以提高應用的啟動速度。相比於使用 Kotlin 開發服務端應用,這一特性對開發 Android 應用而言特別有用。對於 Android 應用來說,我們想要減少應用啟動時間,這樣用戶可以更快看到應用的內容,而不是乾等著看啟動載入頁面。
其次,這樣的延遲載入也有更高的內存效率,因為我們只在它被調用時才將資源載入進內存。在 Android 這樣的移動平台上,內存的使用是非常重要的。因為手機資源是共享的且有限的。例如,如果你創建一個購物應用程序,而用戶可能只會瀏覽你的物品,那麼你可以延遲載入購買相關的 API:
val purchasingApi: PurchasingApi by lazy { val retrofit: Retrofit = Retrofit.Builder() .baseUrl(API_URL) .addConverterFactory(MoshiConverterFactory.create()) .build() retrofit.create(PurchasingApi::class.java) }
通過使用像這樣的延遲載入,如果用戶根本沒有想要在應用中發生購買行為,你的應用將不會載入 PurchasingApi,因此不會消耗它可能會佔用的資源。
延遲載入也是封裝初始化邏輯的好方法:
// bounds is created as soon as the first call to bounds is made val bounds: RectF by lazy { RectF(0f, 0f, width.toFloat(), height.toFloat()) }
只有當 bounds 變數第一次被引用時,將會使用 view 的當前寬和高的值來創建 RectF,這樣我們就不需要一開始顯式的創建 RectF,然後把它設置給 bounds。
自定義 Getters/Setters
Kotlin 的自定義 getters 和 setters 使用 model 類的結構,但指定自定義的行為來獲取和設置欄位值。當為某些框架例如 Parse SDK 使用自定義 model 類時,我們獲取的值不是存放在類實例的局部變數中,而是通過某些自定義方式存儲和檢索到的值,例如從 JSON 中。通過使用自定義的 getters 和 setters,我們可以簡化存取方法的定義:
@ParseClassName("Book") class Book : ParseObject() { // getString() and put() are methods that come from ParseObject var name: String get() = getString("name") set(value) = put("name", value) var author: String get() = getString("author") set(value) = put("author", value) }
存取上面定義的欄位的方式看起來和傳統的訪問 model 類的方式類似:
val book = api.getBook() textAuthor.text = book.author
現在如果你的 model 類的數據來源需要從 Parse SDK 改為其他的數據源,那麼你的代碼可能只需要修改一個地方即可。
Lambdas 表達式
Lambdas 表達式在減少源文件中代碼的總行數的同時,也支持函數式編程。雖然目前在 Android 開發中(譯者註:使用 Java 語言)可以使用 lambdas 表達式,但要麼需要在工程中引入 Retrolambda4,要麼需要改變工程構建時的配置,但 Kotlin 更進一步,完全不需要這些額外的操作即可支持 lambdas。
例如,使用 lambdas 表示式時,onClickListener 的用法如下:
button.setOnClickListener { view -> startDetailActivity() }
它甚至可以支持返回值:
toolbar.setOnLongClickListener { showContextMenu() true }
Android SDK 中有很多設置 listener 或者需要實現單個方法的場景,在這些場景下 lambdas 表示式可以發揮很大的作用。
數據類
數據類簡化了類的定義,自動為類添加 , , 和 方法。它明確定義了 model 類的意圖,以及應該包含什麼內容,同時將純數據與業務邏輯分離開來。
我們以一個例子來看下數據類的定義:
data class User(val name: String, val age: Int)
就這麼簡單,不需要再增加其他的定義,這個類就可以正常工作了。如果將數據類和 Gson 或者類似的 JSON 解析函數庫一起使用,你可以像下面代碼這樣使用默認值創建默認的構建方法:
// Example with Gson s @SerializedName annotation data class User( @SerializedName("name") val name: String = "", @SerializedName("age") val age: Int = 0 )集合過濾
使用 API 時我們經常需要和集合打交道。在大多數情況下你想要過濾或者修改集合的內容。通過使用 Kotlin 的集合過濾功能,我們可以使代碼變得更清晰簡潔。通過下面的集合過濾代碼我們可以更容易的表達結果列表中想要包含的內容:
val users = api.getUsers() // we only want to show the active users in one list val activeUsersNames = items.filter { it.active // the "it" variable is the parameter for single parameter lamdba functions } adapter.setUsers(activeUsers)
使用 Kotlin 內置方法實現集合過濾的功能和其他函數式編程語言非常類似,例如 Java 8 的 streams 或者 Swift 的集合類型。能夠以統一的方式實現集合過濾,有助於我們與團隊成員討論時更好更快的達成共識,例如如何將正確的元素顯示到列表中。
對象表達式
對象表達式允許嚴格的單例定義,所以對於一個可以實例化的類來說它不會有問題。對象表達式也確保你不需要將單例存放在類似 Application 類或者作為一個靜態類的變數。
例如,如果我有一個工具類,它擁有一個靜態的線程相關方法,我想在應用的任何地方中訪問它:
package com.savvyapps.example.util import android.os.Handler import android.os.Looper // notice that this is object instead of class object ThreadUtil { fun onMainThread(runnable: Runnable) { val mainHandler = Handler(Looper.getMainLooper()) mainHandler.post(runnable) } }
然後 ThreadUtil 就可以像調用靜態類方法那樣進行調用了:
ThreadUtil.onMainThread(runnable)
這意味著再也不需要聲明私有的構造方法,或者不得不指定在哪裡存放靜態實例。對象表達式本質上是 Kotlin 語言的第一公民。類似的,我們可以使用對象表達式來代替匿名內部類,如下所示:
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrollStateChanged(state: Int) {} override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} override fun onPageSelected(position: Int) { bindUser(position) } });
這兩種方法本質上是一樣的,它們通過聲明一個對象來創建一個類的單例。
Companion 對象
咋看之下,Kotlin 似乎缺少靜態變數和方法。從某種意義上說,Kotlin 中是沒有這些概念的,但它有 companion 對象的概念。這些 companion 對象是類裡面的單例對象,其中包含了你可能希望以靜態的方式訪問的方法和變數。companion 對象中可以定義常量和方法,類似 Java 中的靜態變數和方法。有了它,你可以採用 fragments 中的 newInstance 模式。
下面讓我們來看一下 companion 對象的最簡單形式:
class User { companion object { const val DEFAULT_USER_AGE = 30 } } // later, accessed like you would a static variable: user.age = User.DEFAULT_USER_AGE
在 Android 中,我們通常使用靜態方法和變數來為 fragments 和 activity intents 創建靜態工廠,如下所示:
class ViewUserActivity : AppCompatActivity() { companion object { const val KEY_USER = "user" fun intent(context: Context, user: User): Intent { val intent = Intent(context, ViewUserActivity::class.java) intent.putExtra(KEY_USER, user) return intent } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_cooking) val user = intent.getParcelableExtra(KEY_USER) //... } }
創建這個 Intent 的調用方式跟你在 Java 中看到的類似:
val intent = ViewUserActivity.intent(context, user) startActivity(intent)
這種模式非常好,因為它減少了 Intent 或者 Fragment 丟失需要顯示的用戶信息或者其他所需要內容的可能性。Companion 對象是在 Kotlin 中保持某種形式的靜態訪問的一種方式,因此應該被相應。
全局常量
Kotlin 允許開發者定義能在整個應用程序的所有地方都能夠訪問的常量。通常情況下,常量應該儘可能減小它們的作用域,但當需要有全局作用域的常量時,這是一種很好的方法,你不需要實現一個常量類:
package com.savvyapps.example import android.support.annotation.StringDef // Note that this is not a class, or an object const val PRESENTATION_MODE_PRESENTING = "presenting" const val PRESENTATION_MODE_EDITING = "editing"
這些全局常量可以在工程中任何地方訪問:
import com.savvyapps.example.PRESENTATION_MODE_EDITING val currentPresentationMode = PRESENTATION_MODE_EDITING
需要記住的是,為了減小代碼複雜性,常量應該儘可能的縮小它的作用域。如果有一個常量值只和 user 類相關,那麼應該將這個常量定義在 user 類的 companion 對象中,而不是通過全局常量的方式。
Optional Parameters
可選參數使得方法調用更靈活,因為我們無需傳遞 null 或者默認值。在定義動畫時尤其有用。
例如,如果你希望在整個應用程序中為 views 的淡齣動畫定義一個方法,但只在特殊的場景下需要指定動畫持續的時間,那麼你可以如下定義這個方法:
fun View.fadeOut(duration: Long = 500): ViewPropertyAnimator { return animate() .alpha(0.0f) .setDuration(duration) }
使用方法如下:
icon.fadeOut() // fade out with default time (500) icon.fadeOut(1000) // fade out with custom time中級建議擴展
擴展的好處在於它允許我們為一個類添加功能同時無需繼承它。例如,你是否曾經希望 Activity 有某些方法,比如 hideKeyboard()?使用擴展,你可以很容易實現這個功能:
fun Activity.hideKeyboard(): Boolean { val view = currentFocus view?.let { val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager return inputMethodManager.hideSoftInputFromWindow(view.windowToken, InputMethodManager.HIDE_NOT_ALWAYS) } return false }
有了擴展,你可以很容易消除使用工具類或者工具方法的需要,並且可以真正的提高代碼的可讀性。我們想要更進一步,使用擴展來改進代碼的組織結構。例如,假設你有一個基礎的 model 類,比如一篇文章。這篇文章可能被看作一個數據類,它從一個 API 數據源中獲取:
class Article(val title: String, val numberOfViews: Int, val topic: String)
假設你想要根據某種公式來計算文章跟用戶的相關性。你是應該直接把相關性作為變數放在這個數據類中嗎?有人會說 Article 這個 model 類應該只保存從 API 中獲取到的數據,僅此而已。在這種情況下,擴展可以再次為你所用:
// In another Kotlin file, possibly named ArticleLogic.kt or something similar fun Article.isArticleRelevant(user: User): Boolean { return user.favoriteTopics.contains(topic) }
現階段,上面這段代碼的作用只是簡單的檢查指定的文章是否在用戶最喜歡的主題列表中。但是,從邏輯上講,這個業務邏輯也可以修改成檢查用戶其他的屬性。由於這個檢查邏輯與 Article model 類是獨立的,你可以修改它,並對方法的目的和它可以更改的能力充滿信心。
lateinit
Kotlin 的一個主要特性是它對空指針安全特性的支持。而 lateinit 提供一種簡單的方式,同時實現空指針安全和 Android 平台要求的變數初始化。這是一個偉大的語言特性,但如果你之前做過較長時間 Java 開發的話,你仍然需要一些時間來適應。在 Kotlin 中,一個欄位要麼立即被賦值,要麼聲明可為空:
var total = 0 // declared right away, no possibility of null var toolbar: Toolbar? = null // could be a toolbar, could be null
在處理 Android 布局時,這種語言特性會令人感到沮喪。我們知道,views 是存在於 Activity 或者 Fragment 中的,在聲明的時候我們不能給 views 賦值,因為它們必須在布局被 inflate 之後才能在 onCreate/onCreateView 中被賦值。你可以在 Activity 中需要用到 view 的地方進行檢查來處理這類問題,但從一個空檢查的處理角度看,這些操作是令人沮喪同時沒有必要的。與之對應的,在 Kotlin 中你可以使用 lateinit 修飾符:
lateinit var toolbar: Toolbar
現在,開發者可以不引用 toolbar,直到它被初始化為止。當和 Butter Knife5之類的函數庫一起使用時,這個效果非常好:
@BindView(R.id.toolbar) lateinit var toolbar: Toolbar override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ButterKnife.bind(this) // you can now reference toolbar with no problems! toolbar.setTitle("Hello There") }安全的類型轉換
某些 Android 編程規範要求進行安全的類型轉換,因為普通的類型轉換可能會引起異常。例如,在 Activity 中創建 Fragment 的一個典型做法是首先使用 FragmentManager 檢查這個 Fragment 實例是否已經存在。如果不存在就創建它並添加到當前 Activity 中。當第一次看到 Kotlin 的類型轉換時,你可能會這樣實現這個功能:
var feedFragment: FeedFragment? = supportFragmentManager .findFragmentByTag(TAG_FEED_FRAGMENT) as FeedFragment
這實際上會導致一個 crash。當你使用 時,它會嘗試對對象進行類型轉換,在這個例子中,轉換後可能為 nul,同時可能引起空指針異常。你需要使用 來代替 ,意思是對對象進行類型轉換,如果轉換失敗,則返回 null。因此,Fragment 的正確初始化應該如下所示:
var feedFragment: FeedFragment? = supportFragmentManager .findFragmentByTag(TAG_FEED_FRAGMENT) as? FeedFragment if (feedFragment == null) { feedFragment = FeedFragment.newInstance() supportFragmentManager.beginTransaction() .replace(R.id.root_fragment, feedFragment, TAG_FEED_FRAGMENT) .commit() }使用 let
let 修飾的對象的值如果不為 null 時,允許執行對應的一個代碼塊。這使得開發者可以避免進行空類型的檢查,使得代碼可讀性更強。例如在 Java 中代碼如下:
if (currentUser != null) { text.setText(currentUser.name) }
在 Kotlin 中使用 let 的方式如下:
user?.let { println(it.name) }
在這個例子中,let 除了讓代碼對開發者更友好,還自動為 user 實例分配一個不為空的變數 it,我們可以繼續使用它而不用擔心它會被置空。
isNullOrEmpty | isNullOrBlank
在開發一個 Android 應用的過程中,我們通常需要進行很多的校驗。如果你曾經在沒有 Kotlin 的情況下處理過這種情況,你可能已經發現並使用過 Android 中的 TextUtils 類。TextUtils 類的用法看下來如下所示:
if (TextUtils.isEmpty(name)) { // alert the user! }
在這個例子中,你會發現如果用戶將它們的用戶名 name 設置為空白,它將通過上面的檢驗。isNullOrEmpty 和 isNullOrBlank 是內置在 Kotlin 語言中的,它們消除了使用 TextUtisl.isEmpty(someString) 的需要,同時提供檢查空白的額外功能。你可以在適當的時候使用這兩個方法:
// If we do not care about the possibility of only spaces... if (number.isNullOrEmpty()) { // alert the user to fill in their number! } // when we need to block the user from inputting only spaces if (name.isNullOrBlank()) { // alert the user to fill in their name! }
欄位校驗的應用程序註冊時常見的一種情況。這些內置的方法對於欄位的校驗和提醒用戶輸入有誤是很好用的。你甚至可以利用擴展方法來實現一些自定義校驗,例如校驗電子郵件地址:
fun TextInputLayout.isValidForEmail(): Boolean { val input = editText?.text.toString() if (input.isNullOrBlank()) { error = resources.getString(R.string.required) return false } else if (emailPattern.matcher(input).matches()) { error = resources.getString(R.string.invalid_email) return false } else { error = null return true } }高級建議避免為 Kotlin 類定義唯一的抽象方法
這個建議可以讓你使用 lambdas 表達式,它使得代碼更清晰更簡潔。例如,當使用 Java 編寫代碼時,經常會遇到編寫監聽器類的情況,如下所示:
public interface OnClickListener { void onClick(View v); }
Kotlin 的一大特性是它為 Java 類執行SAM(Single Abstract Method6,單一抽象方法)的轉換。在 Java 中 ClickListener 的用法如下:
textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // do something } });
使用 Kotlin 代碼可以精簡為:
textView.setOnClickListener { view -> // do something }
但同樣的用法不能用在 Kotlin 中創建的 SAM 介面。這個是語言的設計7決定的,對於 Kotlin 的初學者而言他們會感到吃驚和沮喪。如果相同的監聽器介面是定義在 Kotlin 中,它看起來應該像這樣:
view.setOnClickListener(object : OnClickListener { override fun onClick(v: View?) { // do things } })
為了減少代碼量,你會如下所示這樣來編寫類中的監聽器:
private var onClickListener: ((View) -> Unit)? = null fun setOnClickListener(listener: (view: View) -> Unit) { onClickListener = listener } // later, to invoke onClickListener?.invoke(this)
這將使你可以重新使用 SAM 自動轉換實習的簡單的 lambda 語法。
使用協程代替 AnsyncTask
由於 AsyncTask 很笨重,而且往往會導致內存泄漏,因此我們更喜歡使用協程,因為它能夠改善代碼的可讀性同時不會導致內存泄露。這個資源8可以學習協程的基本概念和用法。需要注意的是,由於協程目前還是 Kotlin 1.1 版本中的實驗特性,在大多數的非同步場景中我們仍然推薦使用 RxJava9。
在本文寫作時,協程的使用需要引入額外的依賴庫:
並在 gradle.properties 文件中增加配置:
kotlin.coroutines=enable
通過使用協程,你可以寫出簡單的,內聯的非同步代碼,同時可以直接的方式修改 UI 界面。
fun bindUser() = launch(UI) { // Call to API or some other things that takes time val user = Api.getUser().execute() // continue doing things with the ui text.text = user.name }結論
以上是自從我們開始使用 Kotlin 進行開發以來,我們總結收集到的認為最有用的一些建議。在此特別感謝我的同事 Nathan Hillyer 的貢獻。我們希望這些建議能夠在你使用 Kotlin 開發 Android 時有一個好的開端。你也可以看看 Kotlin 的官方文檔10。Jake Wharton,來自 Square 公司的一名 Android 開發者,他也提供了一些關於 Kotlin 的有用的資源,其中包括他關於 Android 開發中使用 Kotlin 的演講11和筆記12。
期待隨著 Kotlin 作為一門編程語言不斷進化過程中產生更多的開發建議。
[1] https://kotlinlang.org/docs/reference/ ?
[2] https://try.kotlinlang.org/ ?
[3] https://kotlinlang.org/docs/reference/using-gradle.html#targeting-android ?
[4] https://github.com/orfjackal/retrolambda ?
[5] https://github.com/JakeWharton/butterknife ?
[6] https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions ?
[7] https://youtrack.jetbrains.com/oauth?state=%2Fissue%2FKT-7770 ?
[8] https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md?
[9] https://github.com/ReactiveX/RxJava ?
[10] https://kotlinlang.org/docs/reference/ ?
[11] https://www.youtube.com/watch?v=A2LukgT2mKc ?
[12] https://docs.google.com/document/d/1ReS3ep-hjxWA8kZi0YqDbEhCqTt29hG8P44aA9W0DM8/preview ?
※Vue的數據依賴實現原理簡析
※三個維度,解析產品的交互設計
※Angular 中自定義 toJSON 操作符
※图赏:继 Xplay6 库里定制版之后,vivo 又带来了 X9 NBA 定制版
※InnoDB undo日誌與歷史系統基礎
TAG:推酷 |
※Spring Boot與Kotlin使用Spring-data-jpa簡化數據訪問層
※Kotlin Android 環境搭建
※Google發布Android KTX預覽版,它能為Kotlin開發者做些什麼?
※Android開發者是時候轉向Kotlin了
※Spring Boot與Kotlin 使用MongoDB資料庫
※Kotlin打造Android路由框架
※Kotlin和Swift語言在Redmonk榜上排名大幅提升
※Gradle Kotlin DSL的accessors 生成問題
※Kotlin項目下的Retrofit2網路請求框架
※開發 iOS 應用,Kotlin Native 是否夠格?
※Kotlin 泛型
※Canonical宣布Kotlin編程語言Snap包格式上線
※谷歌發布 Android KTX 預覽版:提供相應 API 層,讓Kotlin開發更簡潔
※Kotlin 繼承
※Kotlin 類和對象
※Kotlin 威脅、Python 逆襲,2018 年程序員需要升級哪些技能?
※Kotlin 威脅、Python 逆襲,2018 年程序員需要升級哪些技能?
※Kotlin語言Web庫又添一虎將:Kweb
※Kotlin 介面
※Kotlin 擴展