Android Weekly Notes #470

Android Weekly Issue #470

Navigating in Jetpack Compose

Jepack Compose的navigation.

文中提到的这个例子的navigation写得不错.

  • navigation的使用.
  • 代码原理.
  • 传参数.

目前还不支持transition.

还推荐了几个开源库:

The Story of My First A-ha Moment With Jetpack Compose

作者用Compose搞了一个数独app.

作者发现的优化方法是, 定义这么一个数据结构和接口:

data class SudokuCellData(
    val number: Int?,
    val row: Int,
    val column: Int,
    val attributes: Set<Attribute> = setOf(),
) {
    interface Attribute {
        @Composable
        fun Draw()
    }
}

然后:

interface Attribute {
     // Here we add the modifier argument, so that our composable can be modified from the outside
     @Composable
     fun Draw(modifier: Modifier = Modifier)
}

data class CenterValue(val values: Set<Int>) : SudokuCellData.Attribute {
    // We add the modifier to all the child classes
    @OptIn(ExperimentalUnitApi::class)
    @Composable
    override fun Draw(modifier: Modifier) {
        // Here we use "modifier" instaed of "Modifier" so that our current modifiers are added upon what was given.
        Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            Text(
                values.sorted().joinToString(""),
                modifier = Modifier.align(Alignment.Center),
                textAlign = TextAlign.Center,
                fontSize = TextUnit(9f, TextUnitType.Sp),

                )
        }
    }
}

用的时候只需要这样:

sudokuCellData.attributes.forEach {
    it.Draw()
}

Jetpack Compose Animations in Real Time

Jetpack Compose的动画.

https://github.com/halilozercan/madewithcompose/tree/main/dotsandlines/src/main/java/com/halilibo/dotsandlines

Create Your KMM Library

一些流行的KMM库:

  • SQLDelight
  • Decompose
  • Realm Kotlin Multiplatform SDK
  • Multiplatform Settings
  • Ktor

Gradle Plugin Tutorial for Android: Getting Started

创建一个gradle plugin.

Multiple back stacks

多个栈的导航.
View的例子:
https://github.com/android/architecture-components-samples/tree/master/NavigationAdvancedSample

Compose的例子:
https://github.com/chrisbanes/tivi

Create an application CoroutineScope using Hilt

创建一个Application的CoroutineScope:

手动:


class ApplicationDiContainer {
    val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
    val myRepository = MyRepository(applicationScope)
}

class MyApplication : Application() {
    val applicationDiContainer = ApplicationDiContainer()
}

用Hilt:


@InstallIn(SingletonComponent::class)
@Module
object CoroutinesScopesModule {

    @Singleton // Provide always the same instance 
    @Provides
    fun providesCoroutineScope(): CoroutineScope {
        // Run this code when providing an instance of CoroutineScope
        return CoroutineScope(SupervisorJob() + Dispatchers.Default)
    }
}

这里, hardcode dispatcher是一个不好的做法.

所以这里用@Qualifier提供了dispatchers:

@InstallIn(SingletonComponent::class)
@Module
object CoroutinesDispatchersModule {

    @DefaultDispatcher
    @Provides
    fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default

    @IoDispatcher
    @Provides
    fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

    @MainDispatcher
    @Provides
    fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main

    @MainImmediateDispatcher
    @Provides
    fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}

这里用ApplicationScope改善可读性:

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope

@InstallIn(SingletonComponent::class)
@Module
object CoroutinesScopesModule {

    @Singleton
    @ApplicationScope
    @Provides
    fun providesCoroutineScope(
        @DefaultDispatcher defaultDispatcher: CoroutineDispatcher
    ): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
}

在测试中替换实现:

// androidTest/projectPath/TestCoroutinesDispatchersModule.kt file

@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [CoroutinesDispatchersModule::class]
)
@Module
object TestCoroutinesDispatchersModule {

    @DefaultDispatcher
    @Provides
    fun providesDefaultDispatcher(): CoroutineDispatcher =
        AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()

    @IoDispatcher
    @Provides
    fun providesIoDispatcher(): CoroutineDispatcher =
        AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()

    @MainDispatcher
    @Provides
    fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}

Compose: List / Detail - Testing part 1

Jetpack Compose的UI Test.

Detect Configuration Regressions In An Android Gradle Build

Gradle build速度的改善.
有一些工具, 比如:
https://github.com/gradle/gradle-profiler

Run Custom Gradle Task After “build”

Learning State & Shared Flows with Unit Tests

StateFlow和SharedFlow都是hot的.

  • StateFlow: conflation, sharing strategies.
  • SharedFlow: replay and buffer emissions.

StateFlow

一个简单的单元测试:

val stateFlow = MutableStateFlow<UIState>(UIState.Success)

@Test
fun `should emit default value`() = runBlockingTest {
     stateFlow.test {
        expectItem() shouldBe UIState.Success
     }
}

这个会成功.

val stateFlow = MutableStateFlow<UIState>(UIState.Success)

  @Test
  fun `should emit default value`() = runBlockingTest {
       stateFlow.test {
          expectItem() shouldBe UIState.Success
          expectComplete()
       }
}

这个会失败, 因为StateFlow永远不会结束. 生产代码中onCompletion不会被执行.

单元测试中onCompletion会执行到是因为turbine会取消collect的协程.

val stateFlow = MutableStateFlow<UIState>(UIState.Success)

   @Test
   fun `should emit default value`() = runBlockingTest {
        stateFlow.emit(UIState.Error)
        stateFlow.test {
            expectItem() shouldBe UIState.Success
            expectItem() shouldBe UIState.Error
        }
    }
}

这个测试会失败是因为flow的conflated特性, 只有最新的value会被cache, 所以只能expect error item.

Code Scream每次collect结果都一样, 都会从头重新来一遍.

Hot Flow的值和它是否被观测无关. StateFlow和SharedFlow都是Hot Flow.

stateIn可以转冷为热.

SharedFlow

SharedFlow不需要默认值.

这个测试是通过的:

val sharedFlow = MutableSharedFlow<String>()

@Test
fun `collect from shared flow`() = runBlockingTest {
   val job = launch(start = CoroutineStart.LAZY) {
       sharedFlow.emit("Event 1")
   }

   sharedFlow.test {
       job.start()
       expectItem() shouldBeEqualTo "Event 1"
   }
}

这个测试会挂:

val sharedFlow = MutableSharedFlow<String>()

@Test
fun `collect from shared flow`() = runBlockingTest {
   sharedFlow.emit("Event 1")

   sharedFlow.test {
       expectItem() shouldBeEqualTo "Event 1"
   }
}

想要修好:

val sharedFlow = MutableSharedFlow<String>(replay = 1)

@Test
fun `collect from shared flow`() = runBlockingTest {
   sharedFlow.emit("Event 1")

   sharedFlow.test {
       expectItem() shouldBeEqualTo "Event 1"
   }
}

shareIn可以转冷为热.

Code

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352

推荐阅读更多精彩内容