個人開源庫的一些更新,兼談Jetpack和Kotlin給Android開發(fā)帶來的變化

peter-luo-oQeUMKp2LEo-unsplash (1).jpg

前段時間,我開發(fā)完成了新的軟件 移動工具箱。最近,我準備把開發(fā)過程中總結的一些東西沉淀到自己個人開源的幾個庫中。最新的一些更新中運用了 Kotlin 和 Jetpack 的一些語法特性,故此總結一下。Jetpack 和 Kotlin 出來已經(jīng)很久了,然而很多應用開發(fā)還停留在 MVP 以及 Java 階段,即便使用了 Kotlin,很多人只不過是像使用 Java 一樣在使用 Kotlin. 實際上,如果能夠結合 Kotlin 和 Jetpack 的語法特性,可以大大提升我們日常開發(fā)的效率。下面,我以個人開源庫的一些新的 Feature 的更新來說明,Jetpack 帶來的新變化以及 Kotlin 的特性的運用。

1、Android-VMLib

這個庫的地址是:https://github.com/Shouheng88/Android-VMLib

1.1 新的數(shù)據(jù)交互設計:隱藏 LiveData

其實早在之前的文章中我也提及過這個庫 《2020 年,我這樣在項目中使用 MVVM》。不過,在新的版本中我又做了一些改動。首先是將應用依賴的一些三方庫提升到了最新版本。其次是,在 BaseActivity 和 BaseFragment 中,我又增加了幾個方法,

// 新增觀察方法,ui 層通過指定 class, flag 和 single 直接對數(shù)據(jù)進行監(jiān)聽
protected fun <T> observe(dataType: Class<T>,
                          flag: Int? = null,
                          single: Boolean = false,
                          success: (res: Resources<T>) -> Unit = {},
                          fail: (res: Resources<T>) -> Unit = {},
                          loading: (res: Resources<T>) -> Unit = {}) {
    vm.getObservable(dataType, flag, single).observe(this, Observer { res ->
        when (res?.status) {
            Status.SUCCESS -> success(res)
            Status.LOADING -> loading(res)
            Status.FAILED -> fail(res)
        }
    })
}

其實之前的文章中,我也提到過這個問題。我們通過自定義的枚舉將 ui 層和 ViewModel 的交互劃分為 “成功”、“失敗” 和 “加載中” 三種狀態(tài),然后在 ui 層根據(jù)狀態(tài)做判斷來分別處理各種狀態(tài)。那么,既然這樣,我們?yōu)槭裁床粚⒚總€狀態(tài)寫成一個方法分別進行回調呢?當然,這里我們使用 Kotlin 的函數(shù)式編程更好地實現(xiàn)了這個目標。并且,我們?yōu)槊總€函數(shù)指定了一個默認的為空的實現(xiàn),這樣用戶只需要根據(jù)自己的需要實現(xiàn)指定的狀態(tài)即可。

另外,在 ViewModel 中我增加了下面幾個方法,

// 頂層 viewmodel 新增方法,包裝了三種狀態(tài)的交互
fun <T> setSuccess(dataType: Class<T>, flag: Int? = null, single: Boolean = false, data: T) {
    getObservable(dataType, flag, single).value = Resources.success(data)
}

fun <T> setLoading(dataType: Class<T>, flag: Int? = null, single: Boolean = false) {
    getObservable(dataType, flag, single).value = Resources.loading()
}

fun <T> setFailed(dataType: Class<T>, flag: Int? = null, single: Boolean = false, code: String?, message: String?) {
    getObservable(dataType, flag, single).value = Resources.failed(code, message)
}

其實也就是將之前獲取 LiveData 的邏輯包裝了一層,然后將三個狀態(tài)分成三個方法。所以,現(xiàn)在 ui 和 ViewModel 的交互變成了下面這樣,

VMLib新交互手稿.jpg

就是說,UI 層通過調用 ViewModel 的方法進行請求(圖中①所示流程),當 ViewModel 完成了請求之后通過調用 setSuccess(), setLoading()setFailed() 方法來把狀態(tài)通過 LiveData 傳遞給 UI 層。UI 層通過 observe() 方法進行監(jiān)聽。這里我們使用 Class、flag 和 Boolean 來共同決定一個 LiveData. 具體來說,Class 自己可以決定 LiveData,這里的 flag 用來區(qū)別 Class 相同的情況。比如,一篇文章有標題和內容,都是 Stirng 類型,如何區(qū)分呢?此時就需要使用 flag 來做區(qū)分。這里的 Boolean 用來定義 “一次性的” LiveData,所謂的 “一次性” 就是指,通知了一次之后就不會再進行第二次通知了。

用一段代碼來展示一下交互邏輯,

VMLib新交互代碼示范.png

這里 ui 層通過 ViewModel 的 requestFirstPage() 發(fā)起請求,ViewModel 中通過 setSuccess(), setLoading()setFailed() 方法來把狀態(tài)通過 LiveData 傳遞給 UI 層。UI 使用 observe() 方法進行監(jiān)聽,不過這里只處理了 “成功” 和 “失敗” 兩種狀態(tài)。這種新的開發(fā)方式代碼量要比之前少了許多,甚至ui層只需要一行代碼就可以完成注冊監(jiān)聽的邏輯。

在這里我們做的工作就是把 LiveData 這個概念隱藏了起來。但通過 Class 來尋找 LiveData 的開發(fā)方式還是比較新穎的,當然也許會有更好的辦法來隱藏掉 LiveData 從而更好地實現(xiàn)注冊和通知的邏輯。所以,可以說 Jetpack 的 LiveData 可能會給 Android 開發(fā)方式帶來一次革新。不過,筆者認為 Android 的 MVVM 實現(xiàn),仍然有所欠缺:雖然 Google 官方提供了 Jetpack 和 Databinding 的一套東西來實現(xiàn) MVVM 架構,但是相比于 Vue 等的處理方式,Android 的實現(xiàn)方式要繁瑣得多。首先,為人詬病的是 Databinding 的編譯速度問題,其次 Android 進行數(shù)據(jù)綁定的時候變量必須先聲明再使用,這并沒有使代碼變得更簡潔。此外,雖然 Android 提供了 style 來定義控件的屬性,但是 style 無法提供像前端的選擇器那樣豐富的功能,也就無法像前端那樣的樣式、邏輯和布局分離。這就導致了使用 xml 寫的布局非常冗長,而進一步在 xml 中做數(shù)據(jù)綁定,會使得 xml 變得更加繁冗。

這里還有一點要說的就是 Kotlin 的函數(shù)式編程,從上面也可以看出,我們的 observe() 方法中通過定義三個函數(shù)來完成狀態(tài)回調,而在 Java 中只能使用接口來進行回調。Java 使用接口來實現(xiàn)的函數(shù)式編程并不像 Kotiln 一樣徹底,即便是將接口回調的方法用到 Kotlin 中也不如 Kotlin 的函數(shù)式編程優(yōu)雅。所以,新的版本中,我也將部分類遷移到了 Kotlin 上面以充分使用 Kotlin 的特性。

2、Android-Utils

這個庫的地址是:https://github.com/Shouheng88/Android-utils

2.1 新增了一波方法

首先,這個庫中增加了許多新的方法,比如在代碼中生成 Drawable 的方法,可以幫我們在代碼中實現(xiàn)主題兼容,因為在自定義的 drawable.xml 文件中使用 ?attr 在低版本兼容的問題,我們可以將部分邏輯通過代碼來實現(xiàn),

// https://github.com/Shouheng88/Android-utils/blob/master/utils/src/main/java/me/shouheng/utils/ui/ImageUtils.java
public static Drawable getDrawable(@ColorInt int color,
                                   float topLeftRadius,
                                   float topRightRadius,
                                   float bottomLeftRadius,
                                   float bottomRightRadius,
                                   int strokeWidth,
                                   @ColorInt int strokeColor) {
    GradientDrawable drawable = new GradientDrawable();
    drawable.setColor(color);
    drawable.setStroke(strokeWidth, strokeColor);
    drawable.setCornerRadii(new float[]{
            topLeftRadius, topLeftRadius,
            topRightRadius, topRightRadius,
            bottomRightRadius, bottomRightRadius,
            bottomLeftRadius, bottomLeftRadius
    });
    return drawable;
}

另外就是對 Drawable 進行著色的邏輯。這可以減少代碼中重復類型的資源,也可以更好地實現(xiàn)應用的主題兼容,

// https://github.com/Shouheng88/Android-utils/blob/master/utils/src/main/java/me/shouheng/utils/ui/ImageUtils.java
public static Drawable tintDrawable(Drawable drawable, @ColorInt int color) {
    final Drawable wrappedDrawable = DrawableCompat.wrap(drawable.mutate());
    DrawableCompat.setTintList(wrappedDrawable, ColorStateList.valueOf(color));
    return wrappedDrawable;
}

此外,還增加了各種進制之前轉換的邏輯、震動以及獲取應用的信息等各種方法。

2.2 Kotlin 的詭計:使用 Kotlin 進行拓展

另外一個比較大的更新是新增了一個 module: utils-ktx,它使用 Kotlin 的新特性對工具類做了進一步的包裝讓調用更加簡潔。當然,不是每個人都會接受這么做,所以,我將其劃分為一個單獨的 module,并且生成一個單獨的依賴,用戶可以自由進行選擇。

utils-ktx 的新增的功能可以用兩個關鍵字來描述:賦能和全局函數(shù)。賦能就是為原有的類增加新的方法,比如 String 和 Bitmap,

// String 類新增的一些方法
fun String.isSpace(): Boolean = StringUtils.isSpace(this)
fun String.isEmpty(): Boolean = StringUtils.isEmpty(this)
// ...
// Bitmap 新增的一些方法
fun Bitmap.toBytes(format: Bitmap.CompressFormat): ByteArray = ImageUtils.bitmap2Bytes(this, format)
fun Bitmap.toDrawable(): Drawable = ImageUtils.bitmap2Drawable(this)
// ...

另一個就是全局函數(shù)。我們之前通過全局 Context 獲取資源已經(jīng)簡化了資源獲取的邏輯,現(xiàn)在進一步優(yōu)化之后,只需要調用下面的方法就可以獲取資源,

@ColorInt fun colorOf(@ColorRes id: Int): Int = ResUtils.getColor(id)
fun stringOf(@StringRes id: Int): String = ResUtils.getString(id)
fun stringOf(@StringRes id: Int, vararg formatArgs: Any): String = ResUtils.getString(id, *formatArgs)
fun drawableOf(@DrawableRes id: Int): Drawable = ResUtils.getDrawable(id)
// ...

所以,當我們希望獲取一個 Drawable 并根據(jù)主題對其進行著色的時候,直接使用下面的代碼就可以完成:

iv.icon = drawableOf(R.drawable.ic_add_circle).tint(Color.WHITE)

再比如,當我們想要在 Activity 中請求系統(tǒng)的存儲權限的時候,只需要使用下面一行代碼就可以完成,

checkStoragePermission { 
    // ... 添加請求到權限之后的邏輯 
}

當然,這樣做固好,只是有點比較諷刺的是,以新增的 String.isSpace() 方法為例,我們新增方法的邏輯實際是通過 StringUtils.isSpace(this) 來完成的。而所謂的新增方法,實際上也就是在編譯之后將新增方法的類的實例作為靜態(tài)方法的第一個參數(shù)進行編譯。我們這樣做就相當于,代碼中轉了過來,而編譯之后又轉了回去。只是代碼看上去簡單了一些,而犧牲的代價是無端多了一些編譯的產(chǎn)物。

另外一個需要注意的地方是,Kotlin 固然靈活,但如果不加以約束會使得項目變得混亂。比如,這里的新增方法和全局方法,都是全局性質的,一個模塊中引入了這些依賴就具有了這些新的特性。如果每個人都隨意往代碼中增加類似的方法,那很顯然會出現(xiàn)各種方法和代碼沖突。所以,如果僅僅是把 Kotlin 當 Java 用還好,但新特性的使用必須加以約束。

Kotlin 的靈活性是把雙刃劍,不僅局限在上述新增方法的情形中。我在開發(fā)中還遇到過使用 Kotlin 定義數(shù)據(jù)庫對象的坑,這里也捎帶介紹下。我在項目中使用 Room 作為數(shù)據(jù)庫,Room 會在編譯時根據(jù)數(shù)據(jù)庫對象自動生成創(chuàng)建數(shù)據(jù)庫 SQL. 這就涉及到了一個空類型判斷的問題。我們知道數(shù)據(jù)庫的列是分為 NULLABLE 和非 NULLABLE 的。Room 會在編譯時比較創(chuàng)建數(shù)據(jù)庫的 Schema,發(fā)現(xiàn) Schema 不一致,即便是空約束不一致,就會要求你做做數(shù)據(jù)遷移,不然就拋異常,而 SQLite 的列更新,一項比較繁瑣,需要先刪后增。以下面的數(shù)據(jù)庫對象為例,

@Entity
data class Repository(
    @PrimaryKey(autoGenerate = true) var id: Int?,
    var username: String,
)

假如你在定義對象的時候直接將 username 字段定義成了 String 類型的,那么你慘了!這意味著你就必須在代碼中保證 username 不為 null,一旦為 null,就會拋出 KotlinNull 的異常。而某天,你突然發(fā)現(xiàn)了這個問題想改成 String? 類型的,對不起,只能做數(shù)據(jù)庫遷移。因為列的定義已經(jīng)從 not null 的變成 nullable 的了。我覺得造成這個問題的一個原因可能是我們習慣了 Java 的開發(fā),Java 默認 String 是 nullable 的。Kotlin 中的 String 和 Java 中長得一樣,但是含義完全不同。

所以,還是那個問題,Kotlin 雖然靈活,但是也是有坑的!

3、Android-UIX

這個庫的地址是:https://github.com/Shouheng88/Android-uix

3.1 再次吐槽 Kotlin

這個庫是一套 ui 的合集,我設計它的目的是用來做一套標準的 ui 庫,除了常用的控件之外,我還希望它能夠將部分頁面作為控件對外暴露,從而簡化我開發(fā)程序的工作量。但是,這個庫自從開發(fā)出來一直沒有對外宣揚,一個原因是,我覺得用了 Kotlin 的一些特性之后,這讓我有些苦惱。

BeautyDialog 為例,這是 Android 對話框的封裝類,我使用了 Kotlin 進行開發(fā),并且使用構建者模式進行構建。讓我反感的是我覺得在 Kotlin 中使用構建者模式看上去非常蠢!Kotlin 提供了許多特性來讓我們直接通過 “字段引用” 的方式為實例賦值。使用構建者模式之后就完全體現(xiàn)不了 Kotlin 的這種優(yōu)雅性。另外,我一般會選擇使用一些 IDEA 插件來輔助生成構建者模式需要的代碼,然而在 Kotlin 中無法使用,必須手動做這類低級的工作。

另外,我通常使用自定義的注解來取代枚舉,但是在 Kotlin 中,將這種自定義的枚舉應用到 when 語句的時候失去了它的檢查和 “提醒” 機制,使我不得不看一下枚舉的定義才能知道該使用那個整數(shù)。

此外,使用 Kotlin 的其他特性,比如為方法指定默認參數(shù)等,這增加了代碼的靈活性的同時也給人帶來不少困擾。以上面的構建者為例,如果把方法的默認參數(shù)當作一次賦值,那么實際上為類實例的一個字段賦值的時候存在了多種可能性——可能是方法默認參數(shù)的值、可能是構建者字段的默認值、可能是類實例字段的默認值。選擇多了,困惑也多了。

以上還僅僅局限于 Kotlin 調用 Kotlin 的情況。如果 Java 中調用 Kotlin 的方法呢,那么以上的 Kotlin 的特性,包括接口的默認方法在 Java 中表現(xiàn)又當如何呢?所以,如果僅僅是做業(yè)務開發(fā),使用 Kotlin 可以清爽許多,但如果把 Kotlin 應用到類庫的開發(fā)中的話,這種靈活性就變成了一把雙刃劍。

不過,Kotlin 的一些特性也確實能讓我們更好地實現(xiàn)一些功能,比如下面這個防止連續(xù)點擊的應用:

// 一個自定義的 OnClickListener
abstract class NoDoubleClickListener : View.OnClickListener {

    private var lastClickTime: Long = 0

    override fun onClick(v: View) {
        val currentTime = System.currentTimeMillis()
        if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) {
            lastClickTime = currentTime
            onNoDoubleClick(v)
        }
    }

    protected abstract fun onNoDoubleClick(v: View)

    companion object {
        var MIN_CLICK_DELAY_TIME                       = 500L
    }
}

// 為 View 添加方法,以后使用這個方法來替換 setOnClickListener 就可以了
fun View.onDebouncedClick(click: (view: View) -> Unit) {
    setOnClickListener(object : NoDoubleClickListener() {
        override fun onNoDoubleClick(v: View) {
            click(v)
        }
    })
}

其實,NoDoubleClickListener 這個抽象類已經(jīng)可以幫助我們實現(xiàn)防止連續(xù)點擊的目的,但是使用了 Kotlin 的特性之后,我們可以為 View 增加一個方法并且使用 Kotlin 的函數(shù)式編程,于是像下面這樣就可以實現(xiàn)防止連續(xù)點擊了,簡潔得多了吧:

btnRateIntro.onDebouncedClick {
    // do something
}

3.2 OpenCV 開發(fā)環(huán)境

在這個類庫中,我最近新增了一個 module,即 uix-image 模塊。這個模塊內搭建了 OpenCV 的開發(fā)環(huán)境并且也包含了 OpenCV 的拓展庫。在我的應用 移動工具箱 中,我用這個模塊實現(xiàn)了包括透視變換、鏡像翻轉、浮雕等十幾種圖像效果。開源的代碼中只實現(xiàn)了兩種基本的特效,以及裁剪的邏輯。如果你想要了解在 Android 里面使用 OpenCV 的話,也可以直接使用這個代碼哦~

3.3 一點遐想

我現(xiàn)在個人開發(fā)項目的時候一般還是會直接使用 BRVAH 當作項目內的 Adapter. 不得不說,這個庫真的好用。每當我開發(fā)自己的項目的時候,總是不由得想起前同事們還在使用原生的 Adapter 做開發(fā)。一個簡單的布局要寫大量的代碼,然后不由得贊嘆一聲,BRVAH 確實好用。然而進來我發(fā)現(xiàn),通過使用泛型等進行簡單包裝之后,甚至可以不用處處聲明 Adapter,

我們可以定義一個工具方法如下,

object AdapterHelper {

    fun <ITEM> getAdapter(@LayoutRes itemLayout:Int,
                          converter: ViewHolderConverter<ITEM>,
                          data: List<ITEM>): Adapter<ITEM>
            = Adapter(itemLayout, converter, data)

    interface ViewHolderConverter<ITEM> {
        fun convert(helper: BaseViewHolder, item: ITEM)
    }

    class Adapter<ITEM>(
        @LayoutRes private val layout: Int,
        private val converter: ViewHolderConverter<ITEM>,
        val list: List<ITEM>
    ): BaseQuickAdapter<ITEM, BaseViewHolder>(layout, list) {
        override fun convert(helper: BaseViewHolder, item: ITEM) {
            converter.convert(helper, item)
        }
    }
}

// 一個自定義的方法
fun BaseViewHolder.goneIf(@IdRes id: Int, goneIf: Boolean) {
    this.getView<View>(id).visibility = if (goneIf) View.GONE else View.VISIBLE
}

然后,每當我們需要獲取 Adapter 的時候只需要像下面這樣即可,

adapter = AdapterHelper.getAdapter(R.layout.item_device_info_item,
    object : AdapterHelper.ViewHolderConverter<InfoItem> {
        override fun convert(helper: BaseViewHolder, item: InfoItem) {
            helper.setText(R.id.tv1, item.name)
            helper.goneIf(R.id.tv_name, TextUtils.isEmpty(item.name))
        }
    }, emptyList())

按照上面這樣的思路,我們甚至可以直接隱藏 Adapter 的概念,直接通過 id 和屬性綁定的方法來實現(xiàn)一個數(shù)據(jù)列表。寫了這么多年的 Android,不論 ListView 還是 RecyclerView 都存在的 Adapter 概念也許就此成為了歷史~

不得不感嘆,Android 開發(fā)確實已經(jīng)比較成熟了~

總結

以上各個代碼庫均是開源的,可以到我的 Github 上面通過源碼做進一步了解。除了跟技術相關的東西,偶爾我也會寫點跟技術無關的東西。感興趣的可以關注我的公眾號~

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容