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()
anddistinctUntilChanged()
.
爲了克服這些侷限性, 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"
}