我在“Now In Android”中学到的 9 件事
Now in Android是一款功能齐全的 Android 应用程序,完全使用 Kotlin 和 Jetpack Compose 构建。它遵循 Android 设计和开发最佳实践,旨在为开发人员提供有用的参考。
https://github.com/android/nowinandroid
UI 状态类
sealed interface InterestsUiState {
object Loading : InterestsUiState
data class Interests(
val authors: List<FollowableAuthor>,
val topics: List<FollowableTopic>
) : InterestsUiState
object Empty : InterestsUiState
}
此类旨在保存屏幕上显示的流数据,这些数据将随时间或事件发生变化,例如:调用 API 显示动作电影的流将需要视图来显示加载屏幕(以防请求花费太长时间)在显示查询结果之前,如果发现异常则显示错误屏幕。
在发现这一点之前,我是这样管理视图模型中屏幕上显示的流数据的:
var authors: LiveData<List<FollowableAuthor>> = MutableLiveData(emptyList())
var topics: LiveData<List<FollowableTopic>> = MutableLiveData(emptyList())
val isLoading = MutableLiveData(true)
val isEmpty: LiveData<Boolean> = authors.switchMap { authors ->
topics.map { topics ->
topics.isEmpty() && authors.isEmpty()
}
}
init {
viewModelScope.launch {
authors = authorsRepository.getAuthorsStream()
topics = topicsRepository.getTopicsStream()
}.invokeOnCompletion {
isLoading.value = false
}
}
现在代码看起来像这样:
val uiState = combine(
authorsRepository.getAuthorsStream(),
topicsRepository.getTopicsStream(),
) { availableAuthors, availableTopics ->
InterestsUiState.Interests(
authors = availableAuthors,
topics = availableTopics
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)
(在这个例子中,我从 LiveData 切换到 StateFlow,但实际上使用“asLiveData()”和“asFlow()”方法仍然很容易互换)
State holder & UI State
https://developer.android.com/topic/architecture/ui-layer/state-production#stream-apis
使用构造函数引用转换流
val myStream = combine(
followedAuthorIdsStream,
authorStream,
::Pair
)
而不是“手动”映射它…
val myStream =
combine(
followedAuthorIdsStream,
authorStream,
) { followedAuthorIds, author ->
Pair(followedAuthorIds, author)
}
“::Class”被称为构造函数引用。构造函数可以像方法和属性一样被引用。您可以在程序需要函数类型对象的任何地方使用它们,该对象采用与构造函数相同的参数并返回适当类型的对象。
轻松将其映射到密封接口结果中
return combine(
followedAuthorIdsStream,
authorStream,
::Pair
).asResult() // <- The change is here !
因为我们之前将我们的流配对成单个流,所以我们可以将它映射到一个会改变的结果类中,就像UIState
类一样,但是以一种更通用的方式,因为 Result.Success
将公开一个字段:数据。
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Throwable? = null) : Result<Nothing>
object Loading : Result<Nothing>
}
Flow
上的 Kotlin 扩展可自动映射到Result
中
fun <T> Flow<T>.asResult(): Flow<Result<T>> {
return this
.map<T, Result<T>> {
Result.Success(it)
}
.onStart { emit(Result.Loading) }
.catch { emit(Result.Error(it)) }
}
将其映射到 UI 状态类中
return combine(
followedAuthorIdsStream,
authorStream,
::Pair
).asResult()
.map { followedAuthorToAuthorResult ->
when (followedAuthorToAuthorResult) {
is Result.Success -> {
val (followedAuthors, author) = followedAuthorToAuthorResult.data
AuthorUiState.Success()
}
is Result.Loading -> AuthorUiState.Loading
is Result.Error -> AuthorUiState.Error
}
}
使用生命周期安全方法搜集状态
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
我正在使用 collectAsState()
并且没有注意到生命周期版本已经结束。
collectAsState()
方法的提醒:
@Composable
public fun <T> StateFlow<T>.collectAsState(
context: CoroutineContext
): State<T>
从此
StateFlow
收集值并通过State
表示其最新值。StateFlow.value
用作初始值。每次有新值发布到StateFlow
时,返回的State
都会更新,从而导致每个State.value
用法的重组。
Composable
中的方法引用
@Composable
fun MyScreen(viewModel: ForYouViewModel) {
ForYouScreen(
onTopicCheckedChanged = viewModel::updateTopicSelection,
onAuthorCheckedChanged = viewModel::updateAuthorSelection,
saveFollowedTopics = viewModel::saveFollowedInterests,
onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved,
)
}
这使代码更具可读性,特别是在这里,您的眼睛需要关注 UI。
调用参考:
@Composable
fun MyScreen(viewModel: ForYouViewModel) {
ForYouScreen(
onTopicCheckedChanged = { topicId, isChecked ->
viewModel.updateTopicSelection(topicId = topicId, isChecked = isChecked)
},
onAuthorCheckedChanged = { authorId, isChecked ->
viewModel.updateAuthorSelection(authorId = authorId, isChecked = isChecked)
},
saveFollowedTopics = { viewModel.saveFollowedInterests() },
onNewsResourcesCheckedChanged = { newsResourceId, isChecked ->
viewModel.updateNewsResourceSaved(
newsResourceId = newsResourceId,
isChecked = isChecked
)
},
)
}
多预览的注解
表示各种设备尺寸的多预览的注解。将此注解添加到Composable
以呈现各种设备。
@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480")
@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480")
@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480")
@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480")
annotation class DevicePreviews
Multipreview
注解表示不同设备尺寸的多个预览。可以将这个注解添加到Compose
中的某个组件上,以在不同设备上渲染多个预览界面。
Android允许您通过继承IssueRegistry来创建自定义lint规则,示例如下(我不会解释如何做,因为我在我的项目中还没有成功地让它正常工作):
class DesignSystemIssueRegistry : IssueRegistry()
在最新的Android版本中,他们创建了一个类来检查是否使用了他们自定义的组件而不是通用的组件。以下是他们编写的代码片段:
class DesignSystemDetector : Detector(), Detector.UastScanner {
...
val METHOD_NAMES = mapOf(
"MaterialTheme" to "NiaTheme",
"Button" to "NiaFilledButton",
"OutlinedButton" to "NiaOutlinedButton",
...
)
}
我发现这对不知道项目中已经存在特定组件的新开发人员很有用。
Composable 扩展
它只适用于 LazyListScope
、ColumnScope
……不适用于 @Composable
。
fun LazyListScope.MyItem() {
item {
// Your Composable
}
}
@Composable
fun MyList() {
LazyColumn {
MyItem()
}
}
我发现这很有用,例如:在多个屏幕上共享可组合的粘性标头。
先前:
@Composable
fun MyStickyHeader() {
// Composable
}
@Composable
fun MyList() {
LazyColumn {
stickyHeader {
MyStickyHeader()
}
}
}
之后:
fun LazyListScope.MyStickyHeader() {
stickyHeader {
// Composable
}
}
@Composable
fun MyList() {
LazyColumn {
MyStickyHeader()
}
}
参考链接
https://medium.com/@romeo.prosecco/top-things-i-learned-on-now-in-android-dba991da1c0