我让 AI 给老项目做了一次分层架构升级:对标 Now in Android,从「穿层」到「端口与适配器」

前言

一个成熟 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 的分层准则提炼成下面这张图,这就是我重构的目标态:

graph TD subgraph UI["UI 层(feature)"] Screen["Screen / Composable"] VM["ViewModel"] end subgraph Domain["Domain 层(core:domain)"] UC["UseCase"] Port["Repository 接口(端口)"] end subgraph Data["Data 层(core:data)"] Impl["Repository 实现(适配器)"] DS["DataSource"] end subgraph Infra["基础设施(core:database / network)"] Room[("Room · SSOT")] Net["Network"] end Screen --> VM VM --> UC UC --> Port Impl -.->|"实现(依赖倒置)"| Port Impl --> DS DS --> Room DS --> Net

这张图的核心就一句话:箭头只能朝「端口(接口)」走,不能朝「具体类」走。 这是判断分层是否健康的唯一标准。

1.2 差距诊断:fragmject 的「穿层点」

拿着这把尺子去量 fragmject,我让 AI 做了全项目依赖扫描 ,很快定位到几个「边界穿洞」的地方。这才是本次重构的方向和目标清单:

graph LR subgraph Before["重构前:边界穿孔"] A1["SystemScreen"] -->|"越权抓取"| A2["MainViewModel"] B1["WebView 组件"] -->|"绕接口走后门"| B2["WebViewManager 实现类"] C1["RepositoryModule"] -->|"15 个工厂样板"| C2["@Provides"] end subgraph After["重构后:边界闭合"] D1["SystemScreen"] --> D2["SystemViewModel"] D2 -->|"依赖端口"| D3["NavigationRepository 接口"] E1["WebView 组件"] -->|"依赖接口"| E2["WebViewPool 接口"] F1["RepositoryModule"] -->|"17 个抽象桥接"| F2["@Binds"] end

有了这张「差距图」,重构的方向和优先级就定死了:先修穿层(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 完整驱动流程(一张图看懂)

graph LR A[&#34;① 评审 Prompt<br/>输出问题清单&#34;] --> B[&#34;人审核分级<br/>P0 优先&#34;] B --> C[&#34;② 修复 Prompt<br/>一次修一类&#34;] C --> D[&#34;③ 验证 Prompt<br/>编译 + lint&#34;] D --> E{&#34;通过?&#34;} E -->|&#34;否,回退重改&#34;| C E -->|&#34;是&#34;| F[&#34;进入下一领域<br/>(下一轮评审)&#34;]

四、实战一:拆除跨层越权 ------ 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 做分层重构不是「把活丢给它」,而是「先对标定方向,再把流程交给它,把判断留给自己」。

完整的动作链,其实就是这四步:

  1. 先对标------拿 NIA 当尺子,画出「目标态」和「差距图」;
  2. 再评审------用评审 Prompt 让 AI 出问题清单,人审核分级;
  3. 小步修------用修复 Prompt 一次改一类,改前必须取证;
  4. 必验尸------用验证 Prompt 跑编译 + lint,红了就回退。

这四步走下来,AI 的重构质量会从「碰运气」变成「可预期」。而分层的收益也会随着边界闭合,一点一点地显形出来------这才是「驱动 AI」和「依赖 AI」的本质区别。

Thanks

以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。 如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧,您的鼓励是我前进的动力。 谢谢~~

源代码地址

相关推荐
youcans_14 小时前
【嵌入式软件AI编程】22. STM32 基础实验:GPIO外部中断
stm32·单片机·嵌入式硬件·ai编程·claude
Flynt15 小时前
AI 写的页面一眼假?有人把"假指纹"写成 62 条规则,不用大模型就能跑
前端·ai编程·claude
lazy熊15 小时前
Codex CLI 配置教程
ai编程
ServBay15 小时前
本地模型已经跑起来了,Copilot Fusion 为什么还要联网?
后端·ai编程·github copilot
北京自在科技16 小时前
谷歌安卓 Gemini 迎来界面改版,Find Hub 接入 Gemini 支持物品记忆查找
android·安卓
qinglongcn16 小时前
三维度 × 四分类——CycleDetector 的完整检测算法
agent·ai编程
熊猫钓鱼>_>17 小时前
鸿蒙PC构建实战:hvigor modelVersion 降级 + schema 清理 + ArkTS 编译三连坑完整复盘
开发语言·华为·ai编程·harmonyos·openharmony·数据·ai生成
骇客野人17 小时前
从大模型对话聊天都人工智能智能体干活到如今的AI编程的思考以及未来发展趋势展望
人工智能·ai编程
小虎AI生活17 小时前
59.3k Stars 的 diagram-design 实战:接入 WorkBuddy,让 AI 按 4px 网格自动出架构图
aigc·ai编程
YB137518 小时前
Jetpack Compose 动画Modifier.animateItem()
android