Now in Android 现代应用开发实践(三):架构设计(UI)

截至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状态对象 触发,而非直接调用setTextsetVisibility等传统命令式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并非简单的"数据存储容器",而是承担了三大核心职责:

  1. 从领域层用例/数据层仓库接收冷流(Cold Flow)形式的原始数据;
  2. 将多源数据流合并、转换为单一的UI状态热流(StateFlow);
  3. 接收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
}

设计优势分析

  1. 状态穷举 :通过密封接口强制覆盖所有可能的UI场景,Compose UI在渲染时可通过when表达式无遗漏处理状态,避免空指针或未处理的边缘情况;
  2. 不可变性Success状态中的newsResources为不可变List,确保状态传递过程中不会被意外修改;
  3. 语义清晰:每个状态的命名与数据结构直接反映业务场景,可读性与可维护性大幅提升。

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合并了NewsRepositoryUserDataRepository的数据流,确保仅当两个数据源均返回数据时才输出结果。


五、用户交互处理:事件传递与业务执行

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均严格遵循:

  1. 事件处理与状态更新分离:ViewModel仅负责处理事件(调用仓库/用例),状态更新由数据层的数据流自动触发,避免手动修改UI状态;
  2. 协程作用域管理 :所有异步操作均在viewModelScope中执行,确保ViewModel销毁时自动取消,避免内存泄漏;
  3. 无UI逻辑泄露:ViewModel不持有任何UI组件引用(如Context、Composable),仅通过状态与UI交互。

六、进阶思考:UI层的状态持有者(State Holders)

根据Google《UI层状态持有者指南》,ViewModel是最核心的状态持有者,但并非唯一选择。Now in Android的UI层设计体现了"分层状态持有"的思想:

  • ViewModel:持有跨配置变更的全局UI状态(如新闻列表、用户兴趣);
  • Compose State :持有局部UI状态(如输入框文本、弹窗显隐),通过remembermutableStateOf实现;

例如,在新闻详情页中,"收藏按钮的点击动画状态"属于局部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官方架构指南的"落地范本",其核心价值体现在:

  1. 极致的状态驱动:通过密封接口+不可变数据类,让UI状态完全由底层数据驱动,消除"状态不一致"问题;
  2. 优雅的数据流管理:冷流转热流的标准化流程,兼顾性能与资源利用率;
  3. 清晰的职责边界:UI层仅负责渲染与事件传递,ViewModel仅负责状态转换与事件处理,无职责混淆;
  4. 适配现代技术栈:完全基于Compose+ViewModel构建,符合Android开发的未来趋势。

对于日常开发而言,可直接复用的实践要点:

  • 采用密封结构建模UI状态,覆盖加载、成功、失败等所有场景;
  • 使用stateIn(SharingStarted.WhileSubscribed(5000))转换Flow为StateFlow;
  • 遵循单向数据流,事件处理通过"UI → ViewModel → 数据层"传递,状态更新由数据层自动触发;
  • 区分全局状态(ViewModel持有)与局部状态(Compose State持有)。

UI层作为应用架构的"终端",其设计不仅要满足功能需求,更要兼顾可维护性与扩展性。Now in Android的实现为我们提供了一套可直接落地的参考方案,也是理解Google现代Android架构思想的最佳范例。


参考资料:

  1. Android官方UI层架构指南
  2. UI层状态持有者最佳实践
  3. UI层事件处理指南
  4. Now in Android官方源码

本文写作时使用的辅助AI模型:Grok 4.1 + Qwen3 Max

Now in Android 中文库:github.com/linxu-link/...

相关推荐
Coolmuster_cn2 小时前
永久擦除您的 Android
android
我命由我123452 小时前
Android 开发 - UriMatcher(一个 URI 分类器)
android·java·java-ee·kotlin·android studio·android-studio·android runtime
阿拉斯攀登2 小时前
第 13 篇 输入设备驱动(触摸屏 / 按键)开发详解,Linux input 子系统全解析
android·linux·运维·驱动开发·rk3568·瑞芯微·rk安卓驱动
学习3人组3 小时前
Workerman实现 WSS 基于客户端 ID 的精准推送
android·java·开发语言
阿拉斯攀登3 小时前
第 11 篇 RK 平台安卓驱动实战 4:I2C 设备驱动开发,以 OLED 屏为例
android·驱动开发·i2c·瑞芯微·嵌入式驱动·rk3576·嵌入式安卓
段娇娇3 小时前
Android jetpack LiveData (二) 原理篇
android·android jetpack
我命由我123454 小时前
Android 多进程开发 - FileDescriptor、Uri、AIDL 接口定义不能抛出异常
android·java·java-ee·kotlin·android studio·android-studio·android runtime
阿拉斯攀登4 小时前
第 14 篇 显示驱动(MIPI/LVDS 屏)适配与调试,DRM 框架详解
android·驱动开发·rk3568·瑞芯微·rk安卓驱动
阿拉斯攀登5 小时前
第 18 篇 综合项目实战:基于 RK3568 的安卓智能门禁系统,全栈开发
android·驱动开发·瑞芯微·嵌入式驱动·rk3576·安卓驱动