10 Flutter 测试与发布

本章涵盖单元测试、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 ⭐⭐⭐⭐

👉 下一章:第十一章------进阶与原理解析

相关推荐
空中海2 小时前
12 Flutter 实战项目与最佳实践
flutter
里欧跑得慢12 小时前
Flutter 测试全攻略:从单元测试到集成测试的完整实践
前端·css·flutter·web
键盘鼓手苏苏16 小时前
Flutter 三方库 pip 的鸿蒙化适配指南 - 实现标准化的画中画(Picture-in-Picture)模式、支持视频悬浮窗与多任务并行交互
flutter·pip·harmonyos
左手厨刀右手茼蒿16 小时前
Flutter 组件 sheety_localization 的适配 鸿蒙Harmony 实战 - 驾驭在线协作式多语言管理、实现鸿蒙端动态词条下发与全球化敏捷发布方案
flutter·harmonyos·鸿蒙·openharmony·sheety_localization
见山是山-见水是水17 小时前
鸿蒙flutter第三方库适配 - 路由书签应用
flutter·华为·harmonyos
火柴就是我17 小时前
记录一些跨平台开发需要的鸿蒙知识
flutter·harmonyos
Tong Z17 小时前
Flutter中的三种通道
flutter
空中海18 小时前
2.3 组件复用与组合
flutter·dart