優雅地結合 Kotlin 特性封裝工具類
theme: smartblue
本文已參與「掘力星計劃」,贏取創作大禮包,挑戰創作激勵金。
前言
在 2019 年 Google I/O 大會上,Google 宣佈今後將優先採用 Kotlin 進行 Android 開發,並且也堅守了這一承諾。使用 Kotlin 進行 Android 開發程式碼更少,可讀性更強,並且能和 Java 程式碼相容。
我之前學習了一些 Kotlin 的語法糖之後,很想運用到自己整理的工具類上。當時公司只有我學習 Kotlin,所以專案是 Kotlin 和 Java 混編的。然後寫工具類時突然想到一個問題,我用 Kotlin 寫的工具類,呼叫的結果和原來 Java 工具類得到的結果不一致怎麼辦。比如正則,我用 Kotlin 寫的和別人用 Java 寫的匹配出來不一樣,那我不是要興師問罪。要麼就特意保證實現邏輯和 Java 工具類一致,這麼做的話為什麼不直接呼叫 Java 工具類呢?所以就給當時公司專案在用的 Java 工具類庫 AndroidUtilCode 封裝拓展庫。
由於絕大部分功能都實現好了,主要做的事是補充沒有的功能和設計一套好用的 Kotlin API。設計 API 看似很簡單,實際做起來很難。因為 Kotlin 的玩法實在太多了,而且並不是用了語法糖就一定會好用,用法騷會帶來一定的學習成本,也不一定好用。個人比較強迫症,在這方面思考了很多,有一些封裝的經驗可以分享給大家。
後來的公司新專案基本是 Kotlin 進行開發,可以不用考慮對 Java 程式碼的相容,就著手開始寫一個純 Kotlin 開發、儘可能輕量的 Kotlin 工具類庫。得益於之前的很多思考,目前實現的還是比較滿意的。
接下來給大家分享個人一些封裝 Kotlin 工具類的經驗和一個好用的 Kotlin 工具類庫。
封裝思路
我看過非常多人寫 Kotlin 的工具類,基本只是單純地把原來寫的 Java 工具類翻譯成 Kotlin 語言。這就像當初推出 C++ 後,有些人還是用面向過程的思想寫程式碼。並不是不能用,但是能做得更好用。
所以下面介紹的是部分 Java 不常見的語法糖和一些使用建議,幫助大家更好地在工具類使用這些特性。
Top-Level
這是 Kotlin 和 Java 一個比較大的差異,Java 的屬性和方法都需要寫在類裡的,而 Kotlin 有 top-level property
頂級屬性和 top-level function
頂級函式,可以把方法和屬性寫在類外面。top-level
顧名思義是最高級別的,可以理解為是全域性的,在別的類裡是能直接呼叫到頂級屬性或頂級方法。
有什麼用呢?比如獲取 Application 物件,Java 工具類是呼叫 AppUtils.getApplication()
來獲取,而 Kotlin 工具類可以直接獲取 application
屬性,能在任何的地方隨時獲取一個 application
屬性是非常爽的事情。我們能獲取一個 application
屬性的話,何必呼叫 AppUtils.getApplication()
呢。絕大多數情況用 Kotlin
寫一個 XXXUtils
去呼叫靜態方法都是多此一舉,明明寫成頂級屬性或頂級方法會更好用。
這雖然是一個很簡單的特性,但是也有地方要注意一下,就是命名要把功能描述清楚,個人認為很重要。比如之前寫了好一個沉浸式狀態列的功能,用法如下:
kotlin
StatusBarUtils.immerse(this)
把方法移到類的外面就能變成頂級方法:
kotlin
immerse(this)
有些人可能這樣改完就算了,但是這種情況是不好的,別人呼叫一個全域性的沉浸方法會很疑惑這是要沉浸什麼東西。之前能用“沉浸”的單詞作為方法名是因為工具類名也具有資訊,可以結合工具類的名稱推匯出是要沉浸狀態列。所以最好改成:
kotlin
immerseStatusBar(this)
頂級方法或頂級屬性的命名要把功能描述清楚,因為這是能全域性呼叫的,不要只是單純地把原有 Java 工具類的類名給去掉。
拓展
Kotlin 可以很方便的擴充套件一個已經存在的類,為它新增額外的方法或屬性,無需繼承類或者使用裝飾者模式。
我們可以進一步優化上面沉浸狀態列的用法,把方法改成 Activity 的拓展方法。在方法前面增加一個接收者:
kotlin
fun Activity.immerseStatusBar() {
...
}
在方法內能用 this 獲取到 Activity 物件,所以原本的 Activity 引數就可以去掉了。這樣就可以在 Activity 呼叫:
```kotlin class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... immerseStatusBar() } } ```
Java 想實現這個用法,需要在 Activity 基類裡寫一個 immerseStatusBar()
方法,而 Kotlin 能直接用拓展實現,無需寫基類。
其實這也是個很簡單的特性,不過個人也有些建議給到大家:
- 給 Any 或者常見的基礎資料型別進行擴充套件要慎重。
- 用法儘量符合原來的使用習慣或直覺,不要差異過大。
什麼意思呢?通過拓展能玩出一些騷操作,但並不是什麼功能都合適。比如我剛開始接觸拓展時,什麼功能都想用拓展來封裝,看到列印日誌要傳兩個引數挺麻煩的,就用拓展函式來減少一個引數,給 String
增加一個列印的拓展方法:
kotlin
"Downloaded progress is $progress".logd("http")
用法確實很騷,但是用了一段時間後覺得並不好用。用法與原來的列印日誌用法差異過大,寫得很彆扭。程式碼閱讀性變差了,要讀到末尾才知道是列印日誌,String 比較長的話可能沒反應過來這行是用來列印日誌的。而且在呼叫字串的方法時會彈出一個很讓人疑惑的程式碼提示。
還有看過別人給 Int 增加一個擴充套件屬性 drawableRes
獲取 Drawable,也是有類似的問題。
kotlin
val drawable = R.drawable.ic_back_icon_black.drawableRes
這兩個例子在功能上都是沒問題的,但是用法差異太大會降低程式碼閱讀性,需要不少時間來適應。還給常見的型別增加了奇怪的程式碼提示,個人是不提倡的。用法騷並不代表著好用,不要為了用語法糖而用語法糖。
當然也有提倡的騷用法,比如給 Int 增加 dp
屬性,將 dp 轉為 px,用法如下:
kotlin
paint.strokeWidth = 1.dp
雖然用法也是很大差異,但是符合直覺。我們讀這行程式碼能很容易想到是給屬性設定了 1 dp 的長度,程式碼可讀性反而更好了,這種用法是提倡的。
高階函式
高階函式是將函式用作引數或返回值的函式。後續會專門寫篇部落格講清楚高階函式的本質,這裡就不過多篇幅介紹怎麼去用了,來分享一個使用場景。多數人用高階函式是用於事件回撥,其實高階函式還能很方便地實現 DSL 用法。比如 Anko Layout
的 DSL:
kotlin
verticalLayout {
editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!") }
}
}
這樣的 DSL 用法比鏈式呼叫舒服一些,而且能分層級,這是鏈式呼叫不好實現的。
那要怎麼運用呢?其實有可選的配置都是可以考慮使用的,最常見的是建造者模式,比如我們很熟悉的 Glide:
kotlin
Glide.with(context)
.load(url)
.placeholder(placeholder)
.fitCenter()
.into(imageView)
我們稍微來封裝一下:
kotlin
fun ImageView.load(url: String?, block: RequestBuilder<Drawable>.() -> Unit) =
Glide.with(context).load(url).apply(block).into(this)
就這麼簡單地封裝就可以把鏈式呼叫轉為 DSL 用法。
kotlin
imageView.load(url) {
placeholder(placeholder)
fitCenter()
}
這樣用法就和 Coil 一樣了,不過還有些黃色警告需要處理,所以個人建議直接用 Coil。DSL 用法比鏈式呼叫更簡潔舒服一點,還能實現多級巢狀。
屬性委託
屬性委託是通過 by 關鍵字將屬性的 get、set 方法委託給 by 後面的表示式。比如:
kotlin
private val viewModel: LoginViewModel by viewModels()
這是官方的 ViewModel 委託用法,獲取 viewModel 屬性時會通過 ViewModelProvider 去獲得 ViewModel 例項。使用委託後我們不用管如何獲得 ViewModel 了,可以專注於寫邏輯程式碼。
怎麼去寫委託需要些篇幅來講,後續會專門寫篇部落格。這裡講一下運用場景,有的人可能學習過屬性委託,但是不知道在哪裡使用。其實我們在通過某種方式獲取或設定屬性時,就可以考慮一下使用屬性委託合不合適。比如通過 intent 獲取傳遞的值:
```kotlin private var id: String? = null
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val id = intent.getStringExtra("id") } ```
這裡可以通過屬性委託來簡化程式碼:
kotiln
private val id: String? by intentExtras("id")
屬性委託能讓我們不用管如何獲得和設定屬性,程式碼更加簡潔,是個不錯的語法糖,可以多思考一下是否適合用屬性委託。
其它經驗
不重複造輪子
比如顯示隱藏需要呼叫 view.visibily = View.GONE
略顯繁瑣的,所以有些人會封裝拓展函式 view.visible()
view.invisible()
、view.gone()
快速實現顯示隱藏。
用起來確實比之前方便了一些,但是還有優化空間,顯示隱藏經常是有個判斷操作的,比如:
kotlin
if (isShowed) {
view.visible()
} else {
view.gone()
}
每次都要這麼判斷稍顯麻煩,所以更優地封裝方式是增加一個 view.isVisible
的 Boolean 值的擴充套件屬性,這樣上面的程式碼就會優化成下面的寫法:
kotlin
view.isVisible = isShowed
這個擴充套件屬性不僅能用於修改顯示隱藏狀態,還能判斷當前是否顯示在佈局上,用起來更加方便。
不過在封裝完呼叫該拓展屬性時,你會發現有重名的屬性需要選擇用哪一個,仔細一看原來官方的 core-ktx
庫已經實現這個擴充套件屬性,我們沒必要再重複造輪子。
所以封裝工具類之前最好先了解一下 Android 官方 ktx 庫和 Kotlin 的標準庫有沒實現相同的功能,我們封裝的工具類的定位應該是對沒有的功能進行補充。 重複造輪子沒有意義,而且造出來的輪子可能還不如官方的。
命名建議
上面說了我們應該是補充官方庫沒有的功能,那麼設計用法時也建議參考一下官方庫的命名和用法。
比如帶引數的建立操作,官方通常會用 listOf()
、mapOf()
等 xxxOf()
的命名,建議與官方統一,不推薦用 createXXX()
或者 newXXX()
等命名。
還有監聽事件的方法命名有些人喜歡命名為 onXXX
,比如:
kotlin
btnLogin.onClick {
// ...
}
這樣直接用介詞開頭挺奇怪的,一般方法名是動詞開頭。所以個人建議參考官方的命名 doOnXXX
:
kotlin
view.doOnAttach {
// ...
}
與官方庫的命名規則進行統一的好處是不容易產生歧義,而且別人可能會根據以往的使用習慣,去猜想你的工具類會不會有某個功能。比如想看下有沒有某個監聽事件,可能會先敲個 do
看下有沒對應功能方法的聯想。所以個人建議不要增加太多個人的命名規則,多參考學習一下官方庫的命名和用法。
最終方案
上述的經驗主要是分享給一些自己有在寫 Kotlin 工具類的小夥伴,而更多的人是不太會寫的,所以這裡分享一個我個人打磨了很久的 Kotlin 工具類庫 —— Longan。
為什麼叫 Longan
?個人想用個水果名來作為庫名,最初想到的是 Guava
(石榴),感覺非常合適,但是發現有一個谷歌的同名庫,所以換了個也是多子的水果 Longan
(龍眼)。
新增依賴:
groovy
allprojects {
repositories {
// ...
maven { url 'http://www.jitpack.io' }
}
}
groovy
dependencies {
implementation 'com.github.DylanCaiCoding.Longan:longan:1.0.0'
// 可選
implementation 'com.github.DylanCaiCoding.Longan:longan-design:1.0.0'
}
保留和改進了一些 Anko
好用的用法,例如:
kotlin
startActivity<SomeOtherActivity>("id" to 5)
logDebug(5)
toast("Hi there!")
snackbar(R.string.message)
alert("Hi, I'm Roy", "Have you tried turning it off and on again?")
還有增加了很多開發常用的功能,比如下面的一些用法:
在需要 Context 或 Activity 的時候,可直接獲取 application
或 topActivity
屬性。
用較少的程式碼實現 TabLayout + ViewPager2 的自定義樣式的底部導航欄:
```kotlin private val titleList = listOf(R.string.home, R.string.shop, R.string.mine) private val iconList = listOf( R.drawable.bottom_tab_home_selector R.drawable.bottom_tab_shop_selector, R.drawable.bottom_tab_mine_selector )
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
viewPager2.adapter = FragmentStateAdapter(HomeFragment(), ShopFragment(), MineFragment())
tabLayout.setupWithViewPager2(viewPager2, enableScroll = false) { tab, position ->
tab.setCustomView(R.layout.layout_bottom_tab) {
findViewById
建立帶引數的 Fragment,在 Fragment 內通過屬性委託獲取引數:
```kotlin class SomeFragment : Fragment() { private val viewModel: SomeViewModel by viewModels() private val id: String by safeArguments(KEY_ID)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) //... viewModel.loadData(id) }
companion object { fun newInstance(id: String) = SomeFragment().withArguments(KEY_ID to id) } } ```
kotlin
val fragment = SomeFragment.newInstance(id)
一行程式碼實現雙擊返回鍵退出 App 或者點選返回鍵不退出 App 回到桌面:
kotlin
pressBackTwiceToExitApp("再次點選退出應用")
// pressBackToNotExitApp()
實現沉浸式狀態列,並且給標題欄的頂邊距增加狀態列高度,可以適配劉海水滴屏:
kotlin
immerseStatusBar()
toolbar.addStatusBarHeightToMarginTop()
// toolbar.addStatusBarHeightToPaddingTop()
快速實現獲取驗證碼的倒計時:
kotlin
btnSendCode.startCountDown(this,
onTick = {
text = "${it}秒"
},
onFinish = {
text = "獲取驗證碼"
})
設定按鈕在某些輸入框都有內容時才能點選:
kotlin
btnLogin.enableWhenOtherTextNotEmpty(edtAccount, edtPwd)
點選事件可以設定的點選間隔,防止一段時間內重複點選:
kotlin
btnLogin.doOnClick(clickIntervals = 500) {
// ...
}
自定義控制元件獲取自定義屬性的程式碼比較多,進行了簡化:
kotlin
withStyledAttrs(attrs, R.styleable.CustomView) {
textSize = getDimension(R.styleable.CustomView_textSize, 12.sp)
textColor = getColor(R.styleable.CustomView_textColor, getCompatColor(R.color.text_normal))
icon = getDrawable(R.styleable.CustomView_icon) ?: getCompatDrawable(R.drawable.default_icon)
iconSize = getDimension(R.styleable.CustomView_iconSize, 30.dp)
}
自定義控制元件繪製居中或者垂直居中的文字:
kotlin
canvas.drawCenterText(text, centerX, centerY, paint)
canvas.drawCenterVerticalText(text, centerX, centerY, paint)
切換到主執行緒,用法與 thread {...}
保持了統一:
kotlin
mainThread {
// ...
}
監聽生命週期操作:
kotlin
lifecycleOwner.doOnLifecycle(
onCreate = {
// ...
},
onDestroy = {
// ...
}
)
在 RecyclerView 資料為空的時候自動顯示一個空佈局:
kotlin
recyclerView.setEmptyView(this, emptyView)
RecyclerView 的 smoothScrollToPosition()
方法是滑動到 item 可見,如果從上往下滑會停在底部,一般不符合需求。所以增加了個始終滑動到頂部位置的拓展方法。
kotlin
recyclerView.smoothScrollToStartPosition(position)
每次判斷 TextView 文字是否不為空要寫 textView.text.toString().isNotEmpty()
特別長,對此進行了簡化:
kotlin
if (textView.isTextNotEmpty()) {
// ...
}
訊息事件傳遞推薦 KunMinX 大佬的方案,用共享 ViewModel 持有的 LiveData 進行分發,避免訊息推送難以溯源、訊息同步不可靠不一致等問題。由於 LiveData 存在依賴倒灌的問題,一般會自行封裝 EventLiveData 用於事件的場景。但是不考慮 Java 的話,直接用協程的 SharedFlow 就行。
kotlin
class SharedViewModel : ViewModel() {
val saveNameEvent = MutableSharedFlow<String>()
}
通過 by applicationViewModels()
獲取 Application 級別的 ViewModel,實現共享 ViewModel:
```kotlin private val sharedViewModel: SharedViewModel by applicationViewModels()
// 傳送事件 sharedViewModel.saveNameEvent.tryEmit(name)
// 監聽事件,提供了類似 LiveData 的 observe 用法,簡化 collect 的程式碼 sharedViewModel.saveNameEvent.launchAndCollectIn(this) { finish() } ```
還有很多好用的 API,比如 Android 10 分割槽儲存適配需要增刪查改媒體檔案的 uri,能簡化很多程式碼,這裡就不一一介紹了。更多的用法請檢視 GitHub,目前已有超過 300 個常用方法或屬性,可以大大提高開發效率。
個人會長期維護,有任何問題都可以提 issues,我會盡快去處理。有什麼想要的功能也可以提。
往期講解封裝的文章
- 《優雅地封裝和使用 ViewBinding,該替代 Kotlin synthetic 和 ButterKnife 了》
- 《 ViewBinding 巧妙的封裝思路,還能這樣適配 BRVAH 》
- 《優雅地封裝 Activity Result API,完美地替代 startActivityForResult()》
總結
本文講了 Top-Level、擴充套件、高階函式、委託等一些 Kotlin 特性的使用建議,還有分享了一些個人工具類的經驗。在最後分享了個人打磨了非常久的 Kotlin 工具類庫 —— Longan,已有超過 300 個常用方法或屬性,能有效提高開發效率,推薦現在用 Kotlin 開發的小夥伴來使用一下。
如果您覺得有幫助的話,希望能點個 star 支援一下喲 ~ 我後面會分享更多封裝相關的文章給大家。
- 如何更好地進行 Android 元件化開發(三)ActivityResult 篇
- 如何更好地進行 Android 元件化開發(五)路由原理篇
- 如何更好地進行 Android 元件化開發(四)登入攔截篇
- 如何更簡潔地實現富文字 Span
- 2022 年中總結|我的 GitHub 竟然被微軟大佬關注了?!
- 手把手帶你實現西瓜影片的責任鏈埋點框架
- 優雅地結合 Kotlin 特性深度解耦標題欄
- 更多 ViewBinding 的封裝思路,適配 BRVAH 竟如此簡單
- 2021 年終總結,GitHub 1k stars 的目標終於達成!!
- 優雅地結合 Kotlin 特性封裝工具類
- 優雅地封裝和使用 ViewBinding,該替代 Kotlin synthetic 和 ButterKnife 了