Android Weekly Notes

Android Weekly Notes #481

2021-10-13  本文已影响0人  圣骑士wind

Android Weekly Issue #481

Clean Code with Kotlin

如何衡量代码质量?
一个非官方的方法是wtfs/min.

利用Kotlin可以帮我们写出更clean的代码. 本文谈到的方面:

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有四个必要的组成部分:

然后作者用这个同样的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 class, 为什么要有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()

这个解决方案:

迁移到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

上一篇下一篇

猜你喜欢

热点阅读