Android Weekly Notes #470 Android Weekly Issue #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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章