美文网首页Android Weekly Notes
Android Weekly Notes #470

Android Weekly Notes #470

作者: 圣骑士wind | 来源:发表于2021-07-31 00:59 被阅读0次

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

相关文章

网友评论

    本文标题:Android Weekly Notes #470

    本文链接:https://www.haomeiwen.com/subject/yjdhvltx.html