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/...

相关推荐
白雪落青衣14 小时前
buuoj course 1详细解析
android
恋猫de小郭14 小时前
Android 发布全新性能分析器,实用性和性能大升级
android·前端·flutter
Kapaseker14 小时前
为什么 Java 的数组需要 new 出来
android·java·kotlin
黄林晴15 小时前
颠覆开发!Google AI Studio 一句话生成原生 Android App
android·google io
恋猫de小郭15 小时前
Flutter 3.44 发布啦,超级大版本更新!!!
android·flutter·ios
zb2006412015 小时前
Laravel10.x重磅升级:新特性全解析
android
2601_9574188015 小时前
深入解析Android相机有线连接:PTP与MTP协议栈实现原理与实践
android·数码相机·智能手机
努力努力再努力wz15 小时前
【QT入门系列】QWidget 六大常用属性详解:windowOpacity、cursor、font、focus、toolTip 与 styleSheet
android·开发语言·数据结构·c++·qt·mysql·算法
撩得Android一次心动15 小时前
C语言基础笔记3【个人用】
android·c语言·开发语言·笔记
小离a_a15 小时前
uniapp小程序封装圆环显示比例数据
android·小程序·uni-app