一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

用 AI 重构 Android 老项目:参照 Now in Android 落地端口与适配器

时间:2026-09-18 18:30:01 编辑:袖梨 来源:一聚教程网

多模块 Android 项目即使目录划分清晰,也可能隐藏着 Screen 跨作用域读取其他 ViewModel、组件绕过接口依赖具体实现等问题。这些缺陷平时未必报错,却会逐渐增加测试、替换实现和协作开发的成本。下面将以 Now in Android 为参照,梳理如何借助 AI 分阶段完成边界诊断、架构修复与结果验证。

前言

一个成熟 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 —— 逼它只诊断、不动手

你是一名资深 Android 架构师。现在对 [项目名] 做一轮「分层架构评审」。

【对标基准】
以 Google Now in Android 的分层准则为标尺,核心判据只有一条:
「依赖箭头只能指向端口(接口),不能指向具体类。」

【评审范围】
[填入本次聚焦的领域,例如:core:data 与 feature 的边界]

【要求】
1. 逐项列出发现的问题,格式固定为:
   - 严重级别(P0=边界穿孔/正确性,P1=契约不一致/冗余,P2=可维护性,P3=风格)
   - 问题描述(一句话说清「谁越权依赖了谁」)
   - 影响面(会导致什么后果)
   - 修复方向(只给方向,不给完整代码)
2. 最后单列一节「已经做对、不应改动的设计」,防止误伤。

【铁律】
本次只输出评审报告,绝对不要修改任何代码。

关键在最后一句「绝对不要修改任何代码」。这逼着 AI 停下来「取证」(读文件、grep 引用点),而不是凭印象输出。评审报告是人可以 review 的,风险被前置拦截了。

3.2 修复 Prompt —— 一次只修一类

基于刚才的评审报告,现在只修复 [P0-1:SystemScreen 越权访问 MainViewModel] 这一项。

【执行步骤】
1. 先重新读取目标文件的当前内容,确认依赖关系和所有引用点;
2. 给出最小改动方案,原则:
   - 依赖方向必须改为「指向端口」,禁止引入新的具体类依赖;
   - 不动无关代码,不改命名习惯;
3. 改完后列出「受影响的调用方」,确保没有遗漏。

【铁律】
一次只修复这一项。修复前必须先读文件确认当前状态,禁止凭记忆编辑。

这一句「修复前必须先读文件」,是防止 AI 基于「过期的记忆」去改已经被改过的文件——这是 AI 重构翻车的头号原因。

3.3 验证 Prompt —— 把幻觉挡在主干外

刚才的改动已应用。现在执行验证闭环,任何一步失败都要停止并报告原因:

1. 静态检查:read_lints 查看目标文件是否有报错;
2. 编译验证:./gradlew :[模块]:compileDebugKotlin 必须通过;
3. 依赖复核:grep 确认旧的具体类依赖已清零、新的端口依赖已生效。

请按顺序执行,输出每步结果。

这条铁律让 AI 的「幻觉代码」在进主干前就被拦下了。 没有验证闭环的 AI 重构,等于裸奔。

3.4 完整驱动流程(一张图看懂)

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

四、实战一:拆除跨层越权 —— Screen 不再「借」别人的 ViewModel

这是本轮最典型的一处「分层穿洞」。

旧代码SystemScreen父组合作用域去抓 MainViewModel 的数据:

// 旧: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 被弹出后再进入 SystemNavKeyviewModel()重新构造一个新的 MainViewModeltreeResult 会被重复请求

新代码:把「体系树」归还给它真正的 owner——SystemViewModel 自己注入领域端口:

// 新:SystemScreen 只依赖自己的 SystemViewModel
@Composable
fun SystemScreen(
    cid: String,
    systemViewModel: SystemViewModel = viewModel(),  // ← 自己的 ViewModel
    onNavigate: (key: NavKey) -> Unit = {},
    onNavigateUp: () -> Unit = {},
) {
    val treeResult by systemViewModel.treeResult.collectAsStateWithLifecycle()
    // ...
}

SystemViewModel 依赖的是领域端口,不再「借」任何人的数据:

@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

// 旧:接口契约不完整
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」的耦合:

// 新:接口契约完整
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 工厂方法绑定:

// 旧:@Provides 为每个 Repository 生成一个 Factory 类
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
    @Provides
    @Singleton
    fun provideUserRepository(remote: UserRemote, store: UserStore): UserRepository =
        UserRepositoryImpl(remote, store)
    // ... 其余 14 个都是这样的样板
}

新代码:改成 @Binds 抽象方法,让 Dagger 自己推导:

// 新:@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 了一遍:

grep_search project(":core:(data|database|network)")  →  feature/ 下零命中

含义:所有 Feature 模块完全不依赖数据层,只依赖 core:domain(端口)+ core:model(实体)。这是 Clean Architecture 依赖倒置原则的教科书级落地——业务逻辑不知道数据从哪来,只知道「我要一个 NavigationRepository

7.2 端口 + 适配器的标准范式

NavigationRepository 为例,三层分工极其清晰:

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

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

源代码地址

  • android/nowinandroid(本文对标基准)
  • miaowmiaow/fragmject
  • 本文涉及的关键文件:
    • SystemScreen.kt
    • SystemViewModel.kt
    • WebViewPool.kt
    • NavigationRepository.kt
    • OfflineFirstNavigationRepository.kt
    • RepositoryModule.kt

热门栏目