有时候我会觉得,AI 写 Android 代码这件事就像让一个刚毕业的实习生去写核心业务模块。你说他不专业吧,他确实能又快又完整地交出代码;你说他能直接上线吧,你又总能在路由、生命周期、内存、异常这几个地方找到大大小小的坑。尤其当你把 AI 生成的代码直接跑起来,发现它第一眼看着很正常,第二眼就觉得不对劲,第三眼就崩溃了。
我用了挺久 AI 辅助写 Android 应用,也帮过不少初学者调试 AI 生成的项目。一个非常直观的感受是:AI 写代码最常见的错误,并不是语法错误,反而是一类更深层的工程问题——它总能把“看起来正确的代码”和“真正能稳定运行的代码”越拉越远。这个问题不解决,AI 就只是一个快速制造 bug 的工具。这篇文章,我整理了 6 个我在 Android 项目里反复见到的错误的修复方案,每个都配了具体的代码场景和排查思路。
1. 先搞清楚:AI 写 Android 代码,问题到底出在哪
AI 生成 Android 代码之所以容易犯错,最根本的原因在于 Android 不是一个普通的 Java/Kotlin 环境。它有一套自己的生命周期、进程模型、UI 刷新机制和资源管理方式。这些约束很难被模型完整理解,尤其是当你的需求描述不够具体,或者它只记住了某个经典教程的片段时,就会出现“编译通过、逻辑看着对、跑起来就崩”的情况。
1.1 AI 代码为什么看起来没问题
你让 AI 写一个“从网络获取数据并显示到列表”的功能,它通常会直接给你一套比较标准的 Retrofit + RecyclerView 代码。这段代码如果放在单个 Activity 里,配合简单的调用,确实有可能跑通。但问题在于,真实 Android 开发里,你不会永远停留在“一个页面、一个按钮、一次请求”的演示场景。
AI 很容易忽略掉一些 Android 特有的隐含约定。比如:
- 页面旋转或暗黑模式切换时,Activity 会被重建,旧数据不能直接保存在成员变量里。
- 网络请求如果放在主线程,就会出现 NetworkOnMainThreadException。
- Activity 销毁后还在回调更新 UI,就会出现 View 泄漏甚至空指针。
- 第三方 SDK 的初始化和权限申请,不能想当然地放在启动时。
这些不是 Kotlin 语言会告诉你的规则,而是 Android 框架的运行常识。AI 在生成代码时,由于训练数据里充满了各种“简化的示例”,很容易产生一种“能用就行”的幻觉——它更擅长生成 Demo,而不是生产级实现。
1.2 真正的边界:Android 不是通用编程环境
如果你让 AI 写一个算法、一个文件处理工具,或者一段数据清洗逻辑,它通常能做得很好,因为这类问题对运行环境不敏感。可一旦进入 Android 环境,所有代码都要重新回答几个问题:
- 它在哪个生命周期阶段被执行?
- 它能不能访问 UI 线程?
- 它持有的上下文是短期还是长期?
- 配置变化后,它还能不能恢复状态?
- 异常发生时,用户会看到什么?
我见过一个案例:AI 生成了一段使用 Application 保存全局缓存的代码,逻辑上没错,但它在处理大图和列表时没有做内存回收,导致应用频繁 OOM。这不是 AI 不聪明,而是它不知道你的业务在真实场景下会积累多少数据。所以,AI 写 Android 代码的正确打开方式,是你先清楚框架边界,再根据这个边界去审查和修改它的产物。
2. 错误一:生命周期和上下文引用,根基就错了
AI 写代码时最常见的第一个错误,是乱用 Context,尤其是把 Activity 的 Context 传递给一个单例对象,或者在一个异步回调里继续持有 Activity。
2.1 典型表现:在 Activity 销毁后仍持有上下文
看下面这段我经常见到的 AI 生成代码:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val client = NetworkClient.getInstance() // 单例 client.startDownload(this) // 直接传 Activity 给长期对象 } }如果NetworkClient内部把这个 Activity 存进了一个成员变量,并且下载任务持续很长时间,那么当用户退出页面后,这个 Activity 依然无法被回收。如果你还开启了广播、监听器或者协程,最后很可能演变成内存泄漏,甚至应用被系统杀掉。
2.2 修复方案:用生命周期感知协程,或者 ViewModel
正确的做法是:不要直接把 Context 存进长生命周期对象。如果必须使用 Context,优先考虑applicationContext;如果一定要做异步任务,就让协程感知生命周期。
class MainViewModel : ViewModel() { private val repository = NetworkRepository() fun startDownload() { viewModelScope.launch { repository.downloadToCache() // 内部用 Dispatchers.IO _state.update { ".done" } } } }ViewModel 会在 Activity 销毁时自动清理协程,这是 Android 官方设计里最应该被 AI 代码复用的机制。每当我看到 AI 生成的代码里出现GlobalScope或Handler手动发消息时,我都会先怀疑是否绕过了生命周期管理。
审查代码时第一件事:检查有没有任何地方持有 Activity 或 Fragment 的引用超过生命周期。如果有,优先改成 ApplicationContext 或 ViewModel 作用域。
3. 错误二:主线程做耗时任务,卡 UI 和内存泄漏一起出现
第二个高频错误,是在主线程做网络请求、数据库写入、文件解析等耗时操作。AI 为了让你快速跑通 demo,经常把网络请求直接写在onCreate里,并且没有任何线程切换。
3.1 典型表现:直接在主线程调用网络请求
以下这段代码就属于典型的 AI 初学者友好写法,但它会在 API 26 以上的模拟器上直接抛出NetworkOnMainThreadException,在真机上可能会卡住 UI 几秒钟,甚至触发 ANR:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val data = fetchData() // 新线程?其实没有 textView.text = data } }即使你运气好,没有崩溃,这对用户来说依然是一种灾难:界面完全卡住,点击没反应,系统弹窗提示“继续等待”还是“关闭应用”。AI 不知道你的真实环境是否符合它假设的“快速返回”,所以它写出的同步逻辑会直接把你带进坑里。
3.2 修复方案:协程 + 生命周期作用域 + 正确调度器
在 Kotlin 里,最自然的写法是使用协程。你需要确保两个点:一是耗时操作在后台线程,二是结果回到主线程更新 UI。
class MyViewModel : ViewModel() { private val _uiState = MutableStateFlow<UiState>(UiState.Loading) val uiState: StateFlow<UiState> = _uiState fun load() { viewModelScope.launch { val result = withContext(Dispatchers.IO) { fetchFromNetwork() } _uiState.value = UiState.Success(result) } } }如果你想用更底层的 Thread + Handler,也不是不行,但你需要自己处理取消、异常和生命周期绑定。AI 生成的代码里,如果你看到Thread { ... }.start()和runOnUiThread频繁出现,就要提高警惕。这类写法不是说不能运行,而是它的复杂度很快会失控,尤其是叠加了很多嵌套回调后,你几乎没法调试。
4. 错误三:状态恢复和配置变更,直接用旧的静态数据
第三个错误是几乎没有 AI 默认会考虑到的:Activity 旋转、语言切换、深色模式切换时,Activity 会被销毁重建。如果你把数据保存在 Activity 的成员变量里,一旦配置变化,所有数据都会丢失。
4.1 典型表现:旋转屏幕/暗黑模式切换以后数据丢失
比如你让 AI 写一个“加载用户信息”的页面,它可能会把结果直接放在activity.userData里。旋转屏幕后,原 Activity 销毁,新 Activity 重新执行onCreate,以前的userData已经是空的了。
AI 不会主动告诉你这个坑,因为它生成的代码在竖屏下测试是正常的。可当用户横屏查看照片,或者收到系统通知触发配置变化时,问题立刻暴露。
4.2 修复方案:ViewModel + SavedStateHandle + onSaveInstanceState
Android 官方的做法是,把跨配置变化的数据放到 ViewModel 中。ViewModel 在 Activity 重建时不会销毁,所以数据可以存活。如果需要应对进程被系统杀死,还要配合SavedStateHandle。
class UserViewModel( private val savedStateHandle: SavedStateHandle ) : ViewModel() { private val _userData = MutableLiveData<User>() val userData: LiveData<User> = _userData fun loadUser() { val cached = savedStateHandle.get<String>("user_json") if (cached != null) { _userData.value = parseUser(cached) return } viewModelScope.launch { val user = api.getUser() _userData.value = user savedStateHandle["user_json"] = user.toJson() } } }我在实际项目里,长期把“配置变更后数据还在不在”作为检验 AI 代码是否合格的标准之一。如果 AI 生成的代码里没有一个 ViewModel 的影子,我基本会认定它只是一个 demo。
5. 错误四:模型幻觉和过时的 API,编译过了却翻车
第四个错误很有意思,它来自模型本身的特性。AI 在训练时会看到很多版本的 API 和第三方库,所以它极容易写出一段在当年某个版本里有效、但现在已经被废弃的代码。它甚至可能“想象”出一个不存在的函数名,让编译器能通过,但运行时找不到对应定义。
5.1 典型表现:用了一个已经被废弃的接口,或者无中生有方法名
假设你让 AI 写“保存图片到相册”的功能,它可能会给你一个类似MediaStore.Images.Media.insertImage的旧 API。这个 API 在 Android 10 之前确实可用,但到 Android 11 之后,如果没处理好作用域存储,就会出现异常。还有一种情况是 AI 会给你一个看起来像官方 API 的方法,例如activity.widget.isVisible = true,但它其实是某篇博客里的扩展函数,你的工程里根本没有这个函数。
5.2 修复方案:依赖版本锁定 + 官方文档比对 + 编译日志逐条核对
针对这个错误,我的建议是建立一套固定的审查流程:
- 先锁定项目的
compileSdk、minSdk、targetSdk和依赖库版本。 - 让 AI 明确标注它用的是哪个版本。
- 把你的 Android 版本策略写进 prompt,例如“基于 compileSdk 34、targetSdk 34”等。
- 编译日志不要只看错误提示,还要逐条检查 warning,尤其要看 deprecated。
- 对不确定的 API,直接对照官方文档或源码确认,不要信任模型给出的代码引用。
实际落地时,我会先把全工程的依赖清单建好,然后要求 AI 生成代码时不许导入未出现在清单里的库。如果它非要导入,就必须解释用途和版本。这样能堵住大部分“幻觉 API”的问题。
审查代码时不要只问“能不能编译”,要问“它用的 API 在 targetSdk 下是否还安全”。AI 很难主动意识到你的目标版本策略。
6. 错误五:把架构写成了“堆好 coude”,依赖注入和分层全被吃掉
第五个错误比较隐蔽。AI 在输出代码时,为了让人容易看懂,非常喜欢把所有逻辑堆到一个 Activity 或 Fragment 里。页面上有几个交互按钮还好,一旦逻辑多了,Activity 就会变成几百行,甚至上千行的“上帝对象”。
6.1 典型表现:MainActivity 几百行,Repository 和 ViewModel 都不存在
比如我有一个项目,让 AI 生成“获取远端列表,下拉刷新,点击进入详情”的功能。它直接在一个 Fragment 里完成了 Provider、网络请求、状态管理、列表适配器和点击事件处理,代码量超过 600 行。表面上看一切都正常,但我想复用其中某个网络请求时,却发现它被嵌套在 Fragment 的内部回调里,根本抽不出来。
这种写法的真正问题不是“代码不优雅”,而是它让后续维护、测试和复用都变得极其困难。比如我想对网络层加一个统一的超时重试,或者对列表项做个埋点,都需要在 600 行里挖缝拼补,稍不小心就会改出新 bug。
6.2 修复方案:用 Hilt 或手动注入,先想好数据流
架构问题不是靠某段 AI 代码能解决的,需要你在 prompt 阶段就给出约束。一个比较好用的 prompt 模板是:
请为 Android 项目生成代码,要求: - 使用 ViewModel + UiState 管理界面状态; - 使用 Repository 封装数据源; - 使用 Hilt 进行依赖注入; - Activity 和 Fragment 只负责 UI 展示和事件转发; - 不要写超过 80 行的函数。按照这个约束生成的代码,即使不完美,也不会把 Activity 变成垃圾场。如果 AI 不使用依赖注入,你也可以手动做一个简单工厂:
object AppContainer { private val apiService: ApiService by lazy { Retrofit.Builder().baseUrl("...").build().create(ApiService::class.java) } fun provideRepository(): MainRepository = MainRepository(apiService) }用这个容器来提供 Repository,至少能保证“换掉网络库”“替换测试数据源”这类需求时不需要改写大量 UI 代码。在我看来,AI 写代码最有价值的部分并不是“一口气生成所有层”,而是“精确生成某一层的某个逻辑模块”。
7. 错误六:只测 happy path,不测边界和异常
最后一个错误,也是最容易被忽略的一个:测试。AI 生成代码时默认一切都正常,网络会通、数据有值、权限已授权、文件存在、列表不为空。但真实用户操作往往会走到相反的路径:没网络、权限被拒绝、列表为空、数据格式异常。
7.1 典型表现:只是点击按钮调通了就以为完事
很多初学朋友让 AI 写代码,跑通后就很开心,直接丢到项目里。但 AI 生成代码里通常没有异常处理。一旦服务端返回一个空字段,或者用户快速点击多次按钮,app 就会崩溃或产生重复请求。
我见过一个比较极端的例子:AI 生成的购物车加购功能,没有做“点击频率控制”,用户快速点了 10 次“加入购物车”,前端就发了 10 个请求,最后生成了 10 条重复订单。这不是 AI 的锅,它只是忠实地按照“点击一次加一次”去实现,你如果没有告诉他“要防止重复提交”,他就会漏掉这个约束。
7.2 修复方案:单元测试 + 集成测试,至少要覆盖五个场景
要为 AI 代码补测试,不一定要一开始就引入复杂的测试框架。可以先做最小粒度的检查:
- 空数组 / 空字符串输入
- 网络超时或 DNS 失败
- 权限拒绝
- 快速重复点击
- Activity 重建后继续回调
这些是普通 Android 业务里最容易挂掉的五个场景。你可以把这些要求写进 prompt,比如:“请帮我给这个 ViewModel 写单元测试,覆盖加载成功、加载失败、数据为空和重复提交四个分支。”
同时,不要只依赖 AI 自动生成的测试,因为它会倾向于让测试成为正确路径的宣言。我更建议你先手动运行一次,再用 JUnit + MockWebServer 模拟接口,最后检查日志和内存。只有看到失败路径能正常降级,才算是真的稳定。
8. 把 AI 写代码变成流程,而不是赌运气
讲完这 6 个错误,我发现它们本质上都能归结到同一个问题:你把 AI 当成了什么。如果你把 AI 当成一个“自动补全代码生成器”,那它就只负责生产代码,不负责保证质量;如果你把 AI 当成一个“编外工程师”,你就得学会给它写 PRD、做代码评审、补边界测试、盯上线日志。前一种心态,适合做原型、做验证、做答题;后一种心态,才是把它放进真实项目里该有的方式。
8.1 一套可复用的检查和修复流程
在我自己的 Android 项目里,凡是 AI 生成的代码,我都会走下面这五步流程:
- 跑通最小构建。不要一次塞给它整个模块,先跑一个页面或一个功能的最小闭环。
- 做静态审查。重点检查 Context、线程、生命周期、权限、异常处理。
- 构造异常场景。关掉网络、拒绝权限、旋转屏幕,看会不会崩溃。
- 明确架构归属。把它放进 Repository / ViewModel / UI 三层中,而不是留在 View 层。
- 形成回归脚本。至少保留一个单元测试或集成测试,避免 AI 下次再生成类似代码踩进同一个坑。
这套流程不复杂,但能解决绝大多数“AI 代码不可用”的问题。
8.2 哪些场景适合 AI 生成,哪些场景不适合
根据我的经验,AI 在 Android 开发里更适合做的包括:
- 生成单个组件的实现(例如一个水波纹按钮、一个分页卡片)
- 生成重复性代码(例如基于模板的 RecyclerView.Adapter)
- 生成基础的网络请求和数据解析层
- 生成单元测试和 Mock 数据
相对不适合做的场景:
- 需要跨页面共享状态的复杂业务逻辑
- 需要处理大量安全策略的代码(登录、支付、敏感权限)
- 必须严格对齐产品规则的状态机
- 对性能和内存要求极高的算法或渲染逻辑
如果非要在这些不适合的场景里使用 AI,也要把它生成的内容当作“草稿”,然后花更多时间做代码评审和手工验证。
8.3 长期维护和代码进化建议
AI 写代码不仅是一次性输入,还会进入项目的长期维护。这里我建议你把 AI 生成代码时的“约束条件”也当作代码仓库的一部分来管理。比如把项目所需的 SDK 版本、架构规范、依赖版本、命名规范都写成一个文档,在每次读项目时让 AI 先读它,再生成代码。
这样做的好处是,AI 后续生成的代码会更贴近你项目的当前状态,而不是某个公开仓库里的旧示例。你会慢慢发现,随着你对 prompt 的打磨和对代码审查流程的固定,AI 写代码的错误率会明显下降,留下来的更多是可直接复用、长期可维护的工程片段。
说到底,AI 写 Android 代码不是一个“能不能用”的命题,而是一个“你会不会用”的命题。把生命周期、线程、状态、架构、异常测试这 6 个坑看成你的检查清单,用它去约束 AI,也用它去审视自己。这样,你才能真正把 AI 变成提高效率的杠杆,而不是每天帮你产生更多 bug 的“压力来源”。