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"
}
网友评论