前言
一个成熟 Androider 的标志是......敢重构但又不把分层搞崩😁
最近手上有个多模块 Compose 项目,骨架是 core + feature + app 三层,Hilt、Room、Navigation 3 都上了,第一眼看挺干净。
但代码翻得越深越觉得不对味------有些地方「层是画出来了,边界却被人悄悄穿了个洞」 。比如一个 Screen 从父作用域去抓别的 ViewModel 的数据;比如接口写得好好的,组件却在背后绕开接口、直接拿具体实现类来调。
这类「分层穿了孔」的问题最烦人的地方在于:它不崩、不报错、跑得好好的,但等你哪天要换实现、要写测试、要并行开发,它就成了隐性炸弹。
这次我没按老办法一个人闷头改,而是先对标 Google 官方的 Now in Android 定方向,再把重构拆成「评审 → 分级 → 修复 → 验证」的闭环,全程驱动 AI 执行。
这篇文章不讲语法,讲的是怎么给分层架构升级定方向,并把它安全地交给 AI。
一、重构前:先对标 Now in Android,定方向和目标
重构最忌讳的就是「上来就改」。没有参照系的重构,改到最后往往变成「换了一种风格,但方向还是歪的」。
所以我的第一步不是写代码,而是找一把尺子 。这把尺子就是 Google 官方的 Now in Android(NIA)。
1.1 为什么选 NIA 做对标
NIA 是 Google 官方维护的 Compose 最佳实践示例,它的价值不在「功能多」,而在「每一层的边界都极其克制」。我重点分析了它三个层面:
① 模块化拓扑 :NIA 把 core 层拆成 common / data / database / model / network / ui / designsystem / testing 等十几颗「原子模块」,每颗只干一件事;feature 层按业务切成独立模块,互相不依赖实现。
② 依赖倒置(最关键) :NIA 的分层里,UI 层永远不知道数据从哪来 。ViewModel 依赖的是 core:domain 里的 Repository 接口 ,而实现藏在 core:data 的适配器里。依赖方向永远朝「抽象」走,不朝「具体类」走。
③ 契约收敛 :NIA 的 DI 绑定清一色用 @Binds 抽象方法,接口契约完整;跨模块通信只走接口,不走实现类。
我把 NIA 的分层准则提炼成下面这张图,这就是我重构的目标态:
这张图的核心就一句话:箭头只能朝「端口(接口)」走,不能朝「具体类」走。 这是判断分层是否健康的唯一标准。
1.2 差距诊断:fragmject 的「穿层点」
拿着这把尺子去量 fragmject,我让 AI 做了全项目依赖扫描 ,很快定位到几个「边界穿洞」的地方。这才是本次重构的方向和目标清单:
有了这张「差距图」,重构的方向和优先级就定死了:先修穿层(P0),再修冗余(P1),最后收风格(P3)。接下来才是把活交给 AI。
二、为什么敢让 AI 来重构分层
先说结论:AI 不适合「直接改」,但非常适合「评审 + 小步修」。
直接甩一句「帮我重构这个项目的分层」,大概率得到一堆看似合理、实则跑不起来的改动------因为 AI 对项目全貌的认知是碎片化的,它会「脑补」不存在的依赖关系。
我的做法反过来:让 AI 先当「评审官」,再当「外科医生」。
- 评审阶段:AI 只输出问题清单,不碰代码。人负责判断「哪些是真问题、哪些是误报」。
- 修复阶段:一次只修一类问题,修完立刻编译 + lint 验证。
- 收敛阶段:多轮迭代,每轮聚焦不同领域,逐层啃干净。
这套流程跑下来,我总结成一条核心经验:
驱动 AI 重构的关键,不是让它「更聪明」,而是让它「更可控」------把一次大手术,拆成一串可验证的小手术。
三、驱动 AI 的 Prompt 模板(可直接复用)
这部分是全文最有价值的。我把整套流程固化成了三段式 Prompt ,每段都可以直接复制改造。核心思路是:用 Prompt 把「流程」锁死,让人只做「判断」。
3.1 评审 Prompt ------ 逼它只诊断、不动手
text
你是一名资深 Android 架构师。现在对 [项目名] 做一轮「分层架构评审」。
【对标基准】
以 Google Now in Android 的分层准则为标尺,核心判据只有一条:
「依赖箭头只能指向端口(接口),不能指向具体类。」
【评审范围】
[填入本次聚焦的领域,例如:core:data 与 feature 的边界]
【要求】
1. 逐项列出发现的问题,格式固定为:
- 严重级别(P0=边界穿孔/正确性,P1=契约不一致/冗余,P2=可维护性,P3=风格)
- 问题描述(一句话说清「谁越权依赖了谁」)
- 影响面(会导致什么后果)
- 修复方向(只给方向,不给完整代码)
2. 最后单列一节「已经做对、不应改动的设计」,防止误伤。
【铁律】
本次只输出评审报告,绝对不要修改任何代码。
关键在最后一句「绝对不要修改任何代码」。这逼着 AI 停下来「取证」(读文件、grep 引用点),而不是凭印象输出。评审报告是人可以 review 的,风险被前置拦截了。
3.2 修复 Prompt ------ 一次只修一类
text
基于刚才的评审报告,现在只修复 [P0-1:SystemScreen 越权访问 MainViewModel] 这一项。
【执行步骤】
1. 先重新读取目标文件的当前内容,确认依赖关系和所有引用点;
2. 给出最小改动方案,原则:
- 依赖方向必须改为「指向端口」,禁止引入新的具体类依赖;
- 不动无关代码,不改命名习惯;
3. 改完后列出「受影响的调用方」,确保没有遗漏。
【铁律】
一次只修复这一项。修复前必须先读文件确认当前状态,禁止凭记忆编辑。
这一句「修复前必须先读文件」,是防止 AI 基于「过期的记忆」去改已经被改过的文件------这是 AI 重构翻车的头号原因。
3.3 验证 Prompt ------ 把幻觉挡在主干外
text
刚才的改动已应用。现在执行验证闭环,任何一步失败都要停止并报告原因:
1. 静态检查:read_lints 查看目标文件是否有报错;
2. 编译验证:./gradlew :[模块]:compileDebugKotlin 必须通过;
3. 依赖复核:grep 确认旧的具体类依赖已清零、新的端口依赖已生效。
请按顺序执行,输出每步结果。
这条铁律让 AI 的「幻觉代码」在进主干前就被拦下了。 没有验证闭环的 AI 重构,等于裸奔。
3.4 完整驱动流程(一张图看懂)
四、实战一:拆除跨层越权 ------ Screen 不再「借」别人的 ViewModel
这是本轮最典型的一处「分层穿洞」。
旧代码 :SystemScreen 从父组合作用域 去抓 MainViewModel 的数据:
kotlin
// 旧:SystemScreen 越权访问 MainViewModel
@Composable
fun SystemScreen(
mainViewModel: MainViewModel = viewModel(), // ← 抓的是「别人」的 ViewModel
onNavigate: (key: NavKey) -> Unit = {},
onNavigateUp: () -> Unit = {},
) {
val treeResult by mainViewModel.treeResult.collectAsStateWithLifecycle()
// ...
}
问题本质 :SystemScreen 为了拿一份「体系树」数据,直接跨到了 MainScreen 的职责边界内。它依赖 Navigation 3 的 ViewModelStoreOwner 生命周期------当 MainNavKey 被弹出后再进入 SystemNavKey,viewModel() 会重新构造一个新的 MainViewModel ,treeResult 会被重复请求。
新代码 :把「体系树」归还给它真正的 owner------SystemViewModel 自己注入领域端口:
kotlin
// 新:SystemScreen 只依赖自己的 SystemViewModel
@Composable
fun SystemScreen(
cid: String,
systemViewModel: SystemViewModel = viewModel(), // ← 自己的 ViewModel
onNavigate: (key: NavKey) -> Unit = {},
onNavigateUp: () -> Unit = {},
) {
val treeResult by systemViewModel.treeResult.collectAsStateWithLifecycle()
// ...
}
而 SystemViewModel 依赖的是领域端口,不再「借」任何人的数据:
kotlin
@HiltViewModel
class SystemViewModel @Inject constructor(
private val navigationRepository: NavigationRepository, // ← 领域端口
) : BaseViewModel() {
private val _treeResult = MutableStateFlow<List<Tree>>(emptyList())
val treeResult: StateFlow<List<Tree>> = _treeResult.asStateFlow()
init {
viewModelScope.launch {
navigationRepository.observeSystemTree().collect { trees ->
_treeResult.value = trees
}
}
}
}
关键点 :SystemViewModel 依赖的是 NavigationRepository 这个接口(端口) ,而不是 MainViewModel 这个具体类。这一步把「跨 ViewModel 的隐式耦合」降级成了「对领域端口的显式依赖」,正是 Ports & Adapters 的核心。
收益 :MainViewModel 再怎么改,SystemScreen 都不受影响;体系树数据来自同一个 Room 数据源(SSOT),不重复请求;分层边界重新闭合。
五、实战二:接口契约统一 ------ WebViewPool 从「双通道」到「单通道」
这处问题更隐蔽:接口是有的,但契约不完整,导致代码绕开接口走了「后门」。
旧代码 :WebViewPool 接口漏掉了三个缓存方法,组件只能绕开接口去拿具体实现类 WebViewManager:
kotlin
// 旧:接口契约不完整
interface WebViewPool {
fun prepare(context: Context)
fun obtain(context: Context, url: String): WebView
fun recycle(webView: WebView)
fun trimToSpare()
fun releaseAll()
// ← isCacheResource / cacheResourceRequest / prefetchDns 不在这里!
}
// 旧:组件绕开接口,走 EntryPointAccessors 拿具体类
val webViewManager = EntryPointAccessors.fromApplication(
context, WebViewManagerEntryPoint::class.java
).webViewManager()
webViewManager.isCacheResource(request) // 依赖具体类,而非接口
新代码:把三个方法补进接口,切断「组件 → impl」的耦合:
kotlin
// 新:接口契约完整
interface WebViewPool {
fun prepare(context: Context)
fun obtain(context: Context, url: String): WebView
fun recycle(webView: WebView)
fun trimToSpare()
fun releaseAll()
// 补齐的三个方法,缓存契约对组件可见
fun isCacheResource(request: WebResourceRequest): Boolean
fun cacheResourceRequest(context: Context, request: WebResourceRequest): WebResourceResponse?
fun prefetchDns(url: String)
}
// 新:组件只依赖 WebViewPool 接口
val webViewPool: WebViewPool = /* 常规注入,接口类型 */
webViewPool.isCacheResource(request)
收益:依赖方向从「组件 → 具体实现」变成「组件 → 接口」,这是依赖倒置原则(DIP)的落地。以后想换 WebView 缓存实现,改 Hilt 绑定即可,组件一行不用动。
六、实战三:DI 绑定风格迁移 ------ @Provides → @Binds
如果说前两个是「边界穿洞」,这个则是「边界内写法不够地道」------但它是最能体现工程洁癖的一处。
旧代码 :15 个 Repository 全部用 @Provides 工厂方法绑定:
kotlin
// 旧:@Provides 为每个 Repository 生成一个 Factory 类
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
@Provides
@Singleton
fun provideUserRepository(remote: UserRemote, store: UserStore): UserRepository =
UserRepositoryImpl(remote, store)
// ... 其余 14 个都是这样的样板
}
新代码 :改成 @Binds 抽象方法,让 Dagger 自己推导:
kotlin
// 新:@Binds 只声明「接口 → 实现」的桥接,零样板
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
@Binds
@Singleton
abstract fun bindHomeRepository(impl: OfflineFirstArticleRepository): HomeRepository
// ... 其余 15 个端口同理,共 17 个领域端口全部 @Binds
}
收益:17 个 Factory 类不再生成,编译更快、dex 更小;绑定关系从「工厂样板」变成「一目了然的接口桥接」。
七、已经做对的分层设计(为什么它们值得保留)
重构不是「推倒重来」。AI 评审的价值,有一半在于识别出哪些分层已经做对了,不要乱动。
7.1 Feature 层零数据层依赖(依赖倒置已达标)
评审时让 AI 全项目 grep 了一遍:
java
grep_search project(":core:(data|database|network)") → feature/ 下零命中
含义 :所有 Feature 模块完全不依赖数据层 ,只依赖 core:domain(端口)+ core:model(实体)。这是 Clean Architecture 依赖倒置原则的教科书级落地------业务逻辑不知道数据从哪来,只知道「我要一个 NavigationRepository」。
7.2 端口 + 适配器的标准范式
以 NavigationRepository 为例,三层分工极其清晰:
kotlin
// core:domain ------ 端口(只有接口,没有实现)
interface NavigationRepository {
fun observeNavigation(): Flow<List<Navigation>>
fun observeSystemTree(): Flow<List<Tree>>
suspend fun refreshNavigation(): DomainResult<Unit>
suspend fun refreshSystemTree(): DomainResult<Unit>
}
// core:data ------ 适配器(实现端口,对外只暴露接口)
@Singleton
class OfflineFirstNavigationRepository @Inject constructor(
private val navigationDao: NavigationDao,
private val treeDao: TreeDao,
private val commonRepo: CommonDataSource,
) : NavigationRepository { /* ... */ }
UI 只认端口,不认适配器 。这就是为什么 SystemScreen 解耦后,能轻松从「借 MainViewModel」切换成「注入 NavigationRepository」------因为端口早就备好了,只是之前有人图省事没走它。
7.3 类型级语义:RequiresAuth / DetailPaneNavKey
最亮眼的一处:项目把「需要登录」「大屏用右侧面板」这两个业务语义 ,抽象成了标记接口 ,于是 AppNavGraph 用两个判断函数就搞定三种导航路径(登录页 / 详情面板 / 常规 push),没有一个 when 分支在枚举具体的 NavKey。这是把「分层语义」沉淀到「类型系统」的高级做法。
八、成果:用「分层健康度」说话
重构最怕「改了一堆,说不清改好了什么」。这次我让 AI 从分层边界的视角汇总,得到一张比「改了 N 个文件」更有说服力的表:
| 分层维度 | 改造前 | 改造后 |
|---|---|---|
| 跨层越权依赖 | SystemScreen → MainViewModel |
零(各自注入领域端口) |
| 接口契约完整性 | WebViewPool 漏 3 个方法,走「双通道」 |
契约闭合,组件只依赖接口 |
| DI 绑定风格 | 15 个 @Provides 工厂样板 |
17 个 @Binds 抽象桥接 |
| Feature 层数据依赖 | 已零依赖 | 保持零依赖(巩固) |
| 领域端口 + 适配器 | 已建立 | 保持清晰(端口自持数据) |
最关键的不是「改了多少」,而是「边界重新闭合了」 ------每一处改造都指向同一个目标:让依赖只朝着「端口」走,而不是朝着「具体类」走。这才是分层架构升级的本质。
最后
AI 做分层重构不是「把活丢给它」,而是「先对标定方向,再把流程交给它,把判断留给自己」。
完整的动作链,其实就是这四步:
- 先对标------拿 NIA 当尺子,画出「目标态」和「差距图」;
- 再评审------用评审 Prompt 让 AI 出问题清单,人审核分级;
- 小步修------用修复 Prompt 一次改一类,改前必须取证;
- 必验尸------用验证 Prompt 跑编译 + lint,红了就回退。
这四步走下来,AI 的重构质量会从「碰运气」变成「可预期」。而分层的收益也会随着边界闭合,一点一点地显形出来------这才是「驱动 AI」和「依赖 AI」的本质区别。
Thanks
以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。 如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧,您的鼓励是我前进的动力。 谢谢~~