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ù)用.
本文介紹如何用AndroidView
和AndroidViewBinding
.
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.