Android Weekly Notes #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的复用.
本文介绍如何用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.