尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Android架构笔记:状态管理、依赖注入与模块化实战
1. 为什么我把架构笔记写成这样如果你和我一样经历过一段“能跑就行”的开发阶段再回头看自己写过的Activity多少都会冒冷汗。启动页跳转写满了碎逻辑接口回调直接在页面里改UI一个方法超过200行——不是不想改是真不知道从哪里下手。后来我开始系统梳理Android架构把踩过的坑、想通的原理、反复调整的写法整理成了一套笔记。这份笔记不是什么标准答案也不是什么“最优实践”它只是我在实际项目里验证过、并且还在继续演进的方案。整套笔记的核心线索只有三个问题状态放哪里、事件往哪走、依赖怎么接。这三个问题想清楚了架构基本就立起来了。剩下的大部分框架层东西都只是为这三个问题服务的工具。1.1 架构不是模型图是一条条约束我最早对架构的误解是只要画得出漂亮的模型图项目就“有架构”了。后来发现根本不是。图是给人看的约束才是给代码用的。真正有效的架构是把“不该做的事情”写死。比如UI层不能直接持有网络请求的具体实现细节Data层不能反向依赖UI层不能在网络回调里去弹Toast页面状态必须集中在ViewModel里维护Activity只负责展示和反馈事件。这些约束不是怕新人犯错而是减少编码时的决策成本。人每天能做的决策有限如果写每一行代码都要纠结“该放哪”到下午基本就废了。约束定好了大部分常见场景照着规则走就行脑子留给真正需要判断的业务逻辑。用一个生活类比一个不做分区、调料全放台面、抹布和菜刀混在一起放的厨房做完第一道菜也许很快。但要连续做十道菜你连锅都找不到。架构不是在开火前把厨房照得漂漂亮亮而是让做第十道菜的时候你还能像做第一道菜一样知道东西在哪儿。1.2 适合谁参考、怎么参考这份笔记适合正在从一个“功能能跑”到“后悔还能改”阶段的人。如果你刚入行先把单一Activity、ViewModel、Repository这些基础组件用熟不要一开始就上多模块。如果你已经做过一两个完整项目那笔记里的分层、依赖注入、状态管理部分会更有参照价值。我建议的阅读方式是先看结论再回去理解推导过程。某个具体方案为什么要这么选比方案本身更重要。因为项目不同、团队不同、业务不同适合的架构不会一样。我的分工是把“取舍逻辑”写清楚把“可直接抄的配置”放在代码块里。这样你既知道怎么用也知道什么时候不该用。2. 模式选型从MVC到MVVM再到MVI的取舍记录模式选型是架构学习里最容易吵起来的部分。我一开始也很纠结到底用MVVM还是MVI后来发现根本不需要站队。每一种模式解决的问题都不一样适用场景也不一样。2.1 四种模式我用下来的亲身体会先放一张简表是我在不同项目里实际用过之后的结论。模式核心特点优点痛点我的使用场景MVCActivity既是View又是Controller简单直接生命周期与业务逻辑混杂类快速膨胀早期小DemoMVPPresenter通过接口隔离View和Model业务逻辑可单独测试接口过多维护成本高遗留项目维护MVVMViewModel持有状态View观察状态状态集中生命周期安全使用不当会隐式更新状态流不清晰日常主力方案MVI状态单向下行事件单向上行状态变化可回溯逻辑链路清晰样板代码多上手门槛高复杂页面、表单、聊天MVVM里我推荐用StateFlow ViewModel Repository的搭配而不是重度依赖DataBinding。DataBinding能少写很多样板代码但表达式里塞逻辑之后代码变得很难调试。我个人的分界线是布局表达式只做展示和简单格式化不写业务判断。业务判断全部放到ViewModel里。MVI也不是银弹。它的核心思想是“页面只有一个不可变状态所有变更通过事件触发最终归约成新状态”。这听起来很美好但在实际页面里如果一个状态对象里有七八个字段每次只改一个字段也要copy整个状态样板代码会非常烦人。我现在的做法是常规页面用MVVM状态复杂、交互链路长的页面引入MVI的单向数据流思想但不完全照搬。2.2 单Activity是不是唯一选项我现在大部分项目都采用单Activity Navigation组件。这样做的好处非常明显屏幕上显示什么页面完全由导航栈决定ViewModel的scope也按返回栈管理。Activity不再是“一页一Activity”里的页面容器而更像是一个应用外壳。但如果你接手的是老项目几百个Activity已经存在了别为了“架构正确”硬改成单Activity。架构调整要跟着业务节奏走而不是跟着概念走。我在某个项目里花了两个迭代做了部分页面的单Activity化只挑高频跳转链路改低频页面等自然重构时再收敛。效果不错风险也可控。2.3 一个可以直接套用的ViewModel骨架下面这段是我日常写页面的基础模板去掉了业务细节保留了结构class MainViewModel( private val repository: MainRepository ) : ViewModel() { private val _uiState MutableStateFlow(MainUiState()) val uiState: StateFlowMainUiState _uiState.asStateFlow() fun loadData() { viewModelScope.launch { _uiState.update { it.copy(loading true, error null) } runCatching { repository.fetchData() } .onSuccess { data - _uiState.update { it.copy(loading false, data data) } } .onFailure { e - _uiState.update { it.copy(loading false, error e.message) } } } } } data class MainUiState( val loading: Boolean false, val error: String? null, val data: ListString emptyList() )这里的关键不是用了runCatching而是页面状态被显式建模。Loading、Error、Data都写在同一个不可变对象里。UI层只需要观察uiState根据状态组合刷新页面。这样做的直接好处是你不用再为“网络请求后还要不要刷新上一个页面”这种问题单独写一堆回调接口。3. 分层与依赖方向最容易被忽略的架构纪律很多人在讲架构时只谈UML图不谈依赖方向。但在我眼里依赖方向才是架构里最核心的纪律。什么分层、模块化本质都是在控制依赖方向。把这个想明白了后面很多东西都顺了。3.1 三层依赖方向到底在防什么官方现在推荐的仍然是三层结构UI层、Domain层、Data层。但严格来说中小项目里Domain层可以很薄甚至可以省略。我个人的划分方式是UI层Activity、Fragment、Compose页面、ViewModelDomain层纯业务逻辑比如计算规则、状态流转Data层本地存储、网络接口、数据库、数据仓库。依赖方向必须是 UI层 → Domain层 → Data层。反过来不行。Data层不能知道UI层的存在这是这条纪律的红线。为什么这么严格因为一旦Data层可以直接操作UI就会出现“网络回调里弹对话框”这种代码。表面上看只是方便实际上会让测试、复用、模块拆分全部变得不可能。Data层里的一个类不再只是一个数据来源它悄悄变成了一个UI控制器任何地方想使用它都得带着它依赖的Activity上下文。时间一长整个项目就像一团缠死的线。顺带说一句Domain层到底要不要留我的经验是看业务复杂度。如果你的业务规则只是“请求一个列表然后展示”那Domain层就是过度设计。但如果你的业务里有复杂的计算、状态机、多数据源合并那Domain层能把规则从UI和Data之间解放出来值得保留。3.2 Hilt落地时的几个取舍依赖注入到了Android上最常见的选择是Hilt。它最大的作用是让依赖关系显式化而不是靠object单例满天飞。我用Hilt时有三条自己定的规矩能用构造注入就用构造注入避免字段注入。字段注入让依赖关系藏在类内部构造注入则强迫所有依赖在创建时可见。作用域要克制。Singleton不是万能钥匙。如果某个对象只在某个页面生命周期内有效把它注成全局单例轻则浪费内存重则持有Activity引用导致泄漏。跨模块依赖不能偷懒。不要在模块A里为了用模块B的接口直接implementation依赖模块B。应该把接口下沉到更底层的公共模块。下面是一个常见的网络层Module示例Module InstallIn(SingletonComponent::class) object NetworkModule { Provides Singleton fun provideApiService( okHttpClient: OkHttpClient ): ApiService { return Retrofit.Builder() .baseUrl(BuildConfig.BASE_URL) .client(okHttpClient) .build() .create(ApiService::class.java) } }这里有个容易被忽略的细节ApiService本身通常是无状态的接口它可以放在任何模块里。但OkHttpClient这种带连接池和线程池的对象一定要做成单例。如果你在多个地方各建一个OkHttpClient最直接的问题是连接池失效SSL握手频繁重来网络请求会莫名变慢。注意Hilt的ActivityScoped、ViewModelScoped不要滥用。我见过一个项目几乎每个类都标Singleton最后变成全局单例满天飞依赖注入失去了意义。4. 协程、Flow与状态管理的实操笔记架构不能只谈分层还要解决异步和状态。早期Android用回调后来用RxJava再后来是协程。我这套笔记里把协程和Flow单拎出来是因为它们已经成了现代Android架构的事实基建。4.1 从回调切换到协程的真实体会协程最大的价值不是写代码更短而是结构化并发。一个viewModelScope启动的协程会跟着ViewModel的销毁自动取消不需要你手动管理。这个特性直接解决了以前回调时代最头疼的问题页面销毁后还在执行的请求结果回头来操作已经销毁的UI。我用协程之后最大的感受是错误处理终于变成线性的了。以前回调嵌套里每层都要单独处理失败失败分支越叠越多。现在一个runCatching或try/catch就能包住整段逻辑代码的可读性提升不是一点半点。当然协程也有坑。最典型的是在Dispatchers.IO里直接更新UI对象或者忘记切回主线程。我这边的惯例是ViewModel里的协程不显式指定线程因为viewModelScope默认在主线程执行网络和数据库操作会在Repository内部通过suspend函数的实现去切换线程。这样UI层和业务层都不需要关心线程细节。4.2 StateFlow、SharedFlow 和一次性事件这是我被问得最多的问题页面通知用StateFlow还是SharedFlow我的判断很简单UI状态用StateFlow。StateFlow只保留最新值适合“当前应该显示什么”这种状态。一次性事件Snackbar、导航跳转、Toast用SharedFlow或Channel。因为一次性事件如果用一个普通Flow来传在配置变更后重放用户就会看到弹窗重复出现。SharedFlow我也很少直接用原始API而是在ViewModel里做一个事件封装class MainViewModel : ViewModel() { private val _events ChannelMainEvent(Channel.BUFFERED) val events _events.receiveAsFlow() fun onButtonClicked() { viewModelScope.launch { _events.send(MainEvent.ShowMessage(按钮被点击)) } } } sealed interface MainEvent { data class ShowMessage(val message: String) : MainEvent }这里的要点是事件和状态分开处理。状态持续存在可以被组合事件是一次性的消费完就消失。合在一起会出各种奇怪问题。比如你用MutableStateFlowUiEvent?保存事件事件发出去后如果没人消费下一次状态更新时这个旧事件可能还在导致相同逻辑触发了两次。4.3 loading、error、empty 的组合处理状态管理的难点通常不是单个状态而是多个状态同时存在。比如一个页面既在刷新列表又在下拉加载更多还同时收到一个网络错误。如果状态建模得不好就会出现“loading把error遮住”之类的问题。我的习惯是给页面建一个不可变状态对象字段组合表达当前界面data class ListUiState( val list: ListItem emptyList(), val loadingMore: Boolean false, val refreshing: Boolean false, val errorMessage: String? null, val endReached: Boolean false )页面根据loadingMore、refreshing、list.size、errorMessage这几个字段的组合决定显示什么。不要写“if loading show loading, else if error show error, else show list”这种简单树因为真实页面的状态永远比这复杂。用字段组合表达后UI层只是状态的翻译器不再是业务判断的堆砌点。5. 模块化从单模块到多模块的实战清单架构笔记写到最后一个大环节就是模块化。如果你是个人开发者单模块完全够用。如果你在一个业务迭代很快的团队里多模块的价值就会体现出来并行开发不冲突、构建缓存更友好、代码边界更清晰。但模块化不是拆得越碎越好。拆得太碎依赖图会变成一团乱麻构建时间不降反升。5.1 模块划分该按什么切常见的划分方式有两种按层切和按功能切。按层切就是把app、core、data、network拆成独立模块。这种方式结构清晰适合团队规模小、业务耦合少的场景。按功能切则是把登录、首页、订单这些业务拆成feature_login、feature_home每个feature内部自己决定是否需要data和domain。我的经验是先按层切再在层内按功能切。也就是说先有一个公共的core模块放基础能力再按业务功能拆出feature模块feature之间不互相依赖。如果两个feature有公共代码就下沉到core里而不是直接依赖对方。这样能把业务边界和复用逻辑同时照顾好。具体的Gradle配置我通常用Kotlin DSL统一管理// gradle.properties android.useAndroidXtrue android.enableJetifiertrue模块化最怕的是在根项目的Gradle配置里写一堆各自独立的重复配置。我会抽出一个公共的构建逻辑模块统一处理SDK版本、编译选项、依赖版本。这样新开模块时只需要几行配置不会“复制粘贴一份又改漏一个版本”。5.2 构建时间与依赖循环的平衡模块多起来之后编译时间不一定会线性缩短。如果A模块依赖BB模块又依赖AGradle直接报循环依赖错误。更隐蔽的问题是模块之间的间接依赖太多任何改动都会牵连一片模块重新编译。我用来控制的做法有几个每周用Gradle的依赖报告检查一次模块依赖方向把网络、工具函数、通用组件下沉到core模块减少feature之间的直接依赖如果某个依赖只在一个模块内部使用不要往上抛到公共模块的api配置里。api和implementation的区别值得单独说一句。用implementation时传递依赖不会暴露给下游模块用api时下游模块也能拿到这个依赖。很多人图省事直接用api结果一个依赖变化所有模块都重新编译。我的准则是能不用api就不用api。这不仅能加快编译还能让模块之间的耦合暴露得更晚、更少。5.3 模块化后最容易踩的坑模块化踩坑是家常便饭我这里列几个自己真踩过的R类找不到。每个模块有自己的R类不能再只写import xxx.R。必须显式import当前模块的R。AndroidManifest合并冲突。多个依赖库都有自己的Manifest合并时如果设置了同名activity或权限会报冲突。处理办法是检查合并后的Manifest而不是猜。Navigation跨模块跳转困难。官方Navigation的findNavController能在同一个Activity内跨模块使用但深链接和路由表如果散落在各个模块要有人统一维护。Hilt跨模块缺少module。feature模块需要使用core模块的依赖时需要在core模块的InstallIn里把相关Module安装到同一个Component漏了编译期不会马上报错运行期才提示无法注入。这些坑单模块时代基本不会出现进入模块化后会轮着踩一遍。我在笔记里专门留了一张检查清单每次新建模块就照着走一遍能省很多排查时间。6. 常见问题速查与我的处理习惯架构笔记写到最后最值钱的反而不是理论而是问题排查的路径。下面这张表是我实际开发中遇到的高频问题以及对应的定位思路。它不能覆盖所有情况但能帮你少走一半弯路。问题现象常见原因我的一般解决路径页面从后台回来接口被重复请求在lifecycleScope里启动collect没有搭配repeatOnLifecycle在Fragment的onStart里用repeatOnLifecycle包裹配置变更后一次性事件重复弹出用StateFlow当事件通道改用Channel/SharedFlow事件消费后再发送Hilt运行期提示无法注入缺少AndroidEntryPoint或模块没有安装到对应Component检查Activity/Application注解再查Module的InstallIn协程里的网络请求没有取消用了GlobalScope而不是viewModelScope全局搜索GlobalScope替换成有清晰生命周期的scope模块化后R资源编译不过没有显式import当前模块的R确认资源所在模块名改成显式import依赖版本冲突不同模块声明了同一个库的不同版本用统一的版本目录或Gradle的依赖约束6.1 环境与构建的五个坑每个都卡过我半天构建环境的问题单独拿出来说。这几个坑不处理好架构笔记写得再漂亮项目也跑不起来。JDK版本不匹配是最常见的。比如某个库要求JDK 17但你本机默认还是JDK 11Gradle会报一些莫名其妙的错误。解决办法是在build.gradle.kts里统一配置compileOptions的sourceCompatibility和targetCompatibility并且确认AS使用的JDK路径和命令行用的JDK一致。SDK路径带空格也算老生常谈。早期的Android SDK如果装在Windows的Program Files目录下路径里的空格会让某些原生工具无法工作。我现在都把SDK放在D:\Android\Sdk这类纯英文无空格路径下。Gradle缓存损坏看起来是玄学。现象是突然某个依赖下载不完报checksum错误。最直接的修复是删除~/.gradle/caches下的对应目录再重新同步项目。不要一上来就重装AS那样成本太高。AndroidX迁移遗漏是历史项目改架构时的高频事故。只改了compileSdk和targetSdk没把android.useAndroidXtrue配上或者还有旧库在拉非AndroidX的support包。迁移时我建议用官方帮助脚本自动扫描一遍同时打开Build窗口搜索android.support关键词。Kotlin插件版本冲突则通常出现在多模块项目里。某个模块用了新版的Kotlin插件另一个模块还在用旧版Gradle会提示版本不一致。我现在的做法是所有版本的Kotlin、Hilt、KSP/APT插件统一写在一个版本目录文件里全项目引用同一处避免各模块自定版本。6.2 我复盘一个架构问题的固定步骤遇到架构相关的问题我很少直接翻Stack Overflow或者问同事。先自己按一套固定步骤走一遍通常会找到答案。第一步复现。明确触发条件把操作步骤和前后状态写下来。这一步看起来笨但至少能排除“我这行代码压根没执行”的乌龙。第二步画依赖链。从触发点开始把UI层调了谁、Repository调了谁、谁持有了谁画出来。很多时候问题不在代码逻辑而在错误的依赖方向。比如Data层的网络回调里直接更新UI这条依赖链本身就违规了。第三步判断是状态问题、事件问题还是生命周期问题。我常问自己如果这个页面不销毁问题会不会出现如果把状态改成不可变对象问题会不会消失这样能快速定位是数据更新方式不对还是生命周期scope没处理好。第四步最小化修改。架构问题最忌讳大范围重构。我倾向于先加一个防御性判断或调整一个对象的scope验证方向是否正确再决定要不要大面积改。第五步补测试。至少补一个覆盖这次问题场景的单元测试。实测下来补测试的意义不只是防止回归而是逼你把问题描述清楚。写测试的时候你会发现很多之前“感觉是bug”的地方其实是自己对状态模型的定义不完整。最后说一个我的实操习惯我这份Android架构笔记与其说是记录答案不如说是记录问题。每次遇到一个架构问题我都会先记现象、再记原因、最后记“下一次怎么提前预防”。一开始笔记很乱后来慢慢形成了上面这套结构模式选型、分层纪律、异步状态、模块化、问题排查。最近一次重构老项目时我靠这些笔记少踩了至少三个大坑也把团队里新人培训的时间从两周压缩到了一周左右。如果你也想整理自己的架构笔记我建议不要直接抄我的目录。先把你最近半年遇到的最痛的问题列出来按这些问题去组织笔记。架构本质上是用来解决问题的问题在哪里架构笔记就从哪里生长。
RELATED

相关推荐

西门子S7-1200在锂电涂布机浆料输送系统的应用与调试

西门子S7-1200在锂电涂布机浆料输送系统的应用与调试

做锂电非标设备这些年,涂布机的浆料输送系统一直是我觉得入门西门子S7-1200最合适的“练兵场”。它不像机器人控制那样偏重运动学,也不像DCS那样强调大点数冗余,而是把模拟量处理、PID调节、变频通讯、触摸屏组态、安全回路这些PLC工程师日常…

📅 2026/10/13 7:42:30
反转链表 II 详解:虚拟头节点与迭代递归全解

反转链表 II 详解:虚拟头节点与迭代递归全解

如果你拿着“Leetcode 103 反转链表 II”这个标题去翻常见题库,大概率会愣一下:不少刷题平台里“反转链表 II”的标准题号是 92,而 103 通常对应的是另一道二叉树题。但这不影响今天的核心,你手里的这份资料只是把题目顺序重新编排…

📅 2026/10/13 7:42:30
用 Flutter 构建 OpenHarmony 井盖巡检地图与排班系统

用 Flutter 构建 OpenHarmony 井盖巡检地图与排班系统

最近一段时间,我一直在折腾一个挺有意思的项目:用 Flutter 给 OpenHarmony 做一套城市井盖地图管理 App,顺带把巡检人员的日程排班功能也塞了进去。项目整体跑通之后,回头看踩过的坑和做过的取舍,我觉得很值得拿出来聊…

📅 2026/10/13 7:42:30
MORE NEWS

更多资讯

📰

数据库课程设计实战:从建库到事务闭环的完整指南

简介:本资源是广东工业大学数据库课程设计大作业的完整实现方案,面向高校计算机及相关专业本科生,聚焦数据库系统开发全流程实践,解决课程设计中需求分析、后端逻辑、前端交互与数据持久化等核心问题。压缩包共195个文件&#xff…

📰

ai-agent-book:从入门到 Agent 工程实践(一)

AI Agent 入门 本blog为datawhale开源教程《ai-agent-book:从入门到Agent工程实践》的组队打卡学习笔记 文章目录AI Agent 入门Agent的组成部分LLM:Agent 的大脑工具:Agent 的手脚上下文:Agent 的眼睛Harness:约束or赋…

📰

【PMSM滤波代码2】PMSM未知负载转矩观测EKF。MATLAB例程(负载增广EKF与实际仿真结果)。订阅专栏后可查看完整代码

如需帮助,或有滤波相关的MATLAB代码定制需求,可从个人主页左侧联系我 订阅专栏后,可直接查看源代码,粘贴到MATLAB空脚本中即可直接运行、得到结果 文章目录 总体介绍 运行结果 状态估计误差 电压与新息诊断 电流观测与估计 负载转矩观测 转速与角度观测 命令行输出 MATLAB源…

📰

MySQL 8.4课程设计实战:从教科书模型到生产级数据库

简介:本资源是广东工业大学数据库课程设计大作业的完整实现方案,面向高校计算机、软件工程等专业学生及数据库初学者,聚焦真实业务场景下的系统开发全流程实践。项目以平行志愿录取系统为原型,采用Java(Spring框架&…

📰

解Bug之路-隔离故障点后业务不恢复?-热点Key处理与亚稳态故障

前言《解Bug之路》作为笔者博客的镇站之宝系列,之前写的都是一些疑难杂症。但在和AI交流的过程中发现,一些对于笔者来说排查很直观的故障其实有不小的价值。这篇文章就是此种情况,虽然排查过程很直观,思路一直流畅没有卡点&#x…

📰

PostGIS 3.5.0 + PostgreSQL 17 Windows二进制捆绑包详解

简介:本资源是PostgreSQL 17专用的PostGIS 3.5.0空间数据库扩展安装包,面向GIS开发人员、地理信息专业学生及需要在PostgreSQL中启用空间数据能力的数据库工程师。它解决了传统关系型数据库缺乏原生地理空间处理能力的问题,可快速为PG17实例集…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬