Android Weekly Notes #482 Android Weekly Issue #482

Android Weekly Issue #482

Kotlin’s Flow in ViewModels: it’s complicated

我們的目標

UI數據加載要考慮的問題:

  • 1.緩存: 已經加載的數據應該可以直接顯示, 而不是需要二次加載.
  • 2.避免後臺工作: 當UI不可見時, 所有後臺工作都應該被取消.
  • 3.在configuration change的時候工作不會被中斷.

ViewModel用來實現1和3, LiveData用來實現2和3.

LiveData以及改進

LiveData的侷限性:

  • 只有主線程操作.
  • 只有3種轉換操作符. map(), switchMap() and distinctUntilChanged().

爲了克服這些侷限性, Jetpack提供了一些bridges, 比如androidx.lifecycle:lifecycle-livedata-ktx中的coroutine builder:

val result: LiveData<Result> = liveData {
    val data = someSuspendingFunction()
    emit(data)
}
  • 這段代碼會根據生命週期自動取消(目標2).
  • 取消動作會延遲5秒, 如果新的activity立即取代, 則不會取消(目標3).
  • 只有值變了纔會重新restart(目標1).

如果repository返回的是流, 則可以這樣做:

val result: LiveData<Result> = someFunctionReturningFlow().asLiveData()

其內部其實就是collect了一下:

fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
    collect {
        emit(it)
    }
}

Flow

  • Flow, SharedFlow和StateFlow.
  • StateFlow和LiveData.

lifecycle:lifecycle-runtime-ktx:2.4.0推出的收集方法:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.result.collect { data ->
            displayResult(data)
        }
    }
}

或者是:

viewLifecycleOwner.lifecycleScope.launch {
    viewModel.result
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect { data ->
            displayResult(data)
        }
}

後面又討論瞭如何避免重播最新的value.

Jetpack Compose navigation architecture with ViewModels

在使用Compose的navigation時, 作者建議把導航的代碼從UI中抽取出來:

class Navigator {

    private val _sharedFlow = 
      MutableSharedFlow<NavTarget>(extraBufferCapacity = 1)
    val sharedFlow = _sharedFlow.asSharedFlow()

    fun navigateTo(navTarget: NavTarget) {
        _sharedFlow.tryEmit(navTarget)
    }

    enum class NavTarget(val label: String) {

        Home("home"),
        Detail("detail")
    }
}

導航代碼:

fun NavigationComponent(
  navController: NavHostController, 
  navigator: Navigator
) {
    LaunchedEffect("navigation") {
        navigator.sharedFlow.onEach {
            navController.navigate(it.label)
        }.launchIn(this)
    }
    
    NavHost(
        navController = navController,
        startDestination = NavTarget.Home.label
    ) {
        ...
    }
}

Coroutines under the hood

協程的內部工作原理.

有很多種選擇來實現掛起函數, Kotlin用的是: continuation-passing style

Jetpack Compose way to animate Android Views

Compose結合Android View的動畫.

文章中有流程圖.

代碼: https://github.com/andreymusth/stateful-animations

Enabling cache & offline support on Android using Room

利用Room實現離線模式.

有精細的時序圖.

Understanding re-composition in Jetpack Compose with a case study

理解recompose.

問題來源: 有一段本該不recompose的代碼recompose了, 爲何.

@Composable
fun CounterRow(counter: Int, onButtonClick: () -> Unit) {
    /** SHOULD NOT BE CALLED ON SLIDER CHANGE **/
    Row(modifier = Modifier.fillMaxWidth()) {
        Button(onClick = onButtonClick) {
            Text(text = "Click me!")
        }
        Spacer(modifier = Modifier.width(24.dp))
        Text(text = counter.toString())
    }
}

這段代碼recompose了, 引起變化的居然是第二個參數, lambda.

關於compose的lifecycle的文檔:
https://developer.android.com/jetpack/compose/lifecycle

原因就是當state變化時, lambda其實被重建了:

ComposeStateTestTheme {
    val state: MainState by viewModel.state.collectAsState()
    MainScaffold(
        state,
        onValueUpdate = { viewModel.updateSlider(it.roundToInt()) },
        onButtonClick = { viewModel.updateCounter() }
    )
}

解決方法就是移出去:

setContent {
    val state: MainState by viewModel.state.collectAsState()
    val onButtonClick = { viewModel.updateCounter() }
    ComposeStateTestTheme {
        MainScaffold(
            state,
            onValueUpdate = { viewModel.updateSlider(it.roundToInt()) },
            onButtonClick = onButtonClick
        )
    }
}

或者使用方法引用:

setContent {
    ComposeStateTestTheme {
        val state: MainState by viewModel.state.collectAsState()
        MainScaffold(
            state,
            onValueUpdate = { viewModel.updateSlider(it.roundToInt()) },
            onButtonClick = viewModel::updateCounter
        )
    }
}

Basic Drag-n-Drop in Jetpack Compose

Compose中的拖拽換位.

在Roadmap中寫了: Support Drag and Drop: https://developer.android.com/jetpack/androidx/compose-roadmap

但是目前, 作者用現有的api實現了一個版本:
https://gist.github.com/surajsau/f5342f443352195208029e98b0ee39f3

Android Drag and Drop Tutorial

基於Android View的拖拽教程.

Principles and Techniques for Effective Localization

國際化設計和實現要考慮的種種方面.

Hilt Testing Best Practices

Hilt在測試中的應用.

Jetpack Compose: Building Grids

在Compose中構建Grid.

A Bit of Gradle Housekeeping

gradle中已經可以清理掉的幾個東西:

android {
    buildToolsVersion "30.0.3"
}
android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
android {
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

以前這樣寫:

android {
    compileSdkVersion 31

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 31
    }
}

現在可以改成這樣:

android {
    compileSdk 31

    defaultConfig {
        minSdk 21
        targetSdk 31
    }
}

還有:

sourceSets.all {
    it.java.srcDir "src/$it.name/kotlin"
}

和:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

Code

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