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