Android Weekly Issue #481
Clean Code with Kotlin
如何衡量代碼質(zhì)量?
一個非官方的方法是wtfs/min.
利用Kotlin可以幫我們寫出更clean的代碼. 本文談到的方面:
- 有意義的名字.
- 可以更多使用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ò)展方法.
因為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
作者試圖實現(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呢?
- 為了克服單繼承的限制.
- 不同點1: 實現(xiàn)sealed interface的類不需要再在同一個文件中, 而是在同一個包中即可. 所以如果lint檢查有行數(shù)限制, 可以采用這種辦法.
- 不同點2: 枚舉可以實現(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類的實現(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)
這個文章做了一個游戲瀏覽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實現(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ā), 因為缺乏一個可擴(kuò)展性的架構(gòu)導(dǎo)致了很多問題, 其中可見的比如ANR和掉幀, 崩潰率, 緩慢.
他們經(jīng)過觀察發(fā)現(xiàn)問題的發(fā)生在一個一個全局的State對象上.
這個技術(shù)棧不但導(dǎo)致了性能問題, 也導(dǎo)致了開發(fā)效率的降低, 所以他們內(nèi)部決定停掉一切feature的開發(fā), 整個team做這項重構(gòu), 叫做Android Reboot.
Introduction to Hilt in the MAD Skills series
MAD Skills系列的Hilt介紹.
Migrating to Compose - AndroidView
把App遷移到Compose, 勢必要用到AndroidView來做一些舊View的復(fù)用.
本文介紹如何用AndroidView和AndroidViewBinding.
Building Android Conversation Bubbles
Slack如何在Android 11上實現(xiàn)Conversation Bubbles.
文章的圖不錯.
websocket的資料:
https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
KaMP Kit goes Jetpack Compose
KMP + Compose的sample.