Android Weekly Notes #481

Android Weekly Issue #481

Clean Code with Kotlin

如何衡量代碼質(zhì)量?
一個非官方的方法是wtfs/min.

利用Kotlin可以幫我們寫出更c(diǎn)lean的代碼. 本文談到的方面:

  • 有意義的名字.
  • 可以更多使用immutability.
  • 方法.
  • high cohesion and loosed coupling. 一些軟件設(shè)計的原則.
  • 測試.
  • 注釋.
  • code review.

Build Function Chains Using Composition in Kotlin

Compose的Modifier讓我們可以通過連接方法的方式無限疊加效果:

// f(x) -> g(x) -> h(x) 
Modifier.width(10.dp).height(10.dp).padding(start = 8.dp).background(color = Color(0xFFFFF0C4))

方法鏈接和聚合

寫一個普通的類如何達(dá)到這種效果呢?
一個簡單的想法可能是返回這個對象:


fun changeOwner(newName: String) : Car {
    this.ownerName = newName
    return this
}

fun repaint(newColor: String) : Car {
    this.color = newColor
    return this
}

這種雖然管用, 但是不支持多種類型, 也不直觀.

Modifier是咋做的呢, 一個例子:

fun Modifier.fillMaxWidth(fraction: Float = 1f) : Modifier

這是一個擴(kuò)展方法.

因?yàn)?code>Modifier是一個接口, 所以它支持了多種類型.

Modifier系統(tǒng)還使用了aggregation來聚合, 使得chaining能夠發(fā)生.

Kotlin的fold()允許我們聚合操作, 在所有動作都執(zhí)行完成后收集結(jié)果.

fold的用法:

// starts folding with initial value 0
// aggregates operation from left to right 

val numbers = listOf(1,2,3,4,5)
numbers.fold(0) { total, number -> total + number}

fold是有方向的:

val numbers = listOf(1,2,3,4,5)

// 1 + 2 -> 3 + 3 -> 6 + 4 -> 10 + 5 = 15
numbers.fold(0) { total, number -> total + number}

// 5 + 4 -> 9 + 3 -> 12 + 2 -> 14 + 1 = 15
numbers.foldRight(0) {total, number -> total + number}

Compose UI modifiers的本質(zhì)

compose modifiers有四個必要的組成部分:

  • Modifier接口
  • Modifier元素
  • Modifier Companion
  • Combined Modifier

然后作者用這個同樣的pattern寫了car的例子:
https://gist.github.com/PatilSiddhesh/a5f415907aca8eb4f971238533bf2cf1

Using AdMob banner Ads in a Compose Layout

Google AdMob: https://developers.google.com/admob/android/banner?hl=en-GB

本文講了如何把它嵌在Compose的UI中.

Jetpack Compose Animations Beyond the State Change

這個loading庫:
https://github.com/HarlonWang/AVLoadingIndicatorView

作者試圖實(shí)現(xiàn)Compose版本的.
然后遇到了一些問題, 主要是Compose的動畫方式和以前不同, 需要思維轉(zhuǎn)變.

這里還有一個animation的代碼庫:
https://github.com/touchlab-lab/compose-animations

Kotlin’s Sealed Interfaces & The Hole in The Sealing

sealed interface是kotlin 1.5推出的.

舉例, 最原始的代碼, 一個callback, 兩個參數(shù):

object SuccessfulJourneyCertificate
object JourneyFailed

fun onJourneyFinished(
    callback: (
         certificate: SuccessfulJourneyCertificate?,
         failure: JourneyFailed?
    ) -> Unit
) {
    // Save callback until journey has finished
}

成功和失敗在同一個回調(diào), 靠判斷null來判斷結(jié)果.

那么問題來了: 如果同時不為空或者同時為空, 代表什么意思呢?

解決方案1: 提供兩個callback方法, 但是會帶來重復(fù)代碼.

解決方案2: 加一個sealed class JourneyResult, 還是用同一個回調(diào)方法.

但是如果我們的情況比較多, 比如有5種成功的情況和4種失敗的情況, 我們就會有9種case.

Enum和sealed的區(qū)別:

  • sealed可以為不同類型定義方法.
  • sealed更自由, 每種類型可以有不同的參數(shù).

有了sealed class, 為什么要有sealed interface呢?

  • 為了克服單繼承的限制.
  • 不同點(diǎn)1: 實(shí)現(xiàn)sealed interface的類不需要再在同一個文件中, 而是在同一個包中即可. 所以如果lint檢查有行數(shù)限制, 可以采用這種辦法.
  • 不同點(diǎn)2: 枚舉可以實(shí)現(xiàn)sealed interface.

比如:

sealed interface Direction

enum class HorizontalDirection : Direction {
    Left, Right
}

enum class VerticalDirection : Direction {
    Up, Down
}

什么時候sealed interface不是一個好主意呢?
一個不太好的例子:

sealed interface TrafficLightColor
sealed interface CarColor

sealed class Color {
    object Red:    Color(), TrafficLightColor, CarColor
    object Blue:   Color(), CarColor
    object Yellow: Color(), TrafficLightColor
    object Black:  Color(), CarColor
    object Green:  Color(), TrafficLightColor
    // ...
}

為什么不好呢?
違反了開閉原則, 我們修改了Color類的實(shí)現(xiàn), 我們的Color類不應(yīng)該知道顏色被用于交通燈還是汽車顏色.

這樣很快就會失控.
每次我們要引入sealed interface的時候, 都要問自己, 新引入的這個接口, 是同等或更高層的抽象嗎.

對于Traffic light更好的解決方案可能是這樣:

enum class TrafficLightColor(
    val colorValue: Color
) {
    Red(Color.Red),
    Yellow(Color.Yellow),
    Green(Color.Green)
}

這樣我們就不需要修改原來的Color模塊, 而是在其外面擴(kuò)展功能, 就符合了開閉原則.

Kotlin delegated property for Datastore Preferences library

之前讀shared preferences然后轉(zhuǎn)成flow的代碼:

//Listen app theme mode (dark, light)
private val selectedThemeChannel: ConflatedBroadcastChannel<String> by lazy {
    ConflatedBroadcastChannel<String>().also { channel ->
        channel.trySend(selectedTheme)
    }
}

private val changeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
    when (key) {
        PREF_DARK_MODE_ENABLED -> selectedThemeChannel.trySend(selectedTheme)
    }
}

val selectedThemeFlow: Flow<String>
    get() = selectedThemeChannel.asFlow()

這個解決方案:

  • 引入了一些中間類型.
  • ConflatedBroadcastChannel這個類已經(jīng)廢棄了, 應(yīng)該用StateFlow.

遷移到data store之后變成了這樣:

//initialization with extension
private val dataStore: DataStore<Preferences> = context.dataStore

val selectedThemeFlow = dataStore.data
    .map { it[stringPreferencesKey(name = "pref_dark_mode")] }

這段代碼:

enum class Theme(val storageKey: String) {
    LIGHT("light"),
    DARK("dark"),
    SYSTEM("system")
}

private const val PREF_DARK_MODE = "pref_dark_mode"

private val prefs: SharedPreferences = context.getSharedPreferences("PREFERENCES_NAME", Context.MODE_PRIVATE)

var theme: String
    get() = prefs.getString(PREF_DARK_MODE, SYSTEM.storageKey) ?: SYSTEM.storageKey
    set(value) {
        prefs.edit {
            putString(PREF_DARK_MODE, value)
        }
    }

可以用delegate property改成:

class StringPreference(
    private val preferences: SharedPreferences,
    private val name: String,
    private val defaultValue: String
) : ReadWriteProperty<Any, String?> {

    @WorkerThread
    override fun getValue(thisRef: Any, property: KProperty<*>) =
        preferences.getString(name, defaultValue) ?: defaultValue

    override fun setValue(thisRef: Any, property: KProperty<*>, value: String?) {
        preferences.edit {
            putString(name, value)
        }
    }
}

使用的時候:

var theme by StringPreference(
    preferences = prefs,
    name = "pref_dark_mode",
    defaultValue = SYSTEM.storageKey
)

Data Store的API沒有提供讀單個值的方法, 所有都是通過flow.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}

文章用了first終結(jié)操作符:

The terminal operator that returns the first element emitted by the flow and then cancels flow’s collection. Throws NoSuchElementException if the flow was empty.

所以寫了拓展方法:

fun <T> DataStore<Preferences>.get(
    key: Preferences.Key<T>,
    defaultValue: T
): T = runBlocking {
    data.first()[key] ?: defaultValue
}

fun <T> DataStore<Preferences>.set(
    key: Preferences.Key<T>,
    value: T?
) = runBlocking<Unit> {
    edit {
        if (value == null) {
            it.remove(key)
        } else {
            it[key] = value
        }
    }
}

然后替換進(jìn)原來的delegates里:

class PreferenceDataStore<T>(
    private val dataStore: DataStore<Preferences>,
    private val key: Preferences.Key<T>,
    private val defaultValue: T
) : ReadWriteProperty<Any, T> {

    @WorkerThread
    override fun getValue(thisRef: Any, property: KProperty<*>) =
        dataStore.get(key = key, defaultValue = defaultValue)

    override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
        dataStore.set(key = key, value = value)
    }
}

代碼庫: https://github.com/egorikftp/Lady-happy-Android

Learn with code: Jetpack Compose — Lists and Pagination (Part 1)

這個文章做了一個游戲?yàn)g覽app, 用的api是這個:
https://rawg.io/apidocs

對于列表的顯示, 用的是LazyVerticalGrid, 并且用Paging3做了分頁.

圖像加載用的是Coil: https://coil-kt.github.io/coil/compose/

最后還講了ui測試.

Realtime Selfie Segmentation In Android With MLKit

image segmentation: 圖像分割, 把主體和背景分隔開.

居然還有這么一個網(wǎng)站: https://paperswithcode.com/task/semantic-segmentation
感覺是結(jié)合學(xué)術(shù)與工程的.

ML Kit提供了自拍背景分離:
https://developers.google.com/ml-kit/vision/selfie-segmentation

作者的所有文章:
https://gist.github.com/shubham0204/94c53703eff4e2d4ff197d3bc8de497f

本文余下部分講了demo實(shí)現(xiàn).

Interfaces and Abstract Classes in Kotlin

Kotlin中的接口和抽象類.

Do more with your widget in Android 12!

Android 12的widgets, 可以在主屏顯示一個todo list.

Sample code: https://github.com/android/user-interface-samples/tree/main/AppWidget

Performance and Velocity: How Duolingo Adopted MVVM on Android

Duolingo的技術(shù)重構(gòu).

他們的app取得成功之后, 要求feature快速開發(fā), 因?yàn)槿狈σ粋€可擴(kuò)展性的架構(gòu)導(dǎo)致了很多問題, 其中可見的比如ANR和掉幀, 崩潰率, 緩慢.

他們經(jīng)過觀察發(fā)現(xiàn)問題的發(fā)生在一個一個全局的State對象上.

這個技術(shù)棧不但導(dǎo)致了性能問題, 也導(dǎo)致了開發(fā)效率的降低, 所以他們內(nèi)部決定停掉一切feature的開發(fā), 整個team做這項(xiàng)重構(gòu), 叫做Android Reboot.

Introduction to Hilt in the MAD Skills series

MAD Skills系列的Hilt介紹.

Migrating to Compose - AndroidView

把App遷移到Compose, 勢必要用到AndroidView來做一些舊View的復(fù)用.

本文介紹如何用AndroidViewAndroidViewBinding.

Building Android Conversation Bubbles

Slack如何在Android 11上實(shí)現(xiàn)Conversation Bubbles.

文章的圖不錯.

websocket的資料:
https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API

KaMP Kit goes Jetpack Compose

KMP + Compose的sample.

Code

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子棘捣,更是在濱河造成了極大的恐慌腮介,老刑警劉巖芯义,帶你破解...
    沈念sama閱讀 211,496評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剂桥,死亡現(xiàn)場離奇詭異凯旭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)芥喇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,187評論 3 385
  • 文/潘曉璐 我一進(jìn)店門西采,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人继控,你說我怎么就攤上這事械馆。” “怎么了武通?”我有些...
    開封第一講書人閱讀 157,091評論 0 348
  • 文/不壞的土叔 我叫張陵霹崎,是天一觀的道長。 經(jīng)常有香客問我冶忱,道長尾菇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,458評論 1 283
  • 正文 為了忘掉前任朗和,我火速辦了婚禮错沽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘眶拉。我一直安慰自己,他們只是感情好憔儿,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,542評論 6 385
  • 文/花漫 我一把揭開白布忆植。 她就那樣靜靜地躺著,像睡著了一般谒臼。 火紅的嫁衣襯著肌膚如雪朝刊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,802評論 1 290
  • 那天蜈缤,我揣著相機(jī)與錄音拾氓,去河邊找鬼。 笑死底哥,一個胖子當(dāng)著我的面吹牛咙鞍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播趾徽,決...
    沈念sama閱讀 38,945評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼续滋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了孵奶?” 一聲冷哼從身側(cè)響起疲酌,我...
    開封第一講書人閱讀 37,709評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后朗恳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體湿颅,經(jīng)...
    沈念sama閱讀 44,158評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,502評論 2 327
  • 正文 我和宋清朗相戀三年粥诫,在試婚紗的時候發(fā)現(xiàn)自己被綠了肖爵。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,637評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡臀脏,死狀恐怖劝堪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情揉稚,我是刑警寧澤秒啦,帶...
    沈念sama閱讀 34,300評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站搀玖,受9級特大地震影響余境,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜灌诅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,911評論 3 313
  • 文/蒙蒙 一芳来、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧猜拾,春花似錦即舌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,744評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盯仪,卻和暖如春紊搪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背全景。 一陣腳步聲響...
    開封第一講書人閱讀 31,982評論 1 266
  • 我被黑心中介騙來泰國打工耀石, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人爸黄。 一個月前我還...
    沈念sama閱讀 46,344評論 2 360
  • 正文 我出身青樓滞伟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親馆纳。 傳聞我的和親對象是個殘疾皇子诗良,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,500評論 2 348

推薦閱讀更多精彩內(nèi)容