33e - 移动端 Steering 规则与反模式
本文是《AI Agent 实战手册》第 33 章第 5 节。 上一节:AI辅助移动端UI | 下一节:AI辅助游戏开发概览
概述
移动端开发是 AI 代码生成最容易”翻车”的领域之一——AI 生成的代码在模拟器上看起来完美,但部署到真机后可能出现内存泄漏导致闪退、后台任务疯狂耗电、权限请求缺失导致崩溃、离线场景完全不可用、iOS 和 Android 行为不一致、以及包体积膨胀到用户拒绝下载等严重问题。2025-2026 年的实践表明,移动端 AI 生成代码的质量问题率高达 45-55%,远高于 Web 前端的 30-40%,根本原因在于移动端有更多平台特定约束、资源限制和系统级交互。本节提供完整的移动端 Steering 规则模板(覆盖 CLAUDE.md、Kiro Steering、Cursor Rules)、八大反模式的深度剖析(含问题代码 vs 正确代码对比),以及针对 iOS(SwiftUI)、Android(Jetpack Compose)、React Native 和 Flutter 四大平台的专用规则。
1. 移动端 Steering 规则概述
1.1 为什么移动端需要专用 Steering 规则
移动端开发与 Web 开发有本质区别,AI 在以下方面特别容易犯错:
| 维度 | Web 开发 | 移动端开发 | AI 常见失误 |
|---|---|---|---|
| 内存管理 | 浏览器自动 GC,页面刷新即释放 | 受限内存,后台可被系统杀死 | 不释放监听器、不取消订阅、循环引用 |
| 电池 | 不关心 | 核心用户体验指标 | 频繁网络请求、持续定位、不必要的后台任务 |
| 权限 | 浏览器统一处理 | 运行时权限,平台差异大 | 缺少权限检查、不处理拒绝、不解释用途 |
| 网络 | 通常稳定 | 弱网/离线是常态 | 无离线策略、无重试、无缓存 |
| 平台差异 | 浏览器差异较小 | iOS/Android 行为差异巨大 | 只测一个平台、忽略平台规范 |
| 包体积 | 可懒加载 | 安装前必须下载完整包 | 引入过多依赖、未压缩资源、未分包 |
| 生命周期 | 页面加载/卸载 | 复杂的前后台切换 | 不处理生命周期、后台状态丢失 |
| 安全 | HTTPS + CSP | 本地存储、证书固定、代码混淆 | 明文存储敏感数据、缺少证书固定 |
1.2 Steering 规则工具对比
| 工具 | 规则文件位置 | 价格 | 移动端适用性 | 适用场景 |
|---|---|---|---|---|
| Claude Code | CLAUDE.md(项目根目录) | Max $100/月起 / API 按量 | ★★★★★ | 大型移动项目,多文件重构 |
| Kiro | .kiro/steering/*.md | 免费(预览期) | ★★★★★ | 分层规则,按平台/模块分区 |
| Cursor | .cursor/rules/*.mdc | 免费 / Pro $20/月 | ★★★★☆ | 日常移动端编码 |
| GitHub Copilot | .github/copilot-instructions.md | $10/月(Individual) | ★★★☆☆ | 代码补全级辅助 |
| Windsurf | .windsurfrules | 免费 / Pro $15/月 | ★★★☆☆ | 轻量级移动端开发 |
| Xcode 26 AI | Xcode 内置上下文 | 免费(Xcode 内置) | ★★★★★ | iOS 原生开发专用 |
| Android Studio Gemini | Studio 内置上下文 | 免费(内置 Gemini) | ★★★★★ | Android 原生开发专用 |
1.3 移动端 Steering 规则架构
项目根目录/
├── CLAUDE.md # 全局规则(通用约束)
├── .kiro/steering/
│ ├── mobile-general.md # 移动端通用规则(always)
│ ├── ios-swiftui.md # iOS 专用规则(auto: *.swift)
│ ├── android-compose.md # Android 专用规则(auto: *.kt)
│ ├── react-native.md # RN 专用规则(auto: *.tsx in src/)
│ └── flutter.md # Flutter 专用规则(auto: *.dart)
├── .cursor/rules/
│ ├── mobile-rules.mdc # Cursor 移动端规则
│ └── platform-specific.mdc # 平台特定规则
└── .github/
└── copilot-instructions.md # Copilot 移动端指令2. 完整 Steering 规则模板
2.1 CLAUDE.md 移动端通用规则模板
以下是适用于跨平台移动端项目(React Native + Expo)的完整 CLAUDE.md 模板:
# CLAUDE.md — 移动端项目规则
## 项目概述
- 框架:React Native 0.79+ / Expo SDK 53
- 语言:TypeScript 5.x(strict mode)
- 导航:React Navigation v7 / Expo Router v4
- 状态管理:Zustand(本地)+ TanStack Query(服务端)
- 样式:StyleSheet.create(禁止内联样式对象)
- 动画:react-native-reanimated v3(原生线程动画)
- 手势:react-native-gesture-handler v2
- 存储:expo-secure-store(敏感数据)+ MMKV(一般数据)
- 测试:Jest + React Native Testing Library + Detox(E2E)
- 最低支持:iOS 16+ / Android API 26+(Android 8.0)
## 内存管理规则(最高优先级)
- 所有 useEffect 必须返回清理函数,取消订阅/移除监听器/清除定时器
- 禁止在组件外部持有组件引用(会导致组件无法被 GC)
- 图片必须使用 expo-image 或 react-native-fast-image,禁止原生 Image 加载大图
- FlatList/FlashList 必须设置 removeClippedSubviews={true}
- 长列表必须使用 FlashList(替代 FlatList),设置 estimatedItemSize
- 禁止在 renderItem 中创建新的函数/对象引用(使用 useCallback/useMemo)
## 电池优化规则
- 定位:仅在必要时请求,使用 foregroundService 而非持续后台定位
- 网络:批量请求,使用 NetInfo 检测网络状态后再发起请求
- 动画:使用 useAnimatedStyle(原生线程),禁止 JS 线程驱动动画
- 后台任务:使用 expo-background-fetch,设置合理的 minimumInterval
- 传感器:使用后立即停止监听,禁止持续监听加速度计/陀螺仪
## 权限处理规则
- 所有权限请求前必须显示自定义说明弹窗(解释为什么需要)
- 必须处理三种状态:granted / denied / blocked
- 权限被拒绝时必须提供降级方案(不能崩溃或白屏)
- 权限被永久拒绝时引导用户到系统设置页
- iOS Info.plist 和 Android AndroidManifest.xml 必须声明所有使用的权限
- 禁止在应用启动时一次性请求所有权限(按需请求)
## 离线支持规则
- 所有 API 请求必须有离线降级策略
- 关键数据必须本地缓存(使用 TanStack Query 的 persistQueryClient)
- 网络状态变化时自动重试失败的请求
- 离线时显示明确的离线状态指示器
- 表单数据在提交失败时必须本地保存,网络恢复后自动重试
## 平台一致性规则
- 使用 Platform.select() 处理平台差异,禁止 Platform.OS === 'ios' 的 if-else
- 导航栏样式必须分别适配 iOS(大标题)和 Android(Material)
- 触觉反馈:iOS 使用 expo-haptics,Android 使用 Vibration API
- 日期选择器:iOS 使用滚轮样式,Android 使用日历样式
- 状态栏:iOS 默认浅色,Android 需要手动设置 translucent
## 包体积控制规则
- 禁止引入 lodash 全量包(使用 lodash-es 或单独导入)
- 图片资源必须压缩(PNG 使用 pngquant,JPEG 质量 ≤ 80%)
- 使用 expo-asset 延迟加载非首屏资源
- 字体文件仅包含使用的字重(禁止引入完整字体族)
- 每次添加新依赖前检查包体积影响(使用 bundlephobia)
- 启用 Hermes 引擎(React Native 默认)
- 使用 ProGuard/R8(Android)和 Bitcode(iOS)进行代码压缩
## 安全规则
- 敏感数据(token、密码、API key)必须存储在 expo-secure-store
- 禁止在 console.log 中输出敏感信息(生产环境移除所有 console)
- API 请求必须使用 HTTPS,生产环境启用证书固定
- 禁止在代码中硬编码 API 密钥(使用环境变量)
- 启用代码混淆(Android ProGuard / iOS 编译优化)
## 无障碍规则
- 所有可交互元素必须设置 accessibilityLabel
- 图片必须设置 accessibilityRole="image" 和描述性 label
- 表单控件必须关联 accessibilityHint
- 支持 Dynamic Type(iOS)和系统字体缩放(Android)
- 最小触摸目标:44x44pt(iOS)/ 48x48dp(Android)2.2 Kiro Steering 移动端分层规则
通用移动端规则(.kiro/steering/mobile-general.md)
---
inclusion: always
---
# 移动端通用 Steering 规则
## 生命周期管理
- 组件卸载时必须清理所有副作用(定时器、订阅、监听器)
- 应用进入后台时暂停非必要操作(动画、轮询、传感器)
- 应用恢复前台时刷新过期数据
- 处理低内存警告(iOS didReceiveMemoryWarning / Android onTrimMemory)
## 网络请求
- 所有请求必须设置超时(默认 15 秒)
- 必须处理网络错误并显示用户友好的错误信息
- 使用指数退避重试策略(最多 3 次)
- 大文件上传/下载必须支持断点续传
## 导航
- 深层导航必须支持 deep linking
- 返回手势不能导致数据丢失(未保存的表单需确认)
- 导航栈不能无限增长(使用 replace 替代 push 在适当场景)
## 测试
- 每个屏幕至少一个快照测试
- 关键业务流程必须有 E2E 测试
- 权限相关功能必须有 mock 测试
- 离线场景必须有专门的测试用例iOS SwiftUI 专用规则(.kiro/steering/ios-swiftui.md)
---
inclusion: auto
globs: ["**/*.swift"]
---
# iOS SwiftUI Steering 规则
## SwiftUI 最佳实践
- 使用 @Observable 宏(iOS 17+)替代 ObservableObject
- 视图体(body)中禁止执行副作用或复杂计算
- 使用 .task {} 替代 .onAppear + async 模式
- 列表使用 List + LazyVStack,禁止在 ScrollView 中嵌套 ForEach 渲染大量视图
- 图片加载使用 AsyncImage 或第三方库(Kingfisher/Nuke),设置缓存策略
## iOS 26 Liquid Glass 适配
- 使用 .glassEffect() 修饰符实现毛玻璃效果
- 导航栏使用系统默认的 Liquid Glass 样式
- TabBar 使用 .tabBarStyle(.automatic) 自动适配
- 避免自定义导航栏覆盖系统 Liquid Glass 效果
## 内存管理
- 使用 [weak self] 在闭包中避免循环引用
- Combine 订阅必须存储在 cancellables Set 中并在 deinit 中取消
- 大图片使用 UIImage.preparingThumbnail(of:) 降采样
- Core Data 使用 NSBatchDeleteRequest 批量删除,避免逐条删除
## 权限(iOS 特定)
- Info.plist 必须包含所有权限的 Usage Description
- 使用 PHPhotoLibrary.authorizationStatus(for:) 检查照片权限级别
- 定位权限区分 whenInUse 和 always,优先请求 whenInUse
- 推送通知使用 UNUserNotificationCenter.requestAuthorization
## App Store 合规
- 禁止使用私有 API
- 必须提供 App Privacy 标签所需的隐私信息
- 应用内购买必须使用 StoreKit 2
- 登录功能必须支持 Sign in with AppleAndroid Jetpack Compose 专用规则(.kiro/steering/android-compose.md)
---
inclusion: auto
globs: ["**/*.kt"]
---
# Android Jetpack Compose Steering 规则
## Compose 最佳实践
- Composable 函数必须是无副作用的(副作用使用 LaunchedEffect/SideEffect)
- 使用 remember {} 缓存计算结果,避免重组时重复计算
- 列表使用 LazyColumn/LazyRow,设置 key 参数优化重组
- 状态提升:UI 状态由 ViewModel 持有,Composable 只负责渲染
- 使用 derivedStateOf {} 避免不必要的重组
## Material 3 Expressive 适配
- 使用 MaterialTheme.colorScheme 获取颜色,禁止硬编码颜色值
- 动态颜色:使用 dynamicDarkColorScheme/dynamicLightColorScheme(Android 12+)
- 组件使用 Material 3 版本(androidx.compose.material3)
- 弹性动画使用 Material Motion 规范
## 内存管理
- ViewModel 中使用 viewModelScope 管理协程生命周期
- 避免在 Composable 中创建 CoroutineScope(使用 rememberCoroutineScope)
- Bitmap 处理使用 BitmapFactory.Options.inSampleSize 降采样
- Room 数据库查询使用 Flow,自动感知生命周期
## 权限(Android 特定)
- 使用 rememberLauncherForActivityResult + RequestPermission 请求权限
- Android 13+ 细分媒体权限(READ_MEDIA_IMAGES/VIDEO/AUDIO)
- Android 14+ 照片选择器优先使用 PickVisualMedia(无需权限)
- 后台定位需要 ACCESS_BACKGROUND_LOCATION(单独请求)
- 通知权限(POST_NOTIFICATIONS)在 Android 13+ 需要运行时请求
## Google Play 合规
- targetSdkVersion 必须满足 Google Play 最新要求(2025: API 35)
- 使用 App Bundle(.aab)而非 APK 发布
- 敏感权限必须在 Play Console 声明用途
- 数据安全部分必须准确填写2.3 Cursor Rules 移动端模板
React Native 项目(.cursor/rules/react-native.mdc)
---
description: React Native 移动端开发规则
globs: ["src/**/*.tsx", "src/**/*.ts", "app/**/*.tsx"]
---
# React Native 开发规则
## 组件规范
- 使用函数组件 + TypeScript,禁止 class 组件
- Props 使用 interface 定义,必须导出类型
- 组件文件结构:类型定义 → 组件 → 样式(StyleSheet.create)
- 禁止在组件内部定义子组件(提取到独立文件)
## 性能规则
- FlatList renderItem 使用 useCallback 包裹
- 避免在 render 中创建新对象/数组(使用 useMemo)
- 图片使用 expo-image,设置 contentFit 和 placeholder
- 动画使用 reanimated worklet,禁止 Animated API 的 JS 驱动动画
- 避免不必要的 re-render:使用 React.memo + 浅比较
## 导航规则
- 使用 Expo Router 的文件系统路由
- 类型安全导航:使用 useRouter<T>() 泛型
- 深层链接必须在 app.json 中配置 scheme
- 模态页面使用 presentation: 'modal'
## 错误处理
- 每个屏幕必须有 ErrorBoundary
- API 错误必须显示用户友好的错误信息
- 网络错误必须区分离线和服务器错误
- 崩溃上报使用 Sentry 或 BugsnagFlutter 项目(.cursor/rules/flutter.mdc)
---
description: Flutter 移动端开发规则
globs: ["lib/**/*.dart"]
---
# Flutter 开发规则
## Widget 规范
- 优先使用 StatelessWidget,仅在需要时使用 StatefulWidget
- 状态管理使用 Riverpod 2.x / Bloc,禁止全局变量
- Widget 拆分:单个 build 方法不超过 80 行
- 使用 const 构造函数优化重建性能
## 性能规则
- ListView.builder 替代 ListView(大列表)
- 图片使用 cached_network_image,设置 memCacheWidth/Height
- 避免在 build 中执行异步操作(使用 FutureBuilder/StreamBuilder)
- 使用 RepaintBoundary 隔离频繁重绘的区域
- 禁止在 build 中创建 TextEditingController(使用 late 或 initState)
## 平台适配
- 使用 Platform.isIOS / Platform.isAndroid 处理平台差异
- Cupertino 组件用于 iOS 风格,Material 组件用于 Android 风格
- 使用 flutter_adaptive_scaffold 处理多屏幕尺寸
- 状态栏样式使用 SystemChrome.setSystemUIOverlayStyle
## 资源管理
- 图片放在 assets/ 目录,使用 1x/2x/3x 多分辨率
- 字体仅包含使用的字重
- 使用 flutter_gen 生成类型安全的资源引用
- 国际化使用 flutter_localizations + arb 文件2.4 跨平台统一 Steering 规则模板
以下是一个可同时用于 CLAUDE.md 和 Kiro Steering 的跨平台移动端规则核心模板:
# 移动端跨平台核心规则
## 🚫 绝对禁止(违反即拒绝 PR)
1. 禁止在生产代码中使用 console.log / print / NSLog 输出敏感信息
2. 禁止明文存储用户凭证(密码、token、API key)
3. 禁止在主线程执行耗时操作(网络请求、文件 I/O、复杂计算)
4. 禁止忽略权限请求结果(必须处理 granted/denied/blocked)
5. 禁止无限制的后台任务(必须设置超时和取消机制)
6. 禁止引入未经审查的第三方库(检查维护状态、安全漏洞、包体积)
7. 禁止硬编码 IP 地址、域名或环境配置
8. 禁止在 App Store / Play Store 审核中使用测试数据
## ⚠️ 必须遵守(代码审查重点)
1. 所有副作用必须有对应的清理逻辑
2. 所有网络请求必须有超时、重试和错误处理
3. 所有用户输入必须验证和清理
4. 所有列表必须使用虚拟化渲染(FlatList/LazyColumn/ListView.builder)
5. 所有图片必须有加载状态、错误状态和缓存策略
6. 所有导航必须支持 deep linking
7. 所有文本必须支持国际化(即使当前只有一种语言)
8. 所有触摸目标最小 44x44pt / 48x48dp
## ✅ 推荐实践(提升代码质量)
1. 使用设计 token 而非硬编码颜色/字体/间距
2. 组件设计遵循单一职责原则
3. 业务逻辑与 UI 分离(MVVM / Clean Architecture)
4. 使用 TypeScript strict / Dart strict / Swift strict concurrency
5. 关键路径有单元测试覆盖
6. 使用 CI/CD 自动化构建和测试3. 反模式一:内存泄漏(Memory Leaks)
3.1 问题描述
内存泄漏是移动端最隐蔽也最致命的问题——应用不会立即崩溃,而是随着使用时间增长逐渐变慢,最终被系统强制杀死。AI 生成的代码特别容易在以下场景产生内存泄漏:
- 未清理的事件监听器和订阅:组件卸载后监听器仍在运行
- 闭包中的强引用:异步回调持有已销毁组件的引用
- 未释放的定时器:setInterval/setTimeout 未在组件卸载时清除
- 图片缓存失控:大量高分辨率图片未设置缓存上限
- 导航栈累积:不断 push 新页面而不 pop,导致页面栈无限增长
3.2 AI 生成的问题代码 vs 正确代码
React Native:未清理的订阅
// ❌ AI 常见错误:未清理 useEffect 中的订阅
import { useEffect, useState } from 'react';
import { AppState, Dimensions } from 'react-native';
import NetInfo from '@react-native-community/netinfo';
function DashboardScreen() {
const [isOnline, setIsOnline] = useState(true);
const [orientation, setOrientation] = useState('portrait');
useEffect(() => {
// 问题 1:NetInfo 订阅未取消
NetInfo.addEventListener(state => {
setIsOnline(state.isConnected ?? false);
});
// 问题 2:AppState 监听未移除
AppState.addEventListener('change', (nextState) => {
if (nextState === 'active') {
// 刷新数据...
}
});
// 问题 3:Dimensions 监听未移除
Dimensions.addEventListener('change', ({ window }) => {
setOrientation(window.width > window.height ? 'landscape' : 'portrait');
});
// 问题 4:定时器未清除
setInterval(() => {
// 轮询数据...
}, 30000);
// ❌ 没有返回清理函数!
}, []);
return (/* ... */);
}// ✅ 正确写法:所有副作用都有对应的清理
import { useEffect, useState, useCallback } from 'react';
import { AppState, useWindowDimensions } from 'react-native';
import NetInfo from '@react-native-community/netinfo';
function DashboardScreen() {
const [isOnline, setIsOnline] = useState(true);
const { width, height } = useWindowDimensions(); // 自动管理生命周期
const orientation = width > height ? 'landscape' : 'portrait';
// 网络状态监听(自动清理)
useEffect(() => {
const unsubscribe = NetInfo.addEventListener(state => {
setIsOnline(state.isConnected ?? false);
});
return () => unsubscribe(); // ✅ 清理订阅
}, []);
// AppState 监听(自动清理)
useEffect(() => {
const subscription = AppState.addEventListener('change', (nextState) => {
if (nextState === 'active') {
// 刷新数据...
}
});
return () => subscription.remove(); // ✅ 移除监听
}, []);
// 轮询(自动清理)
useEffect(() => {
const intervalId = setInterval(() => {
// 轮询数据...
}, 30000);
return () => clearInterval(intervalId); // ✅ 清除定时器
}, []);
// ✅ 使用 useWindowDimensions 替代手动监听 Dimensions
// 它会自动订阅和取消订阅
return (/* ... */);
}SwiftUI:闭包中的循环引用
// ❌ AI 常见错误:闭包中的强引用导致循环引用
class LocationManager: ObservableObject {
@Published var currentLocation: CLLocation?
private var locationManager = CLLocationManager()
private var timer: Timer?
init() {
// 问题 1:Timer 持有 self 的强引用
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in
self.refreshLocation() // ❌ 强引用 self
}
// 问题 2:闭包持有 self 的强引用
NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { _ in
self.refreshLocation() // ❌ 强引用 self
}
}
func refreshLocation() {
// 刷新位置...
}
// ❌ 没有 deinit 清理!
}// ✅ 正确写法:使用 [weak self] 和 deinit 清理
class LocationManager: ObservableObject {
@Published var currentLocation: CLLocation?
private var locationManager = CLLocationManager()
private var timer: Timer?
private var cancellables = Set<AnyCancellable>()
init() {
// ✅ Timer 使用 [weak self]
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
self?.refreshLocation()
}
// ✅ 使用 Combine 的 NotificationCenter publisher(自动管理生命周期)
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { [weak self] _ in
self?.refreshLocation()
}
.store(in: &cancellables) // ✅ 存储在 cancellables 中
}
func refreshLocation() {
// 刷新位置...
}
deinit {
timer?.invalidate() // ✅ 清理定时器
cancellables.removeAll() // ✅ 取消所有订阅
}
}Flutter:未 dispose 的 Controller
// ❌ AI 常见错误:Controller 未 dispose
class ChatScreen extends StatefulWidget {
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final _scrollController = ScrollController();
final _textController = TextEditingController();
final _focusNode = FocusNode();
late StreamSubscription _messageSubscription;
@override
void initState() {
super.initState();
// 问题:订阅了 Stream 但没有在 dispose 中取消
_messageSubscription = chatService.messageStream.listen((message) {
setState(() {
// 添加消息...
});
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
// ❌ 没有 dispose 方法!所有 Controller 和订阅都会泄漏
// ScrollController、TextEditingController、FocusNode、StreamSubscription
// 全部没有被释放
@override
Widget build(BuildContext context) {
return Scaffold(/* ... */);
}
}// ✅ 正确写法:所有资源在 dispose 中释放
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final _scrollController = ScrollController();
final _textController = TextEditingController();
final _focusNode = FocusNode();
late final StreamSubscription _messageSubscription;
@override
void initState() {
super.initState();
_messageSubscription = chatService.messageStream.listen((message) {
if (!mounted) return; // ✅ 检查 widget 是否仍然挂载
setState(() {
// 添加消息...
});
// ✅ 延迟滚动,确保列表已更新
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
});
}
@override
void dispose() {
_messageSubscription.cancel(); // ✅ 取消 Stream 订阅
_scrollController.dispose(); // ✅ 释放 ScrollController
_textController.dispose(); // ✅ 释放 TextEditingController
_focusNode.dispose(); // ✅ 释放 FocusNode
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(/* ... */);
}
}Jetpack Compose:协程泄漏
// ❌ AI 常见错误:在 Composable 中启动不受管理的协程
@Composable
fun LiveDataScreen(viewModel: LiveDataViewModel) {
val scope = CoroutineScope(Dispatchers.IO) // ❌ 每次重组创建新 scope
LaunchedEffect(Unit) {
// 问题 1:使用 GlobalScope,永远不会被取消
GlobalScope.launch {
while (true) {
viewModel.fetchLatestData()
delay(30_000)
}
}
}
// 问题 2:在 onClick 中使用未管理的 scope
Button(onClick = {
scope.launch { // ❌ 这个 scope 不会随 Composable 销毁而取消
viewModel.uploadData()
}
}) {
Text("上传")
}
}// ✅ 正确写法:使用生命周期感知的协程 scope
@Composable
fun LiveDataScreen(viewModel: LiveDataViewModel) {
val scope = rememberCoroutineScope() // ✅ 随 Composable 生命周期管理
// ✅ LaunchedEffect 自动在 Composable 离开组合时取消
LaunchedEffect(Unit) {
while (isActive) { // ✅ 检查协程是否仍然活跃
viewModel.fetchLatestData()
delay(30_000)
}
}
Button(onClick = {
scope.launch { // ✅ 使用 rememberCoroutineScope
viewModel.uploadData()
}
}) {
Text("上传")
}
}
// ✅ ViewModel 中使用 viewModelScope
class LiveDataViewModel : ViewModel() {
private var pollingJob: Job? = null
fun startPolling() {
pollingJob?.cancel() // ✅ 取消之前的轮询
pollingJob = viewModelScope.launch { // ✅ 随 ViewModel 销毁自动取消
while (isActive) {
fetchLatestData()
delay(30_000)
}
}
}
suspend fun fetchLatestData() { /* ... */ }
suspend fun uploadData() { /* ... */ }
override fun onCleared() {
super.onCleared()
pollingJob?.cancel() // ✅ 显式取消(虽然 viewModelScope 会自动取消)
}
}3.3 内存泄漏检测工具
| 工具 | 平台 | 价格 | 用途 |
|---|---|---|---|
| Xcode Instruments (Leaks) | iOS | 免费 | 运行时内存泄漏检测 |
| Xcode Memory Graph Debugger | iOS | 免费 | 可视化对象引用关系 |
| Android Studio Profiler | Android | 免费 | 内存分配追踪和堆转储 |
| LeakCanary | Android | 免费(开源) | 自动检测 Activity/Fragment 泄漏 |
| Flipper | React Native | 免费(开源) | 内存、网络、布局调试 |
| Flutter DevTools | Flutter | 免费 | 内存快照对比和泄漏检测 |
| Finotes | iOS/Android | $99/月起 | 生产环境内存泄漏监控 |
| Sentry Performance | 全平台 | 免费额度 / $26/月起 | 生产环境性能和内存监控 |
3.4 提示词模板:内存泄漏检测与修复
请审查以下 [React Native / SwiftUI / Flutter / Compose] 代码的内存管理:
[粘贴代码]
检查清单:
1. 所有 useEffect / LaunchedEffect / initState 是否有对应的清理逻辑?
2. 闭包中是否存在对 self/this/组件的强引用?
3. 定时器(Timer/setInterval)是否在组件销毁时清除?
4. Stream/Flow/Combine 订阅是否在组件销毁时取消?
5. 大对象(图片、数据集)是否有缓存上限和释放策略?
6. 导航栈是否可能无限增长?
7. 事件监听器是否在不需要时移除?
对于发现的每个问题,请:
- 标注问题位置和严重程度(高/中/低)
- 解释为什么会导致内存泄漏
- 提供修复后的代码4. 反模式二:电池消耗(Battery Drain)
4.1 问题描述
电池消耗是用户卸载应用的首要原因之一。AI 生成的代码往往在功能上正确,但在能耗上极度浪费——频繁的网络请求、持续的 GPS 定位、不必要的后台任务、JS 线程驱动的动画等都会快速耗尽电池。研究表明,批量合并网络请求可减少约 20% 的空闲耗电,而将定位从持续模式改为按需模式可节省高达 40% 的定位相关能耗。
4.2 AI 生成的问题代码 vs 正确代码
React Native:频繁轮询和持续定位
// ❌ AI 常见错误:频繁轮询 + 持续高精度定位
import * as Location from 'expo-location';
function DeliveryTracker() {
const [location, setLocation] = useState(null);
const [orders, setOrders] = useState([]);
useEffect(() => {
// 问题 1:每 5 秒轮询一次订单状态(极度耗电)
const pollInterval = setInterval(async () => {
const response = await fetch('https://api.example.com/orders');
const data = await response.json();
setOrders(data);
}, 5000); // ❌ 5 秒太频繁
// 问题 2:持续高精度 GPS 定位
Location.watchPositionAsync(
{
accuracy: Location.Accuracy.BestForNavigation, // ❌ 最高精度,极度耗电
distanceInterval: 0, // ❌ 任何移动都触发更新
timeInterval: 1000, // ❌ 每秒更新
},
(newLocation) => {
setLocation(newLocation);
// 问题 3:每次位置更新都发送到服务器
fetch('https://api.example.com/location', {
method: 'POST',
body: JSON.stringify(newLocation.coords),
});
}
);
// ❌ 没有清理!
}, []);
return (/* ... */);
}// ✅ 正确写法:智能轮询 + 自适应定位精度
import * as Location from 'expo-location';
import NetInfo from '@react-native-community/netinfo';
import { AppState } from 'react-native';
function DeliveryTracker() {
const [location, setLocation] = useState(null);
const [orders, setOrders] = useState([]);
const appState = useRef(AppState.currentState);
const locationBuffer = useRef<Location.LocationObject[]>([]);
useEffect(() => {
let pollInterval: NodeJS.Timeout;
let locationSubscription: Location.LocationSubscription;
let appStateSubscription: any;
const startTracking = async () => {
// ✅ 根据应用状态调整轮询频率
const getPollInterval = () => {
return appState.current === 'active' ? 30000 : 120000; // 前台 30s,后台 2min
};
const pollOrders = async () => {
const netState = await NetInfo.fetch();
if (!netState.isConnected) return; // ✅ 离线时跳过
try {
const response = await fetch('https://api.example.com/orders');
const data = await response.json();
setOrders(data);
} catch (error) {
// 静默失败,下次轮询重试
}
// ✅ 动态调整下次轮询间隔
pollInterval = setTimeout(pollOrders, getPollInterval());
};
pollInterval = setTimeout(pollOrders, getPollInterval());
// ✅ 自适应定位精度
locationSubscription = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.Balanced, // ✅ 平衡精度(~100m)
distanceInterval: 50, // ✅ 移动 50 米才更新
timeInterval: 10000, // ✅ 最多每 10 秒更新一次
},
(newLocation) => {
setLocation(newLocation);
// ✅ 缓冲位置数据,批量上传
locationBuffer.current.push(newLocation);
if (locationBuffer.current.length >= 5) {
uploadLocations(locationBuffer.current);
locationBuffer.current = [];
}
}
);
// ✅ 监听应用状态变化
appStateSubscription = AppState.addEventListener('change', (nextState) => {
if (nextState === 'background') {
// 进入后台:降低定位频率,暂停非必要轮询
uploadLocations(locationBuffer.current); // 上传缓冲数据
locationBuffer.current = [];
}
appState.current = nextState;
});
};
startTracking();
return () => {
clearTimeout(pollInterval); // ✅ 清理轮询
locationSubscription?.remove(); // ✅ 停止定位
appStateSubscription?.remove(); // ✅ 移除监听
};
}, []);
return (/* ... */);
}
// ✅ 批量上传位置数据
async function uploadLocations(locations: Location.LocationObject[]) {
if (locations.length === 0) return;
try {
await fetch('https://api.example.com/locations/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(locations.map(l => l.coords)),
});
} catch {
// 失败时保留数据,下次重试
}
}SwiftUI:不必要的后台刷新
// ❌ AI 常见错误:过度使用后台刷新
class WeatherViewModel: ObservableObject {
@Published var weather: Weather?
private var timer: Timer?
init() {
// 问题:即使应用在后台也每分钟刷新天气
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
Task {
await self?.fetchWeather() // ❌ 后台也在请求
}
}
// 问题:启动时立即请求,不检查缓存
Task { await fetchWeather() }
}
func fetchWeather() async {
// 每次都发网络请求,不使用缓存
let url = URL(string: "https://api.weather.com/current")!
let (data, _) = try! await URLSession.shared.data(from: url) // ❌ 强制解包
weather = try! JSONDecoder().decode(Weather.self, from: data)
}
}// ✅ 正确写法:智能缓存 + 生命周期感知刷新
class WeatherViewModel: ObservableObject {
@Published var weather: Weather?
@Published var lastUpdated: Date?
private var refreshTask: Task<Void, Never>?
private let cache = WeatherCache()
private let minRefreshInterval: TimeInterval = 600 // 10 分钟
func onAppear() {
// ✅ 先加载缓存
if let cached = cache.load() {
weather = cached.weather
lastUpdated = cached.timestamp
}
// ✅ 仅在缓存过期时刷新
if shouldRefresh() {
startAutoRefresh()
}
}
func onDisappear() {
// ✅ 离开页面时停止自动刷新
refreshTask?.cancel()
refreshTask = nil
}
private func shouldRefresh() -> Bool {
guard let lastUpdated else { return true }
return Date().timeIntervalSince(lastUpdated) > minRefreshInterval
}
private func startAutoRefresh() {
refreshTask?.cancel()
refreshTask = Task { [weak self] in
while !Task.isCancelled {
await self?.fetchWeather()
try? await Task.sleep(for: .seconds(600)) // ✅ 10 分钟刷新一次
}
}
}
func fetchWeather() async {
do {
let url = URL(string: "https://api.weather.com/current")!
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode(Weather.self, from: data)
await MainActor.run {
weather = decoded
lastUpdated = Date()
}
// ✅ 更新缓存
cache.save(weather: decoded, timestamp: Date())
} catch {
// ✅ 网络错误时使用缓存数据,不崩溃
print("Weather fetch failed, using cached data: \(error)")
}
}
}4.3 电池优化检查清单
| 检查项 | 严重程度 | 检测方法 |
|---|---|---|
| GPS 定位精度是否过高 | 🔴 高 | 检查 accuracy 参数 |
| 网络轮询间隔是否过短(<30s) | 🔴 高 | 搜索 setInterval/Timer |
| 后台任务是否有超时限制 | 🔴 高 | 检查 BackgroundFetch 配置 |
| 动画是否在原生线程运行 | 🟡 中 | 检查 useNativeDriver/worklet |
| 传感器监听是否及时停止 | 🟡 中 | 检查加速度计/陀螺仪订阅 |
| 网络请求是否批量合并 | 🟡 中 | 检查请求频率和合并逻辑 |
| 图片是否按需加载 | 🟢 低 | 检查懒加载和缓存策略 |
| 日志输出是否在生产环境关闭 | 🟢 低 | 检查 console.log/print |
4.4 提示词模板:电池优化审查
请审查以下移动端代码的电池消耗情况:
[粘贴代码]
重点检查:
1. 网络请求频率是否合理?是否可以批量合并?
2. GPS 定位精度是否过高?是否可以降低?
3. 后台任务是否有超时和取消机制?
4. 动画是否运行在原生线程?
5. 传感器(加速度计、陀螺仪)是否在不需要时停止?
6. 应用进入后台时是否暂停非必要操作?
对于每个问题,请提供:
- 预估的电池影响(高/中/低)
- 优化后的代码
- 预期的电池节省百分比5. 反模式三:缺少权限处理(Missing Permission Handling)
5.1 问题描述
权限处理是 AI 生成移动端代码中最常被忽略的部分。AI 倾向于假设权限已经被授予,直接调用需要权限的 API,导致应用在权限未授予时崩溃或功能失效。研究表明,超过 82% 的用户希望应用在请求权限前提供清晰的理由说明,而提供合理解释可使权限授予率提高 81%。
常见问题包括:
- 不检查权限状态就直接使用:相机、定位、相册等 API 在无权限时崩溃
- 不处理权限拒绝:用户拒绝后应用白屏或功能完全不可用
- 不处理”永久拒绝”:用户选择”不再询问”后无法引导到设置页
- 启动时请求所有权限:一次性弹出多个权限请求,用户体验极差
- 不区分 iOS 和 Android 权限模型:两个平台的权限粒度和流程不同
5.2 AI 生成的问题代码 vs 正确代码
React Native:不检查权限直接使用
// ❌ AI 常见错误:不检查权限直接使用相机
import { Camera } from 'expo-camera';
function ScannerScreen() {
// 问题 1:没有检查相机权限
// 问题 2:没有处理权限拒绝
// 问题 3:没有处理权限永久拒绝
return (
<Camera
style={{ flex: 1 }}
onBarCodeScanned={({ data }) => {
console.log('Scanned:', data);
}}
/>
// ❌ 如果用户没有授予相机权限,这里会崩溃或显示黑屏
);
}// ✅ 正确写法:完整的权限处理流程
import { useState, useEffect } from 'react';
import { Camera, CameraView } from 'expo-camera';
import { Alert, Linking, Platform, View, Text, TouchableOpacity, StyleSheet } from 'react-native';
type PermissionState = 'loading' | 'undetermined' | 'granted' | 'denied' | 'blocked';
function ScannerScreen() {
const [permissionState, setPermissionState] = useState<PermissionState>('loading');
useEffect(() => {
checkPermission();
}, []);
const checkPermission = async () => {
const { status } = await Camera.getCameraPermissionsAsync();
if (status === 'granted') {
setPermissionState('granted');
} else if (status === 'undetermined') {
setPermissionState('undetermined');
} else {
// iOS: 'denied' 意味着可以再次请求或已永久拒绝
// Android: 需要检查 canAskAgain
setPermissionState('denied');
}
};
const requestPermission = async () => {
// ✅ 先显示自定义说明(在系统弹窗之前)
Alert.alert(
'需要相机权限',
'扫描二维码需要使用您的相机。我们不会录制或存储任何影像。',
[
{ text: '暂不开启', style: 'cancel' },
{
text: '开启相机',
onPress: async () => {
const { status, canAskAgain } = await Camera.requestCameraPermissionsAsync();
if (status === 'granted') {
setPermissionState('granted');
} else if (!canAskAgain) {
setPermissionState('blocked'); // ✅ 永久拒绝
} else {
setPermissionState('denied');
}
},
},
]
);
};
const openSettings = () => {
// ✅ 引导用户到系统设置页
if (Platform.OS === 'ios') {
Linking.openURL('app-settings:');
} else {
Linking.openSettings();
}
};
// ✅ 根据权限状态渲染不同 UI
switch (permissionState) {
case 'loading':
return <LoadingView />;
case 'undetermined':
case 'denied':
return (
<View style={styles.permissionContainer}>
<Text style={styles.icon}>📷</Text>
<Text style={styles.title}>需要相机权限</Text>
<Text style={styles.description}>
扫描二维码需要使用相机。{'\n'}
我们不会录制或存储任何影像。
</Text>
<TouchableOpacity style={styles.button} onPress={requestPermission}>
<Text style={styles.buttonText}>开启相机权限</Text>
</TouchableOpacity>
{/* ✅ 提供手动输入的降级方案 */}
<TouchableOpacity style={styles.linkButton} onPress={() => {/* 导航到手动输入页 */}}>
<Text style={styles.linkText}>手动输入编码</Text>
</TouchableOpacity>
</View>
);
case 'blocked':
return (
<View style={styles.permissionContainer}>
<Text style={styles.icon}>⚙️</Text>
<Text style={styles.title}>相机权限已关闭</Text>
<Text style={styles.description}>
请在系统设置中开启相机权限,{'\n'}
才能使用扫码功能。
</Text>
<TouchableOpacity style={styles.button} onPress={openSettings}>
<Text style={styles.buttonText}>前往设置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.linkButton} onPress={() => {/* 导航到手动输入页 */}}>
<Text style={styles.linkText}>手动输入编码</Text>
</TouchableOpacity>
</View>
);
case 'granted':
return (
<CameraView
style={{ flex: 1 }}
barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
onBarcodeScanned={({ data }) => {
// 处理扫描结果...
}}
/>
);
}
}
const styles = StyleSheet.create({
permissionContainer: {
flex: 1, justifyContent: 'center', alignItems: 'center', padding: 32,
},
icon: { fontSize: 64, marginBottom: 16 },
title: { fontSize: 20, fontWeight: '700', marginBottom: 8, textAlign: 'center' },
description: { fontSize: 15, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
button: {
backgroundColor: '#007AFF', paddingHorizontal: 32, paddingVertical: 14,
borderRadius: 12, marginBottom: 16,
},
buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
linkButton: { padding: 8 },
linkText: { color: '#007AFF', fontSize: 15 },
});Android Compose:权限请求不完整
// ❌ AI 常见错误:权限处理不完整
@Composable
fun CameraScreen() {
// 问题:只处理了 granted,没有处理 denied 和 permanently denied
val permissionState = rememberPermissionState(Manifest.permission.CAMERA)
if (permissionState.status.isGranted) {
CameraPreview()
} else {
// ❌ 直接请求,没有解释为什么需要
Button(onClick = { permissionState.launchPermissionRequest() }) {
Text("授予相机权限")
}
// ❌ 如果用户选择"不再询问",这个按钮将永远无效
}
}// ✅ 正确写法:完整的 Android 权限处理
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraScreen(
onManualInput: () -> Unit // ✅ 降级方案回调
) {
val permissionState = rememberPermissionState(Manifest.permission.CAMERA)
var showRationale by remember { mutableStateOf(false) }
// ✅ 权限说明对话框
if (showRationale) {
AlertDialog(
onDismissRequest = { showRationale = false },
title = { Text("需要相机权限") },
text = { Text("扫描二维码需要使用相机。我们不会录制或存储任何影像。") },
confirmButton = {
TextButton(onClick = {
showRationale = false
permissionState.launchPermissionRequest()
}) { Text("开启") }
},
dismissButton = {
TextButton(onClick = { showRationale = false }) { Text("暂不") }
}
)
}
when {
// ✅ 已授权:显示相机
permissionState.status.isGranted -> {
CameraPreview()
}
// ✅ 需要解释(用户之前拒绝过,但没有选择"不再询问")
permissionState.status.shouldShowRationale -> {
PermissionRequestUI(
icon = Icons.Default.CameraAlt,
title = "需要相机权限",
description = "扫描二维码需要使用相机。\n我们不会录制或存储任何影像。",
primaryAction = "开启相机权限",
onPrimaryClick = { showRationale = true },
secondaryAction = "手动输入编码",
onSecondaryClick = onManualInput
)
}
// ✅ 永久拒绝:引导到设置页
else -> {
val context = LocalContext.current
PermissionRequestUI(
icon = Icons.Default.Settings,
title = "相机权限已关闭",
description = "请在系统设置中开启相机权限。",
primaryAction = "前往设置",
onPrimaryClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
},
secondaryAction = "手动输入编码",
onSecondaryClick = onManualInput
)
}
}
}
// ✅ 可复用的权限请求 UI 组件
@Composable
fun PermissionRequestUI(
icon: ImageVector,
title: String,
description: String,
primaryAction: String,
onPrimaryClick: () -> Unit,
secondaryAction: String,
onSecondaryClick: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize().padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(16.dp))
Text(title, style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center)
Spacer(Modifier.height(8.dp))
Text(description, style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(Modifier.height(24.dp))
Button(onClick = onPrimaryClick, modifier = Modifier.fillMaxWidth()) {
Text(primaryAction)
}
Spacer(Modifier.height(8.dp))
TextButton(onClick = onSecondaryClick) {
Text(secondaryAction)
}
}
}5.3 权限处理最佳实践矩阵
| 权限类型 | iOS 特殊处理 | Android 特殊处理 | 降级方案 |
|---|---|---|---|
| 相机 | Info.plist: NSCameraUsageDescription | CAMERA(运行时) | 手动输入/从相册选择 |
| 照片库 | PHPhotoLibrary 分级权限(limited/full) | Android 14+: PickVisualMedia(无需权限) | 文件选择器 |
| 定位 | whenInUse vs always 分步请求 | ACCESS_FINE vs COARSE + BACKGROUND 分步 | 手动输入地址 |
| 通知 | UNUserNotificationCenter | Android 13+: POST_NOTIFICATIONS | 应用内消息中心 |
| 麦克风 | NSMicrophoneUsageDescription | RECORD_AUDIO | 文字输入替代 |
| 联系人 | NSContactsUsageDescription | READ_CONTACTS | 手动输入联系人 |
| 蓝牙 | NSBluetoothAlwaysUsageDescription | BLUETOOTH_CONNECT (Android 12+) | USB 连接替代 |
5.4 提示词模板:权限处理生成
为 [应用功能] 生成完整的权限处理代码:
框架:[React Native / SwiftUI / Jetpack Compose / Flutter]
需要的权限:[相机 / 定位 / 照片 / 通知 / 麦克风 / ...]
必须包含:
1. 权限状态检查(granted / denied / undetermined / blocked)
2. 自定义权限说明弹窗(在系统弹窗之前显示,解释为什么需要)
3. 权限拒绝后的降级方案(应用不能崩溃或白屏)
4. 永久拒绝后引导到系统设置页
5. iOS 和 Android 的差异处理
6. 可复用的权限请求 UI 组件
权限请求时机:
- 不要在应用启动时请求
- 在用户即将使用相关功能时请求(just-in-time)
- 先显示自定义说明,再触发系统权限弹窗6. 反模式四:离线支持差(Poor Offline Support)
6.1 问题描述
移动端用户经常处于弱网或离线环境——地铁、电梯、偏远地区、飞行模式。AI 生成的代码几乎总是假设网络始终可用,导致离线时应用完全不可用、数据丢失、或显示无意义的错误信息。实践表明,采用本地优先(local-first)的读取路径后,中端 Android 设备上的屏幕加载时间可从约 1.7 秒降至约 300 毫秒。
6.2 AI 生成的问题代码 vs 正确代码
React Native:无离线策略的数据加载
// ❌ AI 常见错误:完全依赖网络,无离线策略
function ProductListScreen() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchProducts();
}, []);
const fetchProducts = async () => {
setLoading(true);
try {
const response = await fetch('https://api.example.com/products');
const data = await response.json();
setProducts(data);
} catch (err) {
setError('加载失败'); // ❌ 离线时只显示"加载失败",无任何数据
} finally {
setLoading(false);
}
};
if (loading) return <ActivityIndicator />;
if (error) return <Text>{error}</Text>; // ❌ 离线时用户看到的就是这个
// ❌ 没有缓存,每次进入页面都要重新加载
// ❌ 没有离线状态指示
// ❌ 没有重试机制
return <FlatList data={products} renderItem={/* ... */} />;
}// ✅ 正确写法:离线优先 + 智能缓存 + 自动重试
import { useQuery, useQueryClient, QueryClient } from '@tanstack/react-query';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo, { useNetInfo } from '@react-native-community/netinfo';
// ✅ 配置持久化缓存
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 缓存 24 小时
staleTime: 1000 * 60 * 5, // 5 分钟内认为数据新鲜
retry: 3, // 自动重试 3 次
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // 指数退避
},
},
});
const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'REACT_QUERY_CACHE',
});
// ✅ 离线感知的产品列表
function ProductListScreen() {
const netInfo = useNetInfo();
const isOffline = !netInfo.isConnected;
const {
data: products,
isLoading,
isError,
error,
refetch,
dataUpdatedAt,
} = useQuery({
queryKey: ['products'],
queryFn: async () => {
const response = await fetch('https://api.example.com/products');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
// ✅ 离线时使用缓存数据,不发起请求
enabled: !isOffline || !queryClient.getQueryData(['products']),
});
return (
<View style={{ flex: 1 }}>
{/* ✅ 离线状态指示器 */}
{isOffline && (
<View style={styles.offlineBanner}>
<Text style={styles.offlineText}>📡 当前处于离线模式,显示的是缓存数据</Text>
</View>
)}
{/* ✅ 数据新鲜度提示 */}
{dataUpdatedAt && (
<Text style={styles.freshness}>
最后更新:{new Date(dataUpdatedAt).toLocaleString()}
</Text>
)}
{isLoading && !products ? (
<ActivityIndicator />
) : isError && !products ? (
// ✅ 错误状态:区分离线和服务器错误
<View style={styles.errorContainer}>
<Text style={styles.errorIcon}>
{isOffline ? '📡' : '⚠️'}
</Text>
<Text style={styles.errorTitle}>
{isOffline ? '无网络连接' : '加载失败'}
</Text>
<Text style={styles.errorMessage}>
{isOffline
? '请检查网络连接后重试'
: '服务器暂时不可用,请稍后重试'}
</Text>
<TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
<Text style={styles.retryText}>重试</Text>
</TouchableOpacity>
</View>
) : (
<FlatList
data={products}
renderItem={({ item }) => <ProductCard product={item} />}
// ✅ 下拉刷新(仅在线时可用)
refreshing={isLoading}
onRefresh={isOffline ? undefined : refetch}
/>
)}
</View>
);
}Flutter:离线表单数据丢失
// ❌ AI 常见错误:表单提交失败时数据丢失
class FeedbackForm extends StatefulWidget {
@override
State<FeedbackForm> createState() => _FeedbackFormState();
}
class _FeedbackFormState extends State<FeedbackForm> {
final _controller = TextEditingController();
Future<void> _submit() async {
try {
final response = await http.post(
Uri.parse('https://api.example.com/feedback'),
body: jsonEncode({'content': _controller.text}),
);
if (response.statusCode == 200) {
Navigator.pop(context);
}
} catch (e) {
// ❌ 网络错误时只显示 SnackBar,用户的输入可能丢失
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('提交失败,请重试')),
);
// ❌ 如果用户此时退出页面,数据就丢失了
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
TextField(controller: _controller),
ElevatedButton(onPressed: _submit, child: Text('提交')),
]),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}// ✅ 正确写法:离线队列 + 自动重试 + 数据持久化
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
class FeedbackForm extends StatefulWidget {
const FeedbackForm({super.key});
@override
State<FeedbackForm> createState() => _FeedbackFormState();
}
class _FeedbackFormState extends State<FeedbackForm> with WidgetsBindingObserver {
final _controller = TextEditingController();
bool _isSubmitting = false;
bool _isOffline = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_checkConnectivity();
_restoreDraft(); // ✅ 恢复草稿
}
Future<void> _checkConnectivity() async {
final result = await Connectivity().checkConnectivity();
setState(() => _isOffline = result.contains(ConnectivityResult.none));
// ✅ 监听网络变化
Connectivity().onConnectivityChanged.listen((results) {
final wasOffline = _isOffline;
setState(() => _isOffline = results.contains(ConnectivityResult.none));
// ✅ 网络恢复时自动重试队列中的请求
if (wasOffline && !_isOffline) {
_retryPendingSubmissions();
}
});
}
// ✅ 自动保存草稿
Future<void> _saveDraft() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('feedback_draft', _controller.text);
}
// ✅ 恢复草稿
Future<void> _restoreDraft() async {
final prefs = await SharedPreferences.getInstance();
final draft = prefs.getString('feedback_draft');
if (draft != null && draft.isNotEmpty) {
_controller.text = draft;
}
}
Future<void> _submit() async {
if (_controller.text.trim().isEmpty) return;
setState(() => _isSubmitting = true);
if (_isOffline) {
// ✅ 离线时保存到本地队列
await _saveToOfflineQueue(_controller.text);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已保存,将在网络恢复后自动提交'),
backgroundColor: Colors.orange,
),
);
_clearDraft();
Navigator.pop(context);
}
} else {
try {
final response = await http.post(
Uri.parse('https://api.example.com/feedback'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'content': _controller.text}),
).timeout(const Duration(seconds: 15)); // ✅ 设置超时
if (response.statusCode == 200) {
_clearDraft();
if (mounted) Navigator.pop(context);
} else {
throw Exception('Server error: ${response.statusCode}');
}
} catch (e) {
// ✅ 提交失败时保存到离线队列
await _saveToOfflineQueue(_controller.text);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('提交失败,已保存到本地,稍后自动重试'),
backgroundColor: Colors.orange,
),
);
}
}
}
setState(() => _isSubmitting = false);
}
Future<void> _saveToOfflineQueue(String content) async {
final prefs = await SharedPreferences.getInstance();
final queue = prefs.getStringList('offline_queue') ?? [];
queue.add(jsonEncode({
'content': content,
'timestamp': DateTime.now().toIso8601String(),
}));
await prefs.setStringList('offline_queue', queue);
}
Future<void> _retryPendingSubmissions() async {
final prefs = await SharedPreferences.getInstance();
final queue = prefs.getStringList('offline_queue') ?? [];
if (queue.isEmpty) return;
final failedItems = <String>[];
for (final item in queue) {
try {
final data = jsonDecode(item);
await http.post(
Uri.parse('https://api.example.com/feedback'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(data),
).timeout(const Duration(seconds: 15));
} catch (e) {
failedItems.add(item); // 重试失败的保留在队列中
}
}
await prefs.setStringList('offline_queue', failedItems);
}
Future<void> _clearDraft() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('feedback_draft');
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_saveDraft(); // ✅ 应用进入后台时保存草稿
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_saveDraft(); // ✅ 页面销毁时保存草稿
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('反馈'),
actions: [
if (_isOffline)
const Padding(
padding: EdgeInsets.only(right: 16),
child: Icon(Icons.cloud_off, color: Colors.orange), // ✅ 离线指示
),
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(children: [
if (_isOffline)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Row(children: [
Icon(Icons.info_outline, color: Colors.orange, size: 20),
SizedBox(width: 8),
Expanded(child: Text('当前离线,提交后将在网络恢复时自动发送',
style: TextStyle(color: Colors.orange, fontSize: 13))),
]),
),
Expanded(
child: TextField(
controller: _controller,
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
decoration: const InputDecoration(
hintText: '请输入您的反馈...',
border: OutlineInputBorder(),
),
onChanged: (_) => _saveDraft(), // ✅ 实时保存草稿
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isSubmitting ? null : _submit,
child: _isSubmitting
? const SizedBox(width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2))
: Text(_isOffline ? '保存并稍后发送' : '提交'),
),
),
]),
),
);
}
}6.3 离线支持架构模式
┌─────────────────────────────────────────────────────────┐
│ 离线优先架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ UI 层 │
│ ┌──────────────────────────────────────────────┐ │
│ │ 离线状态指示器 │ 数据新鲜度标签 │ 重试按钮 │ │
│ └──────────────────────────────────────────────┘ │
│ ↕ │
│ Repository 层(数据源协调) │
│ ┌──────────────────────────────────────────────┐ │
│ │ 1. 先读本地缓存(即时响应) │ │
│ │ 2. 后台请求远程数据 │ │
│ │ 3. 远程数据到达后更新本地缓存和 UI │ │
│ │ 4. 写操作:先写本地 → 加入同步队列 │ │
│ └──────────────────────────────────────────────┘ │
│ ↕ ↕ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ 本地存储 │ │ 同步队列 │ │
│ │ SQLite/MMKV │ │ 待上传操作列表 │ │
│ │ Hive/Drift │ │ 冲突解决策略 │ │
│ └──────────────┘ │ 指数退避重试 │ │
│ └──────────────────────┘ │
│ ↕ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 网络层 │ │
│ │ 连接状态监听 → 在线时自动同步队列 │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘6.4 离线支持工具推荐
| 工具 | 平台 | 价格 | 用途 |
|---|---|---|---|
| TanStack Query + AsyncStorage | React Native | 免费(开源) | 查询缓存持久化 |
| WatermelonDB | React Native | 免费(开源) | 高性能离线数据库(SQLite) |
| MMKV | React Native | 免费(开源) | 高性能键值存储(替代 AsyncStorage) |
| Drift | Flutter | 免费(开源) | 类型安全的 SQLite ORM |
| Hive | Flutter | 免费(开源) | 轻量级 NoSQL 本地存储 |
| PowerSync | Flutter/RN | 免费额度 / $49/月 | 实时离线同步(基于 SQLite) |
| Core Data + CloudKit | iOS | 免费 | Apple 原生离线同步方案 |
| Room + WorkManager | Android | 免费 | Android 原生离线队列方案 |
7. 反模式五:平台不一致(Platform Inconsistency)
7.1 问题描述
跨平台开发中,AI 生成的代码往往只在一个平台上测试通过,在另一个平台上表现完全不同。这不仅包括视觉差异,还包括行为差异——同一段代码在 iOS 和 Android 上可能产生截然不同的结果。
常见的平台不一致问题:
- 导航行为:iOS 支持边缘滑动返回,Android 使用系统返回键
- 状态栏:iOS 默认浅色内容,Android 需要手动配置
- 键盘行为:iOS 键盘弹出时自动推高内容,Android 需要手动处理
- 日期/时间选择器:iOS 使用滚轮,Android 使用日历/时钟
- 触觉反馈:iOS 有丰富的 Haptic Engine,Android 只有基础振动
- 字体渲染:iOS 使用 San Francisco,Android 使用 Roboto
- 阴影效果:iOS 支持 shadowColor/Offset/Radius,Android 使用 elevation
- 安全区域:iOS 有 Dynamic Island/刘海,Android 有各种异形屏
7.2 AI 生成的问题代码 vs 正确代码
React Native:忽略平台差异
// ❌ AI 常见错误:不处理平台差异
import { View, Text, StyleSheet, TextInput, TouchableOpacity } from 'react-native';
function LoginScreen() {
return (
<View style={styles.container}>
{/* 问题 1:没有处理状态栏 */}
<Text style={styles.title}>登录</Text>
{/* 问题 2:阴影在 Android 上不生效 */}
<View style={styles.card}>
<TextInput
style={styles.input}
placeholder="邮箱"
// 问题 3:键盘类型没有平台适配
keyboardType="email-address"
// 问题 4:自动大写在 Android 上行为不同
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="密码"
secureTextEntry
/>
{/* 问题 5:按钮样式在两个平台上看起来不同 */}
<TouchableOpacity style={styles.button}>
<Text style={styles.buttonText}>登录</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, backgroundColor: '#f5f5f5' },
title: { fontSize: 28, fontWeight: 'bold', marginBottom: 20 },
card: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 20,
// ❌ 这些阴影属性只在 iOS 上生效
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
// ❌ Android 需要 elevation,但效果和 iOS 阴影不同
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
marginBottom: 12,
fontSize: 16,
// ❌ 没有处理 Android TextInput 的下划线
},
button: {
backgroundColor: '#007AFF', // ❌ 这是 iOS 蓝色,Android 应该用 Material 色
borderRadius: 8,
padding: 14,
alignItems: 'center',
},
buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});// ✅ 正确写法:完整的平台适配
import {
View, Text, StyleSheet, TextInput, TouchableOpacity,
Platform, StatusBar, KeyboardAvoidingView,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as Haptics from 'expo-haptics';
function LoginScreen() {
const insets = useSafeAreaInsets();
const handleLogin = async () => {
// ✅ 平台适配的触觉反馈
if (Platform.OS === 'ios') {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}
// Android 触觉反馈通过 TouchableOpacity 的 android_ripple 实现
// ... 登录逻辑
};
return (
<>
{/* ✅ 平台适配的状态栏 */}
<StatusBar
barStyle="dark-content"
backgroundColor={Platform.OS === 'android' ? '#f5f5f5' : undefined}
translucent={Platform.OS === 'android'}
/>
{/* ✅ 键盘避让:iOS 用 padding,Android 用 height */}
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : StatusBar.currentHeight || 0}
>
<View style={[
styles.container,
{ paddingTop: insets.top + 20 } // ✅ 安全区域适配
]}>
<Text style={styles.title}>登录</Text>
<View style={styles.card}>
<TextInput
style={[
styles.input,
// ✅ Android 移除默认下划线
Platform.OS === 'android' && { underlineColorAndroid: 'transparent' },
]}
placeholder="邮箱"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
textContentType="emailAddress" // ✅ iOS 自动填充
autoComplete="email" // ✅ Android 自动填充
/>
<TextInput
style={[
styles.input,
Platform.OS === 'android' && { underlineColorAndroid: 'transparent' },
]}
placeholder="密码"
secureTextEntry
textContentType="password" // ✅ iOS 自动填充
autoComplete="password" // ✅ Android 自动填充
/>
<TouchableOpacity
style={styles.button}
onPress={handleLogin}
activeOpacity={0.8}
// ✅ Android Material 水波纹效果
{...(Platform.OS === 'android' && {
android_ripple: { color: 'rgba(255,255,255,0.3)' },
})}
>
<Text style={styles.buttonText}>登录</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, backgroundColor: '#f5f5f5' },
title: {
fontSize: 28,
// ✅ 平台适配的字重
fontWeight: Platform.select({ ios: '700', android: 'bold' }),
marginBottom: 20,
color: '#1a1a1a',
},
card: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 20,
// ✅ 跨平台阴影
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
},
android: {
elevation: 4,
},
}),
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: Platform.select({ ios: 14, android: 12 }), // ✅ 平台适配的内边距
marginBottom: 12,
fontSize: 16,
color: '#1a1a1a',
},
button: {
// ✅ 平台适配的主色
backgroundColor: Platform.select({
ios: '#007AFF', // iOS 系统蓝
android: '#6200EE', // Material 主色
}),
borderRadius: Platform.select({ ios: 10, android: 8 }),
padding: 14,
alignItems: 'center',
marginTop: 4,
},
buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});Flutter:忽略 Cupertino vs Material 差异
// ❌ AI 常见错误:在 iOS 上使用 Material 组件
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('设置')), // ❌ iOS 上应该用 CupertinoNavigationBar
body: ListView(
children: [
// ❌ iOS 上应该用 CupertinoSwitch
SwitchListTile(
title: Text('推送通知'),
value: true,
onChanged: (v) {},
),
// ❌ iOS 上日期选择器应该用滚轮样式
ListTile(
title: Text('生日'),
onTap: () {
showDatePicker( // ❌ 这是 Material 日历样式
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
},
),
],
),
);
}
}// ✅ 正确写法:平台自适应组件
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
// ✅ 根据平台选择不同的 Scaffold
if (Platform.isIOS) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('设置'),
),
child: _buildContent(context),
);
}
return Scaffold(
appBar: AppBar(title: const Text('设置')),
body: _buildContent(context),
);
}
Widget _buildContent(BuildContext context) {
return ListView(
children: [
// ✅ 平台自适应开关
_AdaptiveSwitch(
title: '推送通知',
value: true,
onChanged: (v) {},
),
// ✅ 平台自适应日期选择
ListTile(
title: const Text('生日'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showAdaptiveDatePicker(context),
),
],
);
}
void _showAdaptiveDatePicker(BuildContext context) {
if (Platform.isIOS) {
// ✅ iOS:底部弹出滚轮选择器
showCupertinoModalPopup(
context: context,
builder: (context) => Container(
height: 300,
color: CupertinoColors.systemBackground.resolveFrom(context),
child: Column(children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(child: const Text('取消'),
onPressed: () => Navigator.pop(context)),
CupertinoButton(child: const Text('确定'),
onPressed: () => Navigator.pop(context)),
],
),
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
onDateTimeChanged: (date) {},
),
),
]),
),
);
} else {
// ✅ Android:Material 日历选择器
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
}
}
}
// ✅ 平台自适应开关组件
class _AdaptiveSwitch extends StatelessWidget {
final String title;
final bool value;
final ValueChanged<bool> onChanged;
const _AdaptiveSwitch({
required this.title,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(title),
trailing: Platform.isIOS
? CupertinoSwitch(value: value, onChanged: onChanged)
: Switch(value: value, onChanged: onChanged),
);
}
}7.3 平台差异速查表
| 功能 | iOS 实现 | Android 实现 | React Native 适配 | Flutter 适配 |
|---|---|---|---|---|
| 返回导航 | 边缘滑动 | 系统返回键 | gestureEnabled | WillPopScope |
| 状态栏 | 自动适配 | 需设置 translucent | StatusBar 组件 | SystemChrome |
| 键盘避让 | 自动推高 | 需 windowSoftInputMode | KeyboardAvoidingView | Scaffold.resizeToAvoidBottomInset |
| 阴影 | shadow* 属性 | elevation | Platform.select | BoxShadow / elevation |
| 触觉反馈 | Haptic Engine | Vibration | expo-haptics | HapticFeedback |
| 安全存储 | Keychain | EncryptedSharedPreferences | expo-secure-store | flutter_secure_storage |
| 推送通知 | APNs | FCM | expo-notifications | firebase_messaging |
| 深色模式 | 系统级 | 系统级 | useColorScheme() | MediaQuery.platformBrightnessOf |
| 字体 | San Francisco | Roboto | 系统默认 | 系统默认 |
| 最小触摸目标 | 44x44pt | 48x48dp | 手动设置 | Material 自动处理 |
7.4 提示词模板:平台一致性检查
请检查以下跨平台移动端代码的平台一致性:
框架:[React Native / Flutter]
[粘贴代码]
检查清单:
1. 阴影效果是否在 iOS 和 Android 上都正确显示?
2. 键盘弹出时内容是否在两个平台上都正确避让?
3. 状态栏样式是否在两个平台上都正确?
4. 导航行为(返回手势/返回键)是否在两个平台上都正确?
5. 日期/时间选择器是否使用了平台原生样式?
6. 触觉反馈是否在两个平台上都有实现?
7. 安全区域(刘海屏、Dynamic Island)是否正确处理?
8. 字体渲染和间距是否在两个平台上视觉一致?
9. 组件是否遵循各自平台的设计规范(iOS HIG / Material Design)?
对于发现的每个不一致,请提供平台适配的修复代码。8. 反模式六:包体积过大(Oversized App Bundle)
8.1 问题描述
包体积直接影响用户的下载意愿和留存率。研究表明,应用体积每增加 6MB,安装转化率就会下降约 1%。AI 生成的代码特别容易导致包体积膨胀,因为 AI 倾向于引入功能丰富但体积庞大的第三方库,而不是使用轻量级替代方案或原生 API。
常见的包体积问题:
- 引入全量工具库:如 lodash 全量导入(~70KB gzipped)而非按需导入
- 未压缩的图片资源:高分辨率 PNG/JPEG 未经压缩直接打包
- 未使用的依赖:安装了但从未使用的 npm/pub 包
- 重复的功能库:同时引入 moment.js 和 date-fns,或 axios 和 fetch
- 未启用代码压缩:未配置 ProGuard/R8(Android)或未启用 Bitcode(iOS)
- 字体文件过大:引入完整字体族而非仅需要的字重
- 未使用 App Bundle:Android 使用 APK 而非 AAB 格式
8.2 AI 生成的问题代码 vs 正确代码
React Native:依赖膨胀
// ❌ AI 常见错误:引入不必要的重量级依赖
import _ from 'lodash'; // ❌ 全量导入 lodash(~70KB gzipped)
import moment from 'moment'; // ❌ moment.js(~67KB gzipped,已废弃)
import axios from 'axios'; // ❌ 有原生 fetch,不需要 axios
import { v4 as uuidv4 } from 'uuid'; // ❌ 可以用 crypto.randomUUID()
import numeral from 'numeral'; // ❌ 可以用 Intl.NumberFormat
function ProductCard({ product }) {
// 使用 lodash 只是为了一个简单的格式化
const formattedPrice = _.padStart(product.price.toFixed(2), 8, ' ');
// 使用 moment 只是为了格式化日期
const formattedDate = moment(product.createdAt).format('YYYY-MM-DD');
// 使用 numeral 只是为了格式化数字
const formattedSales = numeral(product.sales).format('0,0');
// 使用 uuid 生成唯一 ID
const trackingId = uuidv4();
return (/* ... */);
}// ✅ 正确写法:使用原生 API 和轻量级替代
// ✅ 不需要 lodash —— 使用原生方法
// ✅ 不需要 moment —— 使用 Intl.DateTimeFormat
// ✅ 不需要 axios —— 使用原生 fetch
// ✅ 不需要 uuid —— 使用 crypto.randomUUID()
// ✅ 不需要 numeral —— 使用 Intl.NumberFormat
function ProductCard({ product }) {
// ✅ 原生字符串方法
const formattedPrice = product.price.toFixed(2).padStart(8, ' ');
// ✅ Intl.DateTimeFormat(零依赖)
const formattedDate = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
}).format(new Date(product.createdAt));
// ✅ Intl.NumberFormat(零依赖)
const formattedSales = new Intl.NumberFormat('zh-CN').format(product.sales);
// ✅ crypto.randomUUID()(React Native 0.72+ 支持)
const trackingId = crypto.randomUUID();
return (/* ... */);
}
// ✅ 如果确实需要 lodash 的某个函数,按需导入
import debounce from 'lodash/debounce'; // ~1KB vs ~70KB
// 或使用更轻量的替代
import { debounce } from 'lodash-es'; // 支持 tree-shakingFlutter:资源未优化
// ❌ AI 常见错误:未优化的资源加载
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// 问题 1:直接加载原始大图(可能 5MB+)
Image.asset('assets/images/background.png'), // ❌ 未压缩的 PNG
// 问题 2:网络图片没有缓存和尺寸限制
Image.network(user.avatarUrl), // ❌ 可能加载 4000x4000 的原图
// 问题 3:使用完整的图标字体
Icon(Icons.home), // Material Icons 字体文件 ~1.5MB
],
);
}
}
// pubspec.yaml
// ❌ 引入了大量未使用的依赖
// dependencies:
// http: ^1.2.0
// dio: ^5.4.0 # ❌ 和 http 功能重复
// intl: ^0.19.0
// google_fonts: ^6.1.0 # ❌ 运行时下载字体,增加网络开销
// flutter_svg: ^2.0.0
// lottie: ^3.0.0 # ❌ 如果只用了一个动画,不值得引入// ✅ 正确写法:优化资源和依赖
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// ✅ 使用 WebP 格式(比 PNG 小 25-35%)
Image.asset(
'assets/images/background.webp',
// ✅ 限制解码尺寸,减少内存占用
cacheWidth: MediaQuery.sizeOf(context).width.toInt(),
),
// ✅ 使用 cached_network_image 缓存 + 限制尺寸
CachedNetworkImage(
imageUrl: user.avatarUrl,
// ✅ 请求服务端缩略图而非原图
// imageUrl: '${user.avatarUrl}?w=200&h=200&fit=crop',
memCacheWidth: 200, // ✅ 内存缓存尺寸限制
memCacheHeight: 200,
placeholder: (_, __) => const CircleAvatar(
backgroundColor: Colors.grey,
child: Icon(Icons.person),
),
errorWidget: (_, __, ___) => const CircleAvatar(
backgroundColor: Colors.grey,
child: Icon(Icons.error),
),
),
],
);
}
}
// ✅ pubspec.yaml 优化
// flutter:
// uses-material-design: false # ✅ 如果不用 Material Icons,关闭可省 ~1.5MB
// assets:
// - assets/images/ # ✅ 只包含需要的资源
//
// dependencies:
// http: ^1.2.0 # ✅ 只保留一个 HTTP 库
// cached_network_image: ^3.3.0 # ✅ 图片缓存
// flutter_svg: ^2.0.0 # ✅ 仅在确实需要 SVG 时引入8.3 包体积优化检查清单
| 优化项 | 预期节省 | 适用平台 | 操作方法 |
|---|---|---|---|
| 启用 ProGuard/R8 | APK 减少 20-30% | Android | minifyEnabled true in build.gradle |
| 使用 App Bundle (.aab) | 下载减少 15-20% | Android | flutter build appbundle / EAS Build |
| 图片转 WebP | 图片减少 25-35% | 全平台 | cwebp 工具批量转换 |
| 移除未使用依赖 | 视情况 | 全平台 | depcheck / dart pub outdated |
| 按需导入 lodash | ~69KB | React Native | import debounce from 'lodash/debounce' |
| 替换 moment.js | ~67KB | React Native | 使用 date-fns 或 Intl API |
| 字体子集化 | 字体减少 80-90% | 全平台 | fonttools / pyftsubset |
| 启用 Hermes | JS bundle 减少 50%+ | React Native | Expo SDK 53 默认启用 |
| Tree Shaking | 视情况 | Flutter | flutter build --release(自动) |
| 延迟加载 | 首屏减少 30-50% | 全平台 | 代码分割 + 懒加载 |
8.4 包体积分析工具
| 工具 | 平台 | 价格 | 用途 |
|---|---|---|---|
| react-native-bundle-visualizer | React Native | 免费(开源) | JS bundle 可视化分析 |
| Bundlephobia | React Native | 免费 | 依赖体积在线查询 |
| source-map-explorer | React Native | 免费(开源) | Source map 分析 |
| flutter build —analyze-size | Flutter | 免费 | Flutter 官方体积分析 |
| Xcode Build Report | iOS | 免费 | iOS 包体积详细报告 |
| Android Studio APK Analyzer | Android | 免费 | APK/AAB 内容分析 |
| Emerge Tools | iOS/Android | 免费额度 / $500/月 | CI 集成的体积监控 |
| depcheck | React Native | 免费(开源) | 检测未使用的 npm 依赖 |
8.5 提示词模板:包体积优化
请分析以下移动端项目的包体积优化机会:
框架:[React Native / Flutter / iOS / Android]
当前包体积:[XX MB]
目标包体积:[XX MB]
package.json / pubspec.yaml 内容:
[粘贴依赖列表]
请检查:
1. 是否有可以用原生 API 替代的第三方库?
2. 是否有功能重复的依赖?
3. 是否有未使用的依赖?
4. 图片资源是否已压缩?是否可以转为 WebP?
5. 字体文件是否已子集化?
6. 是否启用了代码压缩(ProGuard/R8/Hermes)?
7. 是否使用了 App Bundle 而非 APK?
8. 是否有可以延迟加载的模块?
对于每个优化建议,请提供:
- 预期节省的体积
- 具体的操作步骤
- 是否有功能影响9. 反模式七:生命周期管理缺失(Missing Lifecycle Management)
9.1 问题描述
移动端应用有复杂的生命周期——前台、后台、挂起、终止、低内存警告等状态切换。AI 生成的代码通常只考虑”应用在前台正常运行”的场景,忽略了后台切换、低内存、屏幕旋转等关键生命周期事件。
9.2 AI 生成的问题代码 vs 正确代码
React Native:不处理应用生命周期
// ❌ AI 常见错误:不处理应用前后台切换
function VideoPlayerScreen({ videoUrl }) {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(true);
useEffect(() => {
// 问题 1:应用进入后台时视频继续播放(浪费电池,可能违反 App Store 规则)
// 问题 2:应用恢复前台时不刷新数据
// 问题 3:低内存时不释放缓存
// 问题 4:WebSocket 连接在后台不断开
}, []);
return <Video ref={videoRef} source={{ uri: videoUrl }} />;
}// ✅ 正确写法:完整的生命周期管理
import { useEffect, useRef, useState, useCallback } from 'react';
import { AppState, Platform } from 'react-native';
import { Video, ResizeMode } from 'expo-av';
import { useIsFocused } from '@react-navigation/native';
function VideoPlayerScreen({ videoUrl }) {
const videoRef = useRef<Video>(null);
const [isPlaying, setIsPlaying] = useState(true);
const appState = useRef(AppState.currentState);
const wasPlayingBeforeBackground = useRef(false);
const isFocused = useIsFocused(); // ✅ 导航焦点状态
// ✅ 应用前后台切换处理
useEffect(() => {
const subscription = AppState.addEventListener('change', (nextState) => {
if (appState.current === 'active' && nextState.match(/inactive|background/)) {
// 进入后台:暂停视频
wasPlayingBeforeBackground.current = isPlaying;
videoRef.current?.pauseAsync();
setIsPlaying(false);
} else if (
appState.current.match(/inactive|background/) &&
nextState === 'active'
) {
// 恢复前台:如果之前在播放,则继续
if (wasPlayingBeforeBackground.current) {
videoRef.current?.playAsync();
setIsPlaying(true);
}
}
appState.current = nextState;
});
return () => subscription.remove();
}, [isPlaying]);
// ✅ 导航离开时暂停
useEffect(() => {
if (!isFocused) {
videoRef.current?.pauseAsync();
}
}, [isFocused]);
// ✅ 组件卸载时释放资源
useEffect(() => {
return () => {
videoRef.current?.unloadAsync(); // ✅ 释放视频资源
};
}, []);
return (
<Video
ref={videoRef}
source={{ uri: videoUrl }}
resizeMode={ResizeMode.CONTAIN}
shouldPlay={isPlaying && isFocused}
// ✅ 处理播放错误
onError={(error) => {
console.error('Video playback error:', error);
setIsPlaying(false);
}}
/>
);
}Jetpack Compose:不处理配置变更
// ❌ AI 常见错误:屏幕旋转时状态丢失
@Composable
fun FormScreen() {
// 问题:这些状态在配置变更(屏幕旋转)时会丢失
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var selectedDate by remember { mutableStateOf<LocalDate?>(null) }
var formStep by remember { mutableIntStateOf(0) }
// ❌ 用户填了一半表单,旋转屏幕后全部清空
Column {
if (formStep == 0) {
TextField(value = name, onValueChange = { name = it }, label = { Text("姓名") })
TextField(value = email, onValueChange = { email = it }, label = { Text("邮箱") })
} else {
// 第二步...
}
}
}// ✅ 正确写法:使用 ViewModel 保存状态
class FormViewModel : ViewModel() {
// ✅ ViewModel 在配置变更时存活
var name by mutableStateOf("")
var email by mutableStateOf("")
var selectedDate by mutableStateOf<LocalDate?>(null)
var formStep by mutableIntStateOf(0)
// ✅ 使用 SavedStateHandle 在进程被杀死后恢复
// constructor(savedStateHandle: SavedStateHandle) : this() {
// name = savedStateHandle["name"] ?: ""
// email = savedStateHandle["email"] ?: ""
// }
}
@Composable
fun FormScreen(viewModel: FormViewModel = viewModel()) {
// ✅ 状态在屏幕旋转时保持
Column {
if (viewModel.formStep == 0) {
TextField(
value = viewModel.name,
onValueChange = { viewModel.name = it },
label = { Text("姓名") }
)
TextField(
value = viewModel.email,
onValueChange = { viewModel.email = it },
label = { Text("邮箱") }
)
} else {
// 第二步...
}
}
// ✅ 处理返回键:未保存的表单需确认
BackHandler(enabled = viewModel.name.isNotEmpty() || viewModel.email.isNotEmpty()) {
// 显示"放弃编辑?"确认对话框
}
}10. 反模式八:安全存储缺失(Insecure Data Storage)
10.1 问题描述
AI 生成的移动端代码经常将敏感数据存储在不安全的位置——使用 AsyncStorage/SharedPreferences 存储 token、在日志中输出用户信息、在代码中硬编码 API 密钥。移动端的安全存储比 Web 更复杂,因为应用数据可能被越狱/root 设备访问。
10.2 AI 生成的问题代码 vs 正确代码
React Native:不安全的数据存储
// ❌ AI 常见错误:敏感数据存储在不安全的位置
import AsyncStorage from '@react-native-async-storage/async-storage';
async function login(email: string, password: string) {
const response = await fetch('https://api.example.com/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await response.json();
// ❌ Token 存储在 AsyncStorage(未加密,可被其他应用读取)
await AsyncStorage.setItem('auth_token', data.token);
await AsyncStorage.setItem('refresh_token', data.refreshToken);
// ❌ 用户密码存储在本地(绝对不应该)
await AsyncStorage.setItem('user_password', password);
// ❌ 在日志中输出 token
console.log('Login successful, token:', data.token);
}
// ❌ API 密钥硬编码在代码中
const API_KEY = 'sk-1234567890abcdef';
const STRIPE_KEY = 'pk_live_xxxxxxxxxxxxx';// ✅ 正确写法:使用安全存储
import * as SecureStore from 'expo-secure-store';
// ✅ API 密钥通过环境变量注入
// 在 app.config.ts 或 .env 中配置,不提交到代码仓库
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL;
async function login(email: string, password: string) {
const response = await fetch(`${API_BASE_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error(`Login failed: ${response.status}`);
}
const data = await response.json();
// ✅ Token 存储在安全存储(iOS Keychain / Android EncryptedSharedPreferences)
await SecureStore.setItemAsync('auth_token', data.token, {
keychainAccessible: SecureStore.WHEN_UNLOCKED, // ✅ 仅在设备解锁时可访问
});
await SecureStore.setItemAsync('refresh_token', data.refreshToken, {
keychainAccessible: SecureStore.WHEN_UNLOCKED,
});
// ✅ 绝不存储密码
// ✅ 绝不在日志中输出敏感信息
if (__DEV__) {
console.log('Login successful'); // ✅ 仅在开发环境输出,且不包含 token
}
}
// ✅ 安全的 token 读取
async function getAuthToken(): Promise<string | null> {
try {
return await SecureStore.getItemAsync('auth_token');
} catch {
return null; // ✅ 安全存储不可用时优雅降级
}
}
// ✅ 安全的登出
async function logout() {
await SecureStore.deleteItemAsync('auth_token');
await SecureStore.deleteItemAsync('refresh_token');
}10.3 安全存储对比
| 存储方式 | 加密 | 安全级别 | 适用数据 | 平台 |
|---|---|---|---|---|
| AsyncStorage / SharedPreferences | ❌ 无 | 🔴 低 | 非敏感设置、缓存 | 全平台 |
| MMKV | ❌ 默认无 | 🟡 中 | 非敏感高频读写数据 | 全平台 |
| expo-secure-store | ✅ 系统级 | 🟢 高 | Token、密码、API key | React Native |
| flutter_secure_storage | ✅ 系统级 | 🟢 高 | Token、密码、API key | Flutter |
| iOS Keychain | ✅ 硬件级 | 🟢 最高 | 凭证、证书 | iOS |
| Android EncryptedSharedPreferences | ✅ AES-256 | 🟢 高 | 凭证、敏感配置 | Android |
| Android Keystore | ✅ 硬件级 | 🟢 最高 | 加密密钥、证书 | Android |
实战案例:从”AI 生成的 MVP”到”生产级移动应用”
案例背景
一个独立开发者使用 Claude Code + Expo 在 3 天内生成了一个外卖配送追踪应用的 MVP。应用在模拟器上运行完美,但上线后收到大量用户投诉:
- ⭐ 1.2 分(App Store 评分)
- 投诉 1:“用了 2 小时手机就没电了”(电池消耗)
- 投诉 2:“地铁里打开全是白屏”(离线支持差)
- 投诉 3:“Android 上按钮点不了”(平台不一致)
- 投诉 4:“用着用着就闪退”(内存泄漏)
- 投诉 5:“下载要 180MB 太大了”(包体积过大)
- 投诉 6:“第一次打开就要 6 个权限,吓人”(权限处理差)
问题诊断
开发者使用以下 Steering 规则 Prompt 让 AI 诊断问题:
请作为移动端性能专家,审查这个 Expo + React Native 外卖配送追踪应用。
用户反馈了以下问题:电池消耗快、离线白屏、Android 兼容性差、频繁闪退、包体积大、权限请求体验差。
请逐一分析可能的根因,并按严重程度排序。
项目技术栈:
- Expo SDK 53 + React Native 0.79
- React Navigation v7
- 状态管理:useState + useContext(无外部库)
- 网络请求:axios
- 定位:expo-location
- 地图:react-native-maps
- 存储:AsyncStorage
请检查以下文件:[列出关键文件]诊断结果与修复
| 问题 | 根因 | 严重程度 | 修复方案 | 修复耗时 |
|---|---|---|---|---|
| 电池消耗 | GPS 每秒更新 + 5 秒轮询订单 | 🔴 致命 | 降低定位频率到 50m/10s,轮询改为 30s + WebSocket | 2 小时 |
| 离线白屏 | 无缓存,所有数据依赖网络 | 🔴 致命 | 引入 TanStack Query + AsyncStorage 持久化 | 4 小时 |
| Android 兼容 | 阴影用 iOS API,键盘不避让 | 🟡 严重 | Platform.select 适配 + KeyboardAvoidingView | 3 小时 |
| 闪退 | 5 个 useEffect 无清理函数 | 🔴 致命 | 添加清理函数 + LeakCanary 检测 | 2 小时 |
| 包体积 | lodash 全量 + 未压缩图片 + moment.js | 🟡 严重 | 按需导入 + WebP + date-fns | 1 小时 |
| 权限体验 | 启动时一次请求 6 个权限 | 🟡 严重 | 改为 just-in-time 按需请求 + 说明弹窗 | 2 小时 |
修复后的 Steering 规则
开发者在修复后创建了以下 CLAUDE.md 规则,防止 AI 再次生成同类问题:
# CLAUDE.md — 外卖配送追踪应用
## 🚫 绝对禁止(上次上线踩过的坑)
1. 禁止 GPS accuracy 设置为 BestForNavigation(使用 Balanced)
2. 禁止轮询间隔 < 30 秒(使用 WebSocket 替代高频轮询)
3. 禁止 useEffect 没有 return 清理函数
4. 禁止 import _ from 'lodash'(按需导入)
5. 禁止 import moment(使用 date-fns 或 Intl)
6. 禁止在启动时请求权限(按需请求)
7. 禁止 AsyncStorage 存储 token(使用 expo-secure-store)
8. 禁止不处理离线状态(所有 API 调用必须有离线降级)
## ⚠️ 每次生成代码前检查
- 是否处理了 iOS 和 Android 的差异?
- 是否有离线降级方案?
- 是否有清理函数?
- 新依赖的体积是多少?有没有更轻量的替代?修复效果
| 指标 | 修复前 | 修复后 | 改善 |
|---|---|---|---|
| App Store 评分 | ⭐ 1.2 | ⭐ 4.3 | +3.1 |
| 电池消耗(2 小时使用) | 45% | 12% | -73% |
| 离线可用性 | 0%(白屏) | 85%(缓存数据) | +85% |
| Android 崩溃率 | 8.5% | 0.3% | -96% |
| 包体积(iOS) | 180MB | 52MB | -71% |
| 冷启动时间 | 4.2s | 1.8s | -57% |
案例分析
这个案例的核心教训:
- AI 生成的 MVP 不等于生产级应用:AI 擅长快速生成功能代码,但缺乏移动端特有的性能、电池、离线等非功能性考量
- Steering 规则是”踩坑记录”:最好的 Steering 规则来自实际踩过的坑,每次修复 bug 后都应该更新规则
- 移动端的”隐性需求”比 Web 多得多:电池、权限、离线、平台差异、包体积——这些在 Web 上不存在或不重要的问题,在移动端是核心体验
- 诊断工具不可或缺:没有 Instruments、Android Profiler、LeakCanary 等工具,很多问题在开发阶段根本发现不了
避坑指南
❌ 常见错误
-
useEffect 没有清理函数
- 问题:订阅、定时器、监听器在组件卸载后继续运行,导致内存泄漏和意外行为
- 正确做法:每个 useEffect 都必须返回清理函数,取消所有副作用
-
GPS 定位精度设置过高
- 问题:BestForNavigation 精度会持续使用 GPS 硬件,极度耗电
- 正确做法:大多数场景使用 Balanced(~100m)精度即可,仅导航场景使用高精度
-
不检查权限直接调用 API
- 问题:相机、定位等 API 在无权限时崩溃或返回空数据
- 正确做法:先检查权限状态,显示说明弹窗,处理拒绝和永久拒绝
-
完全依赖网络,无离线策略
- 问题:弱网/离线时应用白屏或显示无意义的错误
- 正确做法:本地缓存关键数据,离线队列保存写操作,网络恢复后自动同步
-
忽略 iOS 和 Android 的差异
- 问题:阴影、键盘、导航、触觉反馈等在两个平台上行为不同
- 正确做法:使用 Platform.select() 处理差异,分别测试两个平台
-
引入全量工具库
- 问题:lodash(~70KB)、moment.js(~67KB)等大幅增加包体积
- 正确做法:按需导入或使用原生 API(Intl、Array 方法等)
-
敏感数据存储在 AsyncStorage
- 问题:AsyncStorage 未加密,越狱/root 设备可直接读取
- 正确做法:Token 和密码使用 expo-secure-store / flutter_secure_storage
-
不处理应用生命周期
- 问题:后台时继续播放视频/轮询/定位,浪费电池和流量
- 正确做法:监听 AppState 变化,后台时暂停非必要操作
✅ 最佳实践
- 先写 Steering 规则,再让 AI 生成代码:在项目开始前就创建移动端专用的 CLAUDE.md / Kiro Steering 规则
- 每次修复 bug 后更新 Steering 规则:把踩过的坑转化为规则,防止 AI 再犯
- 在真机上测试,不只是模拟器:内存泄漏、电池消耗、性能问题在模拟器上往往不明显
- 使用平台原生的性能分析工具:Xcode Instruments、Android Profiler、Flipper
- 包体积预算制:设定包体积上限(如 50MB),每次添加依赖前检查影响
- 离线优先设计:假设网络不可用,先设计离线体验,再添加在线增强
- 权限按需请求:在用户即将使用功能时才请求权限,并提供清晰的理由说明
- 自动化质量门:CI/CD 中集成包体积检查、lint 规则、性能基准测试
相关资源与延伸阅读
工具与框架
- LeakCanary — Android 内存泄漏自动检测库,Square 开源,集成简单,生产环境必备
- Flipper — Meta 开源的移动端调试平台,支持 React Native/iOS/Android 的内存、网络、布局调试
- TanStack Query — 服务端状态管理库,内置缓存持久化、离线支持、自动重试,React Native 离线优先架构的核心
- WatermelonDB — React Native 高性能离线数据库,基于 SQLite,支持懒加载和同步
- Emerge Tools — iOS/Android 包体积分析和监控平台,CI 集成,自动检测体积回归
社区与教程
- React Native Performance — React Native 官方性能优化指南,覆盖 JS 线程、原生线程、内存管理
- Flutter Performance Best Practices — Flutter 官方性能最佳实践,覆盖 Widget 重建、着色器编译、内存管理
- Apple Human Interface Guidelines - iOS — Apple 官方 iOS 设计规范,权限请求、导航模式、无障碍的权威参考
- Material Design 3 — Google Material Design 3 规范,Android 组件、动态颜色、自适应布局的权威参考
- Awesome React Native — React Native 社区精选资源列表,包含性能优化、调试工具、最佳实践
参考来源
- Battery Usage Optimization in Mobile Apps (2025)
- Memory Leaks: Architecture Problems in Modern Mobile Development (2025)
- How to Reduce React Native App Bundle Size (2026)
- How to Reduce Flutter App Size by 40% (2025)
- Offline-First React Native: Apps That Work Without Internet (2025)
- Offline-First Flutter: Implementation Blueprint (2025)
- Handling React Native Permissions for iOS and Android (2025)
- Timing, Strategy & Compliance Guide for Mobile Permissions (2025)
- The CLAUDE.md Workflow: AI Coding Productivity (2025)
- Building Mobile Apps with Claude Code (2025)
- Flutter Tree Shaking & Bundle Optimization (2025)
- The Truth About Memory Leaks in Flutter (2025)
- Finding and Fixing Memory Leaks in React Native iOS Apps (2024)
- Building Secure Mobile Apps: iOS and Android Best Practices (2025)
📖 返回 总览与导航 | 上一节:AI辅助移动端UI | 下一节:AI辅助游戏开发概览