截至2026年,Now in Android(以下简称 NiA)依然是由 Google 官方维护、最能代表"现代 Android 应用架构"的标杆项目,GitHub 星标已突破 20K。
它不仅系统性地展示了 Jetpack 全家桶的最佳实践,更关键的是,提供了一整套面向中大型项目的可落地工程方案------涵盖模块化设计 、分层架构 、自动化测试 、性能分析 、代码提交、风格检查 以及团队协作 等核心环节,是 Android 应用开发者的必学项目。
考虑到NiA官方库为英文注释,此处提供一个NiA的中文翻译的库:github.com/linxu-link/...
本系列文章专为 Android 应用开发者打造,将以抓大放小的模式深入解析 Now in Android 的设计精髓,全系列共八章。
- 《Now in Android 现代应用开发实践(一):模块化设计》
- 《Now in Android 现代应用开发实践(二):架构设计(data+domain)》
- 《Now in Android 现代应用开发实践(三):架构设计(UI)》
- 《Now in Android 现代应用开发实践(四):构建逻辑》
- 《Now in Android 现代应用开发实践(五):代码规范》
- 《Now in Android 现代应用开发实践(六):质量保障体系》
- 《Now in Android 现代应用开发实践(七):Benchmark 性能测试》
- 《Now in Android 现代应用开发实践(八):Baseline 性能优化》
在《Now in Android现代应用开发实践》前两篇内容中,我们分别拆解了数据层 的"离线优先"设计、仓库模式与数据同步逻辑,以及领域层用例(Use Cases)对业务逻辑的封装与数据聚合能力。作为应用架构的"最后一公里",UI层是用户与应用交互的核心载体,其设计质量直接决定用户体验与代码可维护性。
Now in Android作为Google官方推出的架构示例应用,严格遵循《Android应用架构指南》中UI层的设计规范,本文将结合官方文档与应用源码,深度解析其UI层的设计思路、核心实现与最佳实践。
一、UI层的核心设计理念
在进入具体实现前,先明确Google官方为UI层定义的核心设计原则 - 这也是Now in Android UI层的设计基石:

1.1 单向数据流(UDF)
UI层遵循"单向数据流"(Unidirectional Data Flow)模式,核心规则可总结为:
- 数据自下而上流动:数据层/领域层的数据流经ViewModel转换为UI状态,最终驱动UI渲染;
- 事件自上而下传递:用户交互产生的事件(如点击、滑动)仅从UI层传递到ViewModel,再由ViewModel调用领域层/数据层完成业务处理;
- UI状态唯一可信源:所有UI状态均由底层数据驱动,UI本身不持有业务数据,仅负责渲染。
这种设计让数据流可追溯、状态变化可预测,大幅降低了复杂UI场景下的调试与维护成本。
1.2 状态驱动UI
UI层的核心目标是"基于状态渲染UI",而非"通过命令修改UI"。所有UI变化均由不可变的UI状态对象 触发,而非直接调用setText、setVisibility等传统命令式API------这也是Jetpack Compose的核心设计思想,与Now in Android的实现高度契合。
二、Now in Android UI层核心组件构成
Now in Android的UI层完全基于现代Android技术栈构建,核心组件分为两大块:
- Jetpack Compose
声明式UI框架,负责将UI状态转换为可视化界面,无XML布局,完全基于Kotlin实现。
- ViewModel
| 组件 | 核心角色 |
|---|---|
| Jetpack Compose | 声明式UI框架,负责将UI状态转换为可视化界面,无XML布局,完全基于Kotlin实现 |
| ViewModel | 状态持有者(State Holder),承接领域层/数据层的数据流,转换为UI状态;处理用户交互事件 |
注意:在新版的官方架构设计文档,ViewModel 与 Compose 共同构成了应用的视图层,这与旧版本的架构文档中将ViewModel单独分为一层有所不同。
ViewModel的职责:
在Now in Android中,ViewModel并非简单的"数据存储容器",而是承担了三大核心职责:
- 从领域层用例/数据层仓库接收冷流(Cold Flow)形式的原始数据;
- 将多源数据流合并、转换为单一的UI状态热流(StateFlow);
- 接收UI层的用户交互事件,调用领域层/数据层完成业务操作(如关注话题、收藏新闻)。
三、UI状态建模:密封层级结构+不可变设计
UI状态是UI层的核心抽象,Now in Android通过密封接口/密封类(Sealed Hierarchy)+ 不可变数据类 实现了极致的状态建模,确保状态的完整性与可预测性。
3.1 核心设计原则
- 不可变性:UI状态对象一旦创建不可修改,状态更新通过生成新对象实现;
- 穷举所有状态:通过密封结构定义UI的所有可能状态(如加载中、加载成功、加载失败),避免遗漏边缘场景;
- 单一职责:每个状态类仅承载对应场景的必要数据,无冗余信息。
3.2 实战案例:NewsFeedUiState("为你推荐"页面状态)
Now in Android的"为你推荐(For You)"页面是典型的列表页,其UI状态通过NewsFeedUiState密封接口建模:
kotlin
// 密封接口定义所有可能的UI状态
sealed interface NewsFeedUiState {
// 加载中状态:无附加数据
object Loading : NewsFeedUiState
// 加载成功状态:持有新闻资源列表(不可变List)
data class Success(
val newsResources: List<UserNewsResource>
) : NewsFeedUiState
// 扩展:可按需添加Error状态(示例中未体现,实际项目建议补充)
data class Error(val message: String) : NewsFeedUiState
}
设计优势分析
- 状态穷举 :通过密封接口强制覆盖所有可能的UI场景,Compose UI在渲染时可通过
when表达式无遗漏处理状态,避免空指针或未处理的边缘情况; - 不可变性 :
Success状态中的newsResources为不可变List,确保状态传递过程中不会被意外修改; - 语义清晰:每个状态的命名与数据结构直接反映业务场景,可读性与可维护性大幅提升。
3.3 Compose中处理密封状态
在ForYouScreen可组合函数中,通过when表达式处理所有状态,实现状态与UI的绑定:
kotlin
@Composable
fun ForYouScreen(
feedState: NewsFeedUiState,
onFollowTopic: (String, Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
when (feedState) {
is NewsFeedUiState.Loading -> {
// 渲染加载中UI:如Shimmer骨架屏
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
}
is NewsFeedUiState.Success -> {
// 渲染新闻列表
LazyColumn {
items(feedState.newsResources) { news ->
NewsItem(
news = news,
onFollowTopic = onFollowTopic
)
}
}
}
is NewsFeedUiState.Error -> {
// 渲染错误提示UI
Text(
text = feedState.message,
color = Color.Red,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
}
}
四、数据流转换:从冷流到UI状态热流
Now in Android的UI层数据流处理遵循"冷流接收 → 转换合并 → 热流暴露"的流程,核心目标是将数据层/领域层的"被动数据流"转换为UI层可实时监听的"主动状态流"。
4.1 核心概念:冷流(Flow)与热流(StateFlow)
- 冷流(Cold Flow) :数据层/领域层暴露的Flow(如
TopicsRepository::getTopics),仅在被收集(collect)时才会产生数据,无订阅时不消耗资源; - 热流(StateFlow) :ViewModel对外暴露的状态流,始终持有最新数据,即使无订阅也会保留状态,适合UI层实时监听。
4.2 Now in Android的实现流程(以InterestsViewModel为例)
InterestsViewModel负责"我的兴趣"页面的状态管理,其核心逻辑是将领域层的List<FollowableTopic>冷流转换为InterestsUiState热流:
步骤1:接收领域层冷流
kotlin
class InterestsViewModel(
private val getFollowableTopicsUseCase: GetFollowableTopicsUseCase,
private val userDataRepository: UserDataRepository
) : ViewModel() {
// 从领域层获取可关注话题的冷流
private val followableTopicsFlow: Flow<List<FollowableTopic>> = getFollowableTopicsUseCase()
}
步骤2:转换为UI状态并转为热流
通过map将原始数据转换为UI状态,再通过stateIn将冷流转为热流(StateFlow),指定协程作用域与初始状态:
kotlin
// 暴露给UI层的UI状态热流
val uiState: StateFlow<InterestsUiState> = followableTopicsFlow
.map { topics ->
// 将原始数据转换为UI状态
InterestsUiState.Interests(topics = topics)
}
.stateIn(
scope = viewModelScope, // ViewModel的协程作用域
started = SharingStarted.WhileSubscribed(5000), // 订阅策略:无订阅5秒后停止
initialValue = InterestsUiState.Loading // 初始状态
)
// 定义兴趣页面的UI状态密封接口
sealed interface InterestsUiState {
object Loading : InterestsUiState
data class Interests(val topics: List<FollowableTopic>) : InterestsUiState
}
关键API解析:stateIn
stateIn是Flow转为StateFlow的核心API,Now in Android使用SharingStarted.WhileSubscribed(5000)作为订阅策略,这是Google推荐的最佳实践:
- 当UI组件(如Compose可组合函数)取消订阅后,数据流会继续存活5秒;
- 若5秒内重新订阅,可复用现有数据流,避免重复请求;
- 若超过5秒无订阅,数据流停止,节省资源。
4.3 多源数据流合并(Combine)
当UI状态依赖多个数据源时,Now in Android使用combine合并数据流。例如"为你推荐"页面的ForYouViewModel,需要合并用户数据(UserData)与新闻资源(NewsResource)两个数据流:
scss
class ForYouViewModel(
private val getUserNewsResourcesUseCase: GetUserNewsResourcesUseCase
) : ViewModel() {
// 合并多源数据并转换为UI状态
val feedState: StateFlow<NewsFeedUiState> = getUserNewsResourcesUseCase()
.map { userNewsResources ->
NewsFeedUiState.Success(userNewsResources)
}
.onStart { emit(NewsFeedUiState.Loading) } // 开始收集时先发送Loading状态
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = NewsFeedUiState.Loading
)
}
注:GetUserNewsResourcesUseCase内部已通过combine合并了NewsRepository与UserDataRepository的数据流,确保仅当两个数据源均返回数据时才输出结果。
五、用户交互处理:事件传递与业务执行
UI层的交互事件(如点击、输入)遵循"UI → ViewModel → 领域层/数据层"的传递路径,Now in Android通过"Lambda回调 + 挂起函数"实现事件的优雅处理。
5.1 核心流程(以"关注话题"为例)
步骤1:UI层定义事件回调
在Compose可组合函数中,通过Lambda表达式接收ViewModel传递的事件处理函数:
kotlin
@Composable
fun InterestsScreen(
uiState: InterestsUiState,
onFollowTopic: (topicId: String, followed: Boolean) -> Unit,
modifier: Modifier = Modifier
) {
when (uiState) {
is InterestsUiState.Interests -> {
LazyColumn {
items(uiState.topics) { topic ->
TopicItem(
topic = topic,
onToggleFollow = { followed ->
// 触发关注/取消关注事件
onFollowTopic(topic.id, followed)
}
)
}
}
}
// 处理Loading状态...
}
}
步骤2:ViewModel实现事件处理逻辑
ViewModel接收UI层的事件,调用数据层仓库的挂起函数完成业务操作(如修改用户关注状态):
kotlin
class InterestsViewModel(
private val userDataRepository: UserDataRepository
) : ViewModel() {
// 暴露给UI层的事件处理函数
fun followTopic(topicId: String, followed: Boolean) {
viewModelScope.launch {
// 调用数据层仓库的挂起函数,修改用户数据
userDataRepository.toggleFollowedTopicId(topicId, followed)
}
}
}
步骤3:关联UI与ViewModel
在Activity/Fragment中,将ViewModel的事件处理函数传递给Compose UI:
kotlin
class InterestsActivity : ComponentActivity() {
private val viewModel: InterestsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NiaTheme {
InterestsScreen(
uiState = viewModel.uiState.collectAsStateWithLifecycle().value,
onFollowTopic = viewModel::followTopic
)
}
}
}
}
5.2 事件处理最佳实践
Google在《UI层事件处理指南》中强调了以下原则,Now in Android均严格遵循:
- 事件处理与状态更新分离:ViewModel仅负责处理事件(调用仓库/用例),状态更新由数据层的数据流自动触发,避免手动修改UI状态;
- 协程作用域管理 :所有异步操作均在
viewModelScope中执行,确保ViewModel销毁时自动取消,避免内存泄漏; - 无UI逻辑泄露:ViewModel不持有任何UI组件引用(如Context、Composable),仅通过状态与UI交互。
六、进阶思考:UI层的状态持有者(State Holders)
根据Google《UI层状态持有者指南》,ViewModel是最核心的状态持有者,但并非唯一选择。Now in Android的UI层设计体现了"分层状态持有"的思想:
- ViewModel:持有跨配置变更的全局UI状态(如新闻列表、用户兴趣);
- Compose State :持有局部UI状态(如输入框文本、弹窗显隐),通过
remember、mutableStateOf实现;
例如,在新闻详情页中,"收藏按钮的点击动画状态"属于局部UI状态,无需放入ViewModel,直接在Compose中持有:
kotlin
@Composable
fun NewsDetailItem(
news: UserNewsResource,
onBookmark: () -> Unit
) {
// 局部UI状态:收藏按钮的动画状态
val bookmarkAnimateState by remember { mutableStateOf(false) }
IconButton(
onClick = {
bookmarkAnimateState = true
onBookmark()
}
) {
Icon(
painter = if (news.isBookmarked) painterResource(id = R.drawable.ic_bookmark)
else painterResource(id = R.drawable.ic_bookmark_outline),
contentDescription = "Bookmark",
modifier = Modifier.animateScale(if (bookmarkAnimateState) 1.2f else 1f)
)
}
}
这种设计既保证了全局状态的可复用性,又避免了ViewModel被大量局部状态污染。
七、总结:Now in Android UI层的借鉴意义
Now in Android的UI层设计是Google官方架构指南的"落地范本",其核心价值体现在:
- 极致的状态驱动:通过密封接口+不可变数据类,让UI状态完全由底层数据驱动,消除"状态不一致"问题;
- 优雅的数据流管理:冷流转热流的标准化流程,兼顾性能与资源利用率;
- 清晰的职责边界:UI层仅负责渲染与事件传递,ViewModel仅负责状态转换与事件处理,无职责混淆;
- 适配现代技术栈:完全基于Compose+ViewModel构建,符合Android开发的未来趋势。
对于日常开发而言,可直接复用的实践要点:
- 采用密封结构建模UI状态,覆盖加载、成功、失败等所有场景;
- 使用
stateIn(SharingStarted.WhileSubscribed(5000))转换Flow为StateFlow; - 遵循单向数据流,事件处理通过"UI → ViewModel → 数据层"传递,状态更新由数据层自动触发;
- 区分全局状态(ViewModel持有)与局部状态(Compose State持有)。
UI层作为应用架构的"终端",其设计不仅要满足功能需求,更要兼顾可维护性与扩展性。Now in Android的实现为我们提供了一套可直接落地的参考方案,也是理解Google现代Android架构思想的最佳范例。
参考资料:
本文写作时使用的辅助AI模型:Grok 4.1 + Qwen3 Max
Now in Android 中文库:github.com/linxu-link/...