本章涵盖单元测试、Widget 测试、集成测试、CI/CD 流水线、构建发布、安全隐私保护五大工程质量保障环节。
📋 章节目录
| 节 | 主题 | 核心内容 |
|---|---|---|
| 10.1 | 单元测试与组件测试 | test 包、flutter_test、mockito |
| 10.2 | 集成测试 | integration_test、真机测试 |
| 10.3 | 构建与发布 | APK/AAB/IPA、签名混淆 |
| 10.4 | 安全与隐私 | 安全存储、生物识别、防逆向 |
| 10.5 | CI/CD 自动化流水线 | GitHub Actions、质量门禁 |
10.1 单元测试与组件测试
Flutter 提供了完整的测试体系,从纯逻辑的单元测试到 Widget 级的组件测试,确保代码质量和功能正确性。
一、test 包(单元测试)
1.1 基本测试
dart
// test/cart_model_test.dart
import 'package:test/test.dart';
import 'package:my_app/models/cart_model.dart';
void main() {
group('CartModel', () {
late CartModel cart;
setUp(() {
cart = CartModel(); // 每个测试前重置
});
tearDown(() {
cart.dispose(); // 每个测试后清理
});
test('初始状态:购物车为空', () {
expect(cart.items, isEmpty);
expect(cart.totalPrice, equals(0.0));
expect(cart.itemCount, equals(0));
});
test('添加商品:数量递增', () {
final product = Product(id: 1, name: 'Flutter Book', price: 99.0);
cart.addItem(product);
expect(cart.itemCount, equals(1));
expect(cart.totalPrice, equals(99.0));
});
test('重复添加:数量合并', () {
final product = Product(id: 1, name: 'Flutter Book', price: 99.0);
cart.addItem(product);
cart.addItem(product);
expect(cart.items.length, equals(1));
expect(cart.items.first.quantity, equals(2));
expect(cart.totalPrice, equals(198.0));
});
test('移除商品', () {
final product = Product(id: 1, name: 'Test', price: 50.0);
cart.addItem(product);
cart.removeItem(product.id.toString());
expect(cart.items, isEmpty);
});
test('计算折扣后总价', () {
cart.addItem(Product(id: 1, name: 'A', price: 100.0));
cart.addItem(Product(id: 2, name: 'B', price: 50.0));
cart.applyDiscount(0.1); // 9折
expect(cart.totalPrice, closeTo(135.0, 0.01));
});
});
}
1.2 异步测试
dart
test('异步获取用户数据', () async {
final repo = UserRepository(mockApi);
final user = await repo.fetchUser(1);
expect(user.id, equals(1));
expect(user.name, isNotEmpty);
});
test('Stream 测试', () async {
final controller = StreamController<int>();
final values = <int>[];
controller.stream.listen((v) => values.add(v));
controller.add(1);
controller.add(2);
controller.add(3);
await controller.close();
expect(values, equals([1, 2, 3]));
});
二、flutter_test(Widget 测试)
2.1 基本 Widget 测试
dart
// test/widget/login_form_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/login_form.dart';
void main() {
testWidgets('LoginForm: 空表单提交显示验证错误', (tester) async {
bool submitted = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: LoginForm(
onSubmit: (email, password) => submitted = true,
),
),
),
);
final submitButton = find.byType(ElevatedButton);
await tester.tap(submitButton);
await tester.pump();
expect(find.text('请输入邮箱'), findsOneWidget);
expect(find.text('请输入密码'), findsOneWidget);
expect(submitted, isFalse);
});
testWidgets('LoginForm: 输入正确信息后提交成功', (tester) async {
String? submittedEmail;
String? submittedPassword;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: LoginForm(
onSubmit: (email, password) {
submittedEmail = email;
submittedPassword = password;
},
),
),
),
);
await tester.enterText(find.byKey(const Key('email_field')), 'test@example.com');
await tester.enterText(find.byKey(const Key('password_field')), 'password123');
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(submittedEmail, equals('test@example.com'));
expect(submittedPassword, equals('password123'));
});
}
2.2 常用 find 查找器
dart
find.text('提交') // 按文字查找
find.byType(ElevatedButton) // 按 Widget 类型查找
find.byKey(const Key('id')) // 按 Key 查找
find.byIcon(Icons.add) // 按图标查找
find.bytooltip('Add Item') // 按 Tooltip 查找
find.descendant( // 在后代中查找
of: find.byType(Card),
matching: find.byType(Text),
)
2.3 pump 方法
dart
await tester.pump(); // 触发一帧
await tester.pump(const Duration(milliseconds: 300)); // 等待指定时长
await tester.pumpAndSettle(); // 等待所有动画完成
2.4 手势模拟
dart
await tester.tap(finder); // 点击
await tester.doubleTap(finder); // 双击
await tester.longPress(finder); // 长按
await tester.drag(finder, Offset(0, -200)); // 拖拽
await tester.fling(finder, Offset(-200, 0), 1000); // 快速滑动
await tester.enterText(finder, 'input'); // 文字输入
三、Mock 与依赖注入测试
yaml
dev_dependencies:
mockito: ^5.4.4
build_runner: ^2.4.0
dart
@GenerateMocks([UserRepository, AuthService])
import 'user_controller_test.mocks.dart';
void main() {
late MockUserRepository mockRepo;
late MockAuthService mockAuth;
late UserController controller;
setUp(() {
mockRepo = MockUserRepository();
mockAuth = MockAuthService();
controller = UserController(
repository: mockRepo,
authService: mockAuth,
);
});
test('loadUser: 成功时更新用户状态', () async {
final expectedUser = User(id: 1, name: 'Alice');
when(mockRepo.fetchUser(1)).thenAnswer((_) async => expectedUser);
await controller.loadUser(1);
expect(controller.currentUser, equals(expectedUser));
expect(controller.isLoading, isFalse);
verify(mockRepo.fetchUser(1)).called(1);
});
test('loadUser: 网络失败时设置错误状态', () async {
when(mockRepo.fetchUser(any))
.thenThrow(NetworkException(message: 'Connection failed'));
await controller.loadUser(1);
expect(controller.errorMessage, isNotNull);
expect(controller.currentUser, isNull);
});
}
小结
| 测试类型 | 包 | 适用场景 |
|---|---|---|
| 单元测试 | test |
纯逻辑(Model、Controller、Service) |
| Widget 测试 | flutter_test |
UI 组件行为验证 |
| Mock | mockito |
隔离依赖,控制测试条件 |
bash
flutter test # 运行所有测试
flutter test test/cart_model_test.dart # 运行特定文件
flutter test --coverage # 显示覆盖率
genhtml coverage/lcov.info -o coverage/html
10.2 集成测试
集成测试(Integration Test)在真实设备或模拟器上运行,验证完整的用户交互流程,是端到端测试的有效手段。
一、integration_test 包
yaml
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
1.1 基本集成测试
dart
// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('登录流程集成测试', () {
testWidgets('完整登录流程', (tester) async {
app.main();
await tester.pumpAndSettle();
expect(find.text('欢迎登录'), findsOneWidget);
expect(find.byKey(const Key('email_field')), findsOneWidget);
await tester.enterText(find.byKey(const Key('email_field')), 'test@example.com');
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('password_field')), 'password123');
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('login_button')));
await tester.pumpAndSettle(const Duration(seconds: 5));
expect(find.text('首页'), findsOneWidget);
expect(find.text('欢迎登录'), findsNothing);
});
testWidgets('错误密码显示错误提示', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('email_field')), 'test@example.com');
await tester.enterText(find.byKey(const Key('password_field')), 'wrong_password');
await tester.tap(find.byKey(const Key('login_button')));
await tester.pumpAndSettle(const Duration(seconds: 3));
expect(find.text('账号或密码错误'), findsOneWidget);
});
});
group('购物车流程集成测试', () {
testWidgets('完整加购到结算流程', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('product_tab')));
await tester.pumpAndSettle();
final firstProduct = find.byKey(const Key('product_item_0'));
expect(firstProduct, findsOneWidget);
await tester.tap(find.descendant(
of: firstProduct,
matching: find.byKey(const Key('add_to_cart')),
));
await tester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
await tester.tap(find.byKey(const Key('cart_tab')));
await tester.pumpAndSettle();
expect(find.byType(CartItemTile), findsOneWidget);
});
});
}
1.2 运行集成测试
bash
flutter test integration_test/app_test.dart
flutter test integration_test/app_test.dart -d emulator-5554
flutter test integration_test/
二、CI/CD 集成
2.1 GitHub Actions 示例
yaml
name: Flutter Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.0'
channel: stable
- name: Install dependencies
run: flutter pub get
- name: Generate code
run: dart run build_runner build --delete-conflicting-outputs
- name: Run unit & widget tests
run: flutter test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
integration-test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- name: Start iOS Simulator
run: |
xcrun simctl boot "iPhone 15"
sleep 30
- name: Run integration tests
run: flutter test integration_test/ -d "iPhone 15"
2.2 Fastlane 自动化
ruby
# fastlane/Fastfile
lane :test do
sh("flutter test --coverage")
sh("flutter test integration_test/ -d emulator")
end
lane :deploy_android do
sh("flutter build appbundle --release --dart-define-from-file=.env.prod")
upload_to_play_store(
track: 'internal',
aab: '../build/app/outputs/bundle/release/app-release.aab'
)
end
小结
| 特性 | 单元测试 | 集成测试 |
|---|---|---|
| 运行速度 | 快(毫秒级) | 慢(秒级,需设备) |
| 覆盖范围 | 单个类/函数 | 完整用户流程 |
| 依赖 | 可 Mock | 使用真实依赖 |
| CI 频率 | 每次 PR | 每日/每次发布 |
10.3 构建与发布
构建和发布是 Flutter 项目生命周期的最后一环,掌握各平台的构建命令、代码混淆和签名配置,确保安全高效地交付应用。
一、构建命令
1.1 Android
bash
# APK(测试/分发)
flutter build apk --release
flutter build apk --release --split-per-abi # 按 ABI 分包,减小包体
# AAB(Google Play 上架,推荐)
flutter build appbundle --release
# 指定环境
flutter build appbundle \
--release \
--flavor production \
--dart-define-from-file=.env.prod \
-t lib/main.dart
# 输出路径
# build/app/outputs/apk/release/app-release.apk
# build/app/outputs/bundle/release/app-release.aab
1.2 iOS
bash
# 构建(需要 macOS + Xcode)
flutter build ios --release
# 生成 IPA(需要 Apple 开发者账号)
flutter build ipa --release
# 指定 Flavor
flutter build ipa \
--flavor production \
--dart-define-from-file=.env.prod
1.3 Web
bash
flutter build web --release
flutter build web --web-renderer canvaskit --release
# 部署到 Firebase Hosting
firebase deploy --only hosting
1.4 桌面
bash
flutter build macos --release
flutter build windows --release
flutter build linux --release
二、代码混淆与优化
2.1 混淆配置
bash
# Android:开启混淆 + 符号分离
flutter build apk --release \
--obfuscate \
--split-debug-info=build/debug-info/android
# iOS:开启混淆
flutter build ipa --release \
--obfuscate \
--split-debug-info=build/debug-info/ios
--split-debug-info 会将调试符号分离到指定目录,上传到 Sentry 后可恢复混淆后的堆栈。
bash
dart run sentry_dart_plugin
2.2 ProGuard 配置(Android)
# android/app/proguard-rules.pro
-keep class io.flutter.** { *; }
-keep class com.google.firebase.** { *; }
-dontwarn com.example.**
三、签名配置
3.1 Android 签名
bash
# 创建 Keystore
keytool -genkey -v -keystore my-release-key.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias my-key-alias
# 不要将 keystore 提交到 Git!
properties
# android/key.properties(不提交到 Git)
storePassword=your_password
keyPassword=your_key_password
keyAlias=my-key-alias
storeFile=../../my-release-key.jks
groovy
// android/app/build.gradle
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ?
file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
}
3.2 iOS 签名
iOS 签名通过 Xcode 的 Signing & Capabilities 配置,或在 CI 中使用 Fastlane match:
ruby
# fastlane/Matchfile
git_url("https://github.com/org/certificates.git")
app_identifier("com.example.myapp")
username("developer@example.com")
lane :setup_signing do
match(type: "appstore", readonly: true)
end
四、App Bundle(AAB)与动态交付
bash
flutter build appbundle --release
AAB 优势:
- Google Play 根据用户设备动态分发,减少下载体积
- 支持 Play Feature Delivery(按需下载功能模块)
- 自动支持 App Bundle 分包(语言/密度/ABI)
五、发布到各平台
| 平台 | 工具 | 方式 |
|---|---|---|
| Google Play | Google Play Console | 上传 AAB |
| App Store | Xcode / Transporter / Fastlane | 上传 IPA |
| Web | Firebase Hosting / Vercel | 上传 build/web |
| Windows | Microsoft Store / 自行分发 | MSIX 安装包 |
小结
| 步骤 | 要点 |
|---|---|
| 构建 | APK 用于测试,AAB 用于 Google Play |
| 混淆 | --obfuscate + --split-debug-info |
| 签名 | Keystore 不提交 Git,CI 用环境变量注入 |
| AAB | 动态交付减小下载体积 |
10.4 安全与隐私
移动端应用面临逆向工程、数据泄露、中间人攻击等安全威胁。掌握安全存储、网络安全和防逆向手段,是生产级 App 的基本要求。
一、安全存储(flutter_secure_storage)
shared_preferences 以明文存储,不适合存放敏感数据。flutter_secure_storage 使用 iOS Keychain / Android EncryptedSharedPreferences 加密存储。
yaml
dependencies:
flutter_secure_storage: ^9.2.2
dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorageService {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
static Future<void> saveToken(String token) =>
_storage.write(key: 'access_token', value: token);
static Future<String?> getToken() =>
_storage.read(key: 'access_token');
static Future<void> deleteToken() =>
_storage.delete(key: 'access_token');
static Future<void> saveRefreshToken(String token) =>
_storage.write(key: 'refresh_token', value: token);
static Future<String?> getRefreshToken() =>
_storage.read(key: 'refresh_token');
static Future<void> clearAll() => _storage.deleteAll();
}
二、生物识别认证
yaml
dependencies:
local_auth: ^2.2.0
dart
import 'package:local_auth/local_auth.dart';
class BiometricService {
static final _auth = LocalAuthentication();
static Future<bool> isAvailable() async {
final canAuth = await _auth.canCheckBiometrics;
final isDeviceSupported = await _auth.isDeviceSupported();
return canAuth && isDeviceSupported;
}
static Future<List<BiometricType>> getAvailableBiometrics() =>
_auth.getAvailableBiometrics();
static Future<bool> authenticate({
String reason = '请验证身份以继续操作',
}) async {
try {
return await _auth.authenticate(
localizedReason: reason,
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: false,
useErrorDialogs: true,
),
);
} on PlatformException catch (e) {
debugPrint('Biometric error: ${e.code} - ${e.message}');
return false;
}
}
}
ElevatedButton(
onPressed: () async {
final authenticated = await BiometricService.authenticate(
reason: '请验证指纹以查看支付密码',
);
if (authenticated) {
_showSecureContent();
}
},
child: const Text('指纹解锁'),
)
三、网络安全
3.1 证书锁定(Certificate Pinning)
dart
import 'package:dio/dio.dart';
import 'dart:io';
class SecureDioClient {
static Dio createSecureDio() {
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
final context = SecurityContext();
context.setTrustedCertificatesBytes(
File('assets/certs/api_certificate.pem').readAsBytesSync(),
);
return HttpClient(context: context)
..badCertificateCallback = (cert, host, port) {
final expectedFingerprint = 'AB:CD:EF:...';
return cert.sha256Fingerprint == expectedFingerprint;
};
};
return dio;
}
}
3.2 敏感数据传输加密
dart
import 'package:encrypt/encrypt.dart';
class EncryptionService {
static final _key = Key.fromUtf8('my32lengthsupersecretnooneknows1');
static final _iv = IV.fromLength(16);
static final _encrypter = Encrypter(AES(_key));
static String encrypt(String plainText) {
return _encrypter.encrypt(plainText, iv: _iv).base64;
}
static String decrypt(String encryptedText) {
return _encrypter.decrypt64(encryptedText, iv: _iv);
}
}
四、防逆向与代码保护
4.1 代码混淆
bash
flutter build apk --release \
--obfuscate \
--split-debug-info=build/debug-info/
flutter build ipa --release \
--obfuscate \
--split-debug-info=build/debug-info/
4.2 Root / Jailbreak 检测
yaml
dependencies:
flutter_jailbreak_detection: ^1.10.0
dart
import 'package:flutter_jailbreak_detection/flutter_jailbreak_detection.dart';
class SecurityCheck {
static Future<bool> isDeviceSecure() async {
final isJailbroken = await FlutterJailbreakDetection.jailbroken;
final isDeveloperMode = await FlutterJailbreakDetection.developerMode;
if (isJailbroken) {
SecurityLogger.log('Jailbroken device detected');
return false;
}
return true;
}
}
4.3 截图保护
dart
import 'package:flutter_windowmanager/flutter_windowmanager.dart';
@override
void initState() {
super.initState();
if (Platform.isAndroid) {
FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE);
}
}
@override
void dispose() {
if (Platform.isAndroid) {
FlutterWindowManager.clearFlags(FlutterWindowManager.FLAG_SECURE);
}
super.dispose();
}
五、数据安全实践
dart
class DataSanitizer {
// 手机号脱敏:138****0000
static String maskPhone(String phone) {
if (phone.length < 7) return phone;
return '${phone.substring(0, 3)}****${phone.substring(phone.length - 4)}';
}
// 邮箱脱敏:t***@example.com
static String maskEmail(String email) {
final parts = email.split('@');
if (parts.length != 2 || parts[0].length < 2) return email;
return '${parts[0][0]}***@${parts[1]}';
}
// 身份证脱敏
static String maskIdCard(String idCard) {
if (idCard.length < 8) return idCard;
return '${idCard.substring(0, 3)}${'*' * (idCard.length - 7)}${idCard.substring(idCard.length - 4)}';
}
}
class SafeLogger {
static void log(String message) {
if (kReleaseMode) return;
debugPrint(message);
}
static Map<String, dynamic> _sanitizeBody(Map<String, dynamic> body) {
return body.map((key, value) {
if (['password', 'token', 'secret', 'creditCard'].contains(key)) {
return MapEntry(key, '***');
}
return MapEntry(key, value);
});
}
}
小结
| 安全领域 | 推荐方案 |
|---|---|
| 敏感数据存储 | flutter_secure_storage(Keychain/EncryptedSP) |
| 生物识别 | local_auth(指纹/面容) |
| 网络安全 | 证书锁定 + HTTPS 强制 |
| 代码保护 | --obfuscate + --split-debug-info |
| 设备安全 | Root/Jailbreak 检测 |
| 数据脱敏 | 手机号/邮箱/身份证脱敏 + 日志安全 |
10.5 CI/CD 自动化流水线
完善的 CI/CD 流水线是工程化成熟度的标志。Flutter 项目的流水线涵盖代码检查、测试、多环境构建、自动分发和发布等环节。
一、GitHub Actions 完整配置
yaml
# .github/workflows/flutter_ci.yml
name: Flutter CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
FLUTTER_VERSION: '3.19.6'
JAVA_VERSION: '17'
jobs:
lint:
name: 代码检查
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 安装 Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: 安装依赖
run: flutter pub get
- name: 代码格式检查
run: dart format --output=none --set-exit-if-changed .
- name: 静态分析
run: flutter analyze --fatal-warnings
- name: 检查过期依赖
run: flutter pub outdated
test:
name: 单元测试 & 覆盖率
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- run: flutter pub get
- name: 运行测试
run: flutter test --coverage --reporter=github
- name: 上传覆盖率到 Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage/lcov.info
build-android:
name: Android 构建
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- run: flutter pub get
- name: 配置签名
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/keystore.jks
cat > android/key.properties << EOF
storeFile=keystore.jks
storePassword=${{ secrets.KEYSTORE_PASSWORD }}
keyAlias=${{ secrets.KEY_ALIAS }}
keyPassword=${{ secrets.KEY_PASSWORD }}
EOF
- name: 构建 APK
run: |
flutter build apk --release \
--dart-define=API_BASE_URL=${{ secrets.DEV_API_URL }}
- name: 构建 AAB
run: |
flutter build appbundle --release \
--dart-define=API_BASE_URL=${{ secrets.PROD_API_URL }} \
--obfuscate \
--split-debug-info=build/debug-info
- name: 上传构建产物
uses: actions/upload-artifact@v4
with:
name: android-release
path: |
build/app/outputs/flutter-apk/app-release.apk
build/app/outputs/bundle/release/app-release.aab
retention-days: 7
build-ios:
name: iOS 构建
runs-on: macos-14
needs: test
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- run: flutter pub get
- name: 安装 Apple 证书
env:
CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE }}
run: |
CERT_PATH=$RUNNER_TEMP/certificate.p12
PP_PATH=$RUNNER_TEMP/profile.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
echo "$CERTIFICATE_BASE64" | base64 -d -o $CERT_PATH
echo "$PROVISIONING_PROFILE_BASE64" | base64 -d -o $PP_PATH
security create-keychain -p "" $KEYCHAIN_PATH
security default-keychain -s $KEYCHAIN_PATH
security unlock-keychain -p "" $KEYCHAIN_PATH
security import $CERT_PATH -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/
- name: 构建 IPA
run: |
flutter build ipa --release \
--obfuscate \
--split-debug-info=build/debug-info \
--export-options-plist=ios/ExportOptions.plist
- name: 上传 IPA
uses: actions/upload-artifact@v4
with:
name: ios-release
path: build/ios/ipa/*.ipa
retention-days: 7
distribute:
name: 分发到测试组
runs-on: ubuntu-latest
needs: [build-android, build-ios]
if: github.ref == 'refs/heads/develop'
steps:
- uses: actions/download-artifact@v4
- name: 分发 APK 到 Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_ANDROID_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
groups: testers
file: android-release/app-release.apk
releaseNotes: "分支: ${{ github.ref_name }}, 提交: ${{ github.sha }}"
二、Fastlane 自动化
ruby
# fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "发布到 TestFlight"
lane :beta do
setup_ci if ENV['CI']
increment_build_number(
build_number: ENV['GITHUB_RUN_NUMBER'] || latest_testflight_build_number + 1
)
sh("cd .. && flutter build ipa --release --dart-define=ENVIRONMENT=prod")
upload_to_testflight(
ipa: "build/ios/ipa/MyApp.ipa",
api_key_path: "fastlane/api_key.json",
skip_waiting_for_build_processing: true
)
slack(
message: "iOS Beta 已上传到 TestFlight 🚀",
slack_url: ENV['SLACK_WEBHOOK_URL']
)
end
desc "发布到 App Store"
lane :release do
upload_to_app_store(
force: true,
skip_screenshots: true,
submit_for_review: false
)
end
end
platform :android do
desc "发布到 Google Play Internal Testing"
lane :beta do
gradle(
task: "bundle",
build_type: "Release",
project_dir: "android"
)
upload_to_play_store(
track: "internal",
json_key: "fastlane/google_play_key.json",
aab: "build/app/outputs/bundle/release/app-release.aab"
)
end
end
三、版本管理
yaml
# pubspec.yaml
version: 1.5.2+28
# ↑ ↑
# 版本名 版本号(versionCode/CFBundleVersion)
bash
# 自动化版本号递增脚本 scripts/bump_version.sh
PUBSPEC="pubspec.yaml"
CURRENT=$(grep "^version:" $PUBSPEC | awk '{print $2}')
VERSION_NAME=$(echo $CURRENT | cut -d'+' -f1)
BUILD_NUM=$(echo $CURRENT | cut -d'+' -f2)
NEW_BUILD=$((BUILD_NUM + 1))
NEW_VERSION="$VERSION_NAME+$NEW_BUILD"
sed -i "s/^version: .*/version: $NEW_VERSION/" $PUBSPEC
echo "版本号已更新: $CURRENT → $NEW_VERSION"
小结
| 环节 | 工具 |
|---|---|
| 代码检查 | flutter analyze + dart format |
| 测试 | flutter test --coverage |
| Android 构建 | GitHub Actions + Gradle 签名 |
| iOS 构建 | GitHub Actions + Apple 证书自动化 |
| 测试分发 | Firebase App Distribution |
| 生产发布 | Fastlane → TestFlight / Google Play |
测试覆盖率目标
| 层 | 覆盖率目标 |
|---|---|
| 业务逻辑(ViewModel/Bloc) | ≥ 80% |
| 工具函数 | ≥ 90% |
| UI Widget | ≥ 60% |
| 集成测试(核心流程) | 登录/下单/支付 |
章节总结
| 知识点 | 必掌握程度 |
|---|---|
| 单元测试(test + mockito) | ⭐⭐⭐⭐⭐ |
| Widget 测试(flutter_test) | ⭐⭐⭐⭐⭐ |
| 集成测试 | ⭐⭐⭐ |
| APK/AAB 构建 + 签名 | ⭐⭐⭐⭐⭐ |
| 代码混淆(--obfuscate) | ⭐⭐⭐⭐ |
| flutter_secure_storage | ⭐⭐⭐⭐⭐ |
| GitHub Actions CI/CD | ⭐⭐⭐⭐ |
👉 下一章:第十一章------进阶与原理解析