Android Weekly Notes #481 Android Weekly Issue #481

Android Weekly Issue #481

Clean Code with Kotlin

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

利用Kotlin可以幫我們寫出更clean的代碼. 本文談到的方面:

  • 有意義的名字.
  • 可以更多使用immutability.
  • 方法.
  • high cohesion and loosed coupling. 一些軟件設計的原則.
  • 測試.
  • 註釋.
  • 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))

方法鏈接和聚合

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


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

這是一個擴展方法.

因爲Modifier是一個接口, 所以它支持了多種類型.

Modifier系統還使用了aggregation來聚合, 使得chaining能夠發生.

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

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的本質

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

作者試圖實現Compose版本的.
然後遇到了一些問題, 主要是Compose的動畫方式和以前不同, 需要思維轉變.

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

Kotlin’s Sealed Interfaces & The Hole in The Sealing

sealed interface是kotlin 1.5推出的.

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

object SuccessfulJourneyCertificate
object JourneyFailed

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

成功和失敗在同一個回調, 靠判斷null來判斷結果.

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

解決方案1: 提供兩個callback方法, 但是會帶來重複代碼.

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

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

Enum和sealed的區別:

  • sealed可以爲不同類型定義方法.
  • sealed更自由, 每種類型可以有不同的參數.

有了sealed class, 爲什麼要有sealed interface呢?

  • 爲了克服單繼承的限制.
  • 不同點1: 實現sealed interface的類不需要再在同一個文件中, 而是在同一個包中即可. 所以如果lint檢查有行數限制, 可以採用這種辦法.
  • 不同點2: 枚舉可以實現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類的實現, 我們的Color類不應該知道顏色被用於交通燈還是汽車顏色.

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

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

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

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

Kotlin delegated property for Datastore Preferences library

之前讀shared preferences然後轉成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這個類已經廢棄了, 應該用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終結操作符:

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
        }
    }
}

然後替換進原來的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)

這個文章做了一個遊戲瀏覽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: 圖像分割, 把主體和背景分隔開.

居然還有這麼一個網站: https://paperswithcode.com/task/semantic-segmentation
感覺是結合學術與工程的.

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

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

本文餘下部分講了demo實現.

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的技術重構.

他們的app取得成功之後, 要求feature快速開發, 因爲缺乏一個可擴展性的架構導致了很多問題, 其中可見的比如ANR和掉幀, 崩潰率, 緩慢.

他們經過觀察發現問題的發生在一個一個全局的State對象上.

這個技術棧不但導致了性能問題, 也導致了開發效率的降低, 所以他們內部決定停掉一切feature的開發, 整個team做這項重構, 叫做Android Reboot.

Introduction to Hilt in the MAD Skills series

MAD Skills系列的Hilt介紹.

Migrating to Compose - AndroidView

把App遷移到Compose, 勢必要用到AndroidView來做一些舊View的複用.

本文介紹如何用AndroidViewAndroidViewBinding.

Building Android Conversation Bubbles

Slack如何在Android 11上實現Conversation Bubbles.

文章的圖不錯.

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

KaMP Kit goes Jetpack Compose

KMP + Compose的sample.

Code

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章