rwadurian/frontend/mobile-app/flutter_android_update_guid...

36 KiB
Raw Blame History

Flutter Android APK 在线升级完整方案

支持 Google Play 和自建服务器双渠道的 Flutter Android 应用升级解决方案

目录


一、方案概述

1.1 升级渠道对比

渠道 适用场景 升级体验 审核要求
Google Play 海外市场、正规渠道 应用内更新,体验流畅 需要审核
自建服务器 国内市场、企业内部 下载 APK 安装 无需审核

1.2 核心特性

  • 支持 Google Play 应用内更新(灵活更新、强制更新)
  • 支持自建服务器 APK 下载安装
  • 使用应用专属目录,无需外部存储权限
  • SHA-256 文件完整性校验
  • 支持应用市场来源检测
  • 渠道构建层面完全隔离
  • 强制 HTTPS 下载

二、技术栈与依赖

2.1 Flutter 依赖

dependencies:
  # 版本信息
  package_info_plus: ^8.0.0
  
  # HTTP 请求与下载
  dio: ^5.4.0
  
  # Google Play 应用内更新
  in_app_update: ^4.2.2
  
  # 权限管理
  permission_handler: ^11.0.0
  
  # 路径获取
  path_provider: ^2.1.0
  
  # SHA-256 校验
  crypto: ^3.0.3
  
  # URL 启动
  url_launcher: ^6.2.0

2.2 Android 依赖

Google Play Core Library 会由 in_app_update 插件自动添加。


三、架构设计

3.1 目录结构

lib/
└── core/
    └── updater/
        ├── models/
        │   ├── version_info.dart              # 版本信息模型
        │   └── update_config.dart             # 更新配置
        ├── channels/
        │   ├── google_play_updater.dart       # Google Play 更新
        │   └── self_hosted_updater.dart       # 自建服务器更新
        ├── version_checker.dart               # 版本检测
        ├── download_manager.dart              # 下载管理
        ├── apk_installer.dart                 # APK 安装器
        ├── app_market_detector.dart           # 应用市场检测
        └── update_service.dart                # 升级服务统一入口

四、Google Play 应用内更新

4.1 Google Play 更新器实现

文件: lib/core/updater/channels/google_play_updater.dart

import 'package:in_app_update/in_app_update.dart';

enum GooglePlayUpdateType {
  flexible,    // 灵活更新:后台下载,可继续使用
  immediate,   // 强制更新:阻塞式,必须更新
}

class GooglePlayUpdater {
  /// 检查是否有更新可用
  static Future<bool> checkForUpdate() async {
    try {
      final updateInfo = await InAppUpdate.checkForUpdate();
      return updateInfo.updateAvailability == UpdateAvailability.updateAvailable;
    } catch (e) {
      print('Check update failed: $e');
      return false;
    }
  }

  /// 灵活更新
  /// 用户可以在后台下载,继续使用应用
  static Future<void> performFlexibleUpdate() async {
    try {
      final updateInfo = await InAppUpdate.checkForUpdate();
      
      if (updateInfo.updateAvailability == UpdateAvailability.updateAvailable) {
        if (updateInfo.flexibleUpdateAllowed) {
          await InAppUpdate.startFlexibleUpdate();
          
          InAppUpdate.completeFlexibleUpdate().then((_) {
            print('Update completed, app will restart');
          }).catchError((e) {
            print('Update failed: $e');
          });
        } else {
          print('Flexible update not allowed');
        }
      }
    } catch (e) {
      print('Flexible update error: $e');
    }
  }

  /// 强制更新
  /// 阻塞式更新,用户必须更新才能继续使用
  static Future<void> performImmediateUpdate() async {
    try {
      final updateInfo = await InAppUpdate.checkForUpdate();
      
      if (updateInfo.updateAvailability == UpdateAvailability.updateAvailable) {
        if (updateInfo.immediateUpdateAllowed) {
          await InAppUpdate.performImmediateUpdate();
        } else {
          print('Immediate update not allowed');
        }
      }
    } catch (e) {
      print('Immediate update error: $e');
    }
  }

  /// 智能更新策略
  /// 根据版本差异决定更新方式
  static Future<void> smartUpdate({
    required int currentVersion,
    required int latestVersion,
  }) async {
    final versionDiff = latestVersion - currentVersion;
    
    if (versionDiff >= 10) {
      // 版本差异大,强制更新
      await performImmediateUpdate();
    } else {
      // 版本差异小,灵活更新
      await performFlexibleUpdate();
    }
  }
}

4.2 使用示例

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    _checkGooglePlayUpdate();
  }

  Future<void> _checkGooglePlayUpdate() async {
    await Future.delayed(const Duration(seconds: 3));
    
    final hasUpdate = await GooglePlayUpdater.checkForUpdate();
    if (hasUpdate) {
      _showUpdateDialog();
    }
  }

  void _showUpdateDialog() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: const Text('发现新版本'),
        content: const Text('有新版本可用,是否立即更新?'),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              GooglePlayUpdater.performFlexibleUpdate();
            },
            child: const Text('更新'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('稍后'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(/* ... */);
  }
}

五、自建服务器 APK 升级

5.1 版本信息模型

文件: lib/core/updater/models/version_info.dart

import 'package:json_annotation/json_annotation.dart';

part 'version_info.g.dart';

@JsonSerializable()
class VersionInfo {
  final String version;          // 版本号: "1.2.0"
  final int versionCode;         // 版本代码: 120
  final String downloadUrl;      // APK 下载地址
  final int fileSize;            // 文件大小(字节)
  final String fileSizeFriendly; // 友好显示: "25.6 MB"
  final String sha256;           // SHA-256 校验
  final bool forceUpdate;        // 是否强制更新
  final String? updateLog;       // 更新日志
  final DateTime releaseDate;    // 发布时间

  VersionInfo({
    required this.version,
    required this.versionCode,
    required this.downloadUrl,
    required this.fileSize,
    required this.fileSizeFriendly,
    required this.sha256,
    this.forceUpdate = false,
    this.updateLog,
    required this.releaseDate,
  });

  factory VersionInfo.fromJson(Map<String, dynamic> json) =>
      _$VersionInfoFromJson(json);
  
  Map<String, dynamic> toJson() => _$VersionInfoToJson(this);
}

5.2 版本检测器

文件: lib/core/updater/version_checker.dart

import 'package:package_info_plus/package_info_plus.dart';
import 'package:dio/dio.dart';
import 'models/version_info.dart';

class VersionChecker {
  final String apiBaseUrl;
  final Dio _dio;

  VersionChecker({required this.apiBaseUrl})
      : _dio = Dio(BaseOptions(
          baseUrl: apiBaseUrl,
          connectTimeout: const Duration(seconds: 10),
        ));

  /// 获取当前版本信息
  Future<PackageInfo> getCurrentVersion() async {
    return await PackageInfo.fromPlatform();
  }

  /// 从服务器获取最新版本信息
  Future<VersionInfo?> fetchLatestVersion() async {
    try {
      final response = await _dio.get('/api/app/version/check');
      
      if (response.statusCode == 200) {
        return VersionInfo.fromJson(response.data);
      }
      return null;
    } catch (e) {
      print('Fetch version failed: $e');
      return null;
    }
  }

  /// 检查是否有新版本
  Future<VersionInfo?> checkForUpdate() async {
    try {
      final currentInfo = await getCurrentVersion();
      final latestInfo = await fetchLatestVersion();

      if (latestInfo == null) return null;

      final currentCode = int.parse(currentInfo.buildNumber);
      if (latestInfo.versionCode > currentCode) {
        return latestInfo;
      }

      return null;
    } catch (e) {
      print('Check update failed: $e');
      return null;
    }
  }

  /// 是否需要强制更新
  Future<bool> needForceUpdate() async {
    final latestInfo = await checkForUpdate();
    return latestInfo?.forceUpdate ?? false;
  }
}

5.3 下载管理器

文件: lib/core/updater/download_manager.dart

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert';

class DownloadManager {
  final Dio _dio = Dio();
  CancelToken? _cancelToken;

  /// 下载 APK 文件
  /// [url] 下载地址(必须是 HTTPS
  /// [sha256Expected] SHA-256 校验值
  /// [onProgress] 下载进度回调 (已下载字节, 总字节)
  Future<File?> downloadApk({
    required String url,
    required String sha256Expected,
    Function(int received, int total)? onProgress,
  }) async {
    try {
      // 强制 HTTPS
      if (!url.startsWith('https://')) {
        print('Download URL must use HTTPS');
        return null;
      }

      _cancelToken = CancelToken();

      // 使用应用专属目录(无需额外权限)
      final dir = await getApplicationDocumentsDirectory();
      final savePath = '${dir.path}/app_update.apk';
      final file = File(savePath);

      if (await file.exists()) {
        await file.delete();
      }

      print('Downloading APK to: $savePath');

      await _dio.download(
        url,
        savePath,
        cancelToken: _cancelToken,
        onReceiveProgress: (received, total) {
          if (total != -1) {
            final progress = (received / total * 100).toStringAsFixed(0);
            print('Download progress: $progress%');
            onProgress?.call(received, total);
          }
        },
        options: Options(
          receiveTimeout: const Duration(minutes: 10),
          sendTimeout: const Duration(minutes: 10),
        ),
      );

      print('Download completed');

      // 校验 SHA-256
      final isValid = await _verifySha256(file, sha256Expected);
      if (!isValid) {
        print('SHA-256 verification failed');
        await file.delete();
        return null;
      }

      print('SHA-256 verified');
      return file;
    } catch (e) {
      print('Download failed: $e');
      return null;
    }
  }

  /// 取消下载
  void cancelDownload() {
    _cancelToken?.cancel('User cancelled download');
  }

  /// 校验文件 SHA-256
  Future<bool> _verifySha256(File file, String expectedSha256) async {
    try {
      final bytes = await file.readAsBytes();
      final digest = sha256.convert(bytes);
      final actualSha256 = digest.toString();
      
      print('Expected SHA-256: $expectedSha256');
      print('Actual SHA-256:   $actualSha256');
      
      return actualSha256.toLowerCase() == expectedSha256.toLowerCase();
    } catch (e) {
      print('SHA-256 verification error: $e');
      return false;
    }
  }
}

5.4 APK 安装器

文件: lib/core/updater/apk_installer.dart

import 'dart:io';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';

class ApkInstaller {
  static const MethodChannel _channel = MethodChannel('com.yourapp/apk_installer');

  /// 安装 APK
  static Future<bool> installApk(File apkFile) async {
    try {
      if (!await apkFile.exists()) {
        print('APK file not found');
        return false;
      }

      // Android 8.0+ 需要请求安装权限
      if (Platform.isAndroid) {
        final hasPermission = await _requestInstallPermission();
        if (!hasPermission) {
          print('Install permission denied');
          return false;
        }
      }

      print('Installing APK: ${apkFile.path}');
      final result = await _channel.invokeMethod('installApk', {
        'apkPath': apkFile.path,
      });
      
      print('Installation triggered: $result');
      return true;
    } catch (e) {
      print('Install failed: $e');
      return false;
    }
  }

  /// 请求安装权限Android 8.0+
  static Future<bool> _requestInstallPermission() async {
    if (await Permission.requestInstallPackages.isGranted) {
      return true;
    }

    final status = await Permission.requestInstallPackages.request();
    return status.isGranted;
  }
}

5.5 应用市场检测器

文件: lib/core/updater/app_market_detector.dart

import 'dart:io';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';

class AppMarketDetector {
  static const MethodChannel _channel = MethodChannel('com.yourapp/app_market');

  /// 获取安装来源
  static Future<String?> getInstallerPackageName() async {
    if (!Platform.isAndroid) return null;

    try {
      final installer = await _channel.invokeMethod<String>('getInstallerPackageName');
      return installer;
    } catch (e) {
      print('Get installer failed: $e');
      return null;
    }
  }

  /// 判断是否来自应用市场
  static Future<bool> isFromAppMarket() async {
    final installer = await getInstallerPackageName();
    
    if (installer == null || installer.isEmpty) {
      return false; // 直接安装
    }

    // 常见应用市场包名
    const marketPackages = [
      'com.huawei.appmarket',
      'com.xiaomi.market',
      'com.oppo.market',
      'com.bbk.appstore',
      'com.tencent.android.qqdownloader',
      'com.qihoo.appstore',
      'com.baidu.appsearch',
      'com.wandoujia.phoenix2',
      'com.dragon.android.pandaspace',
      'com.sec.android.app.samsungapps',
    ];

    return marketPackages.contains(installer);
  }

  /// 打开应用市场详情页
  static Future<void> openAppMarketDetail(String packageName) async {
    final marketUri = Uri.parse('market://details?id=$packageName');

    if (await canLaunchUrl(marketUri)) {
      await launchUrl(marketUri, mode: LaunchMode.externalApplication);
    } else {
      final webUri = Uri.parse('https://play.google.com/store/apps/details?id=$packageName');
      await launchUrl(webUri, mode: LaunchMode.externalApplication);
    }
  }
}

5.6 自建服务器更新器

文件: lib/core/updater/channels/self_hosted_updater.dart

import 'dart:io';
import 'package:flutter/material.dart';
import '../version_checker.dart';
import '../download_manager.dart';
import '../apk_installer.dart';
import '../app_market_detector.dart';
import '../models/version_info.dart';

class SelfHostedUpdater {
  final VersionChecker versionChecker;
  final DownloadManager downloadManager;

  SelfHostedUpdater({
    required String apiBaseUrl,
  })  : versionChecker = VersionChecker(apiBaseUrl: apiBaseUrl),
        downloadManager = DownloadManager();

  /// 检查并提示更新
  Future<void> checkAndPromptUpdate(BuildContext context) async {
    final versionInfo = await versionChecker.checkForUpdate();
    
    if (versionInfo == null) {
      print('Already latest version');
      return;
    }

    if (!context.mounted) return;

    // 检测安装来源
    final isFromMarket = await AppMarketDetector.isFromAppMarket();
    
    if (isFromMarket) {
      _showMarketUpdateDialog(context, versionInfo);
    } else {
      _showSelfHostedUpdateDialog(context, versionInfo);
    }
  }

  /// 应用市场更新提示
  void _showMarketUpdateDialog(BuildContext context, VersionInfo versionInfo) {
    showDialog(
      context: context,
      barrierDismissible: !versionInfo.forceUpdate,
      builder: (context) => WillPopScope(
        onWillPop: () async => !versionInfo.forceUpdate,
        child: AlertDialog(
          title: Text(versionInfo.forceUpdate ? '发现重要更新' : '发现新版本'),
          content: SingleChildScrollView(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('最新版本: ${versionInfo.version}'),
                if (versionInfo.updateLog != null) ...[
                  const SizedBox(height: 16),
                  const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
                  const SizedBox(height: 8),
                  Text(versionInfo.updateLog!),
                ],
                const SizedBox(height: 16),
                const Text(
                  '检测到您的应用来自应用市场,建议前往应用市场更新以获得最佳体验。',
                  style: TextStyle(fontSize: 12, color: Colors.grey),
                ),
              ],
            ),
          ),
          actions: [
            if (!versionInfo.forceUpdate)
              TextButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('稍后'),
              ),
            ElevatedButton(
              onPressed: () async {
                Navigator.pop(context);
                final packageInfo = await versionChecker.getCurrentVersion();
                await AppMarketDetector.openAppMarketDetail(packageInfo.packageName);
              },
              child: const Text('前往应用市场'),
            ),
          ],
        ),
      ),
    );
  }

  /// 自建更新对话框
  void _showSelfHostedUpdateDialog(BuildContext context, VersionInfo versionInfo) {
    showDialog(
      context: context,
      barrierDismissible: !versionInfo.forceUpdate,
      builder: (context) => WillPopScope(
        onWillPop: () async => !versionInfo.forceUpdate,
        child: AlertDialog(
          title: Text(versionInfo.forceUpdate ? '发现重要更新' : '发现新版本'),
          content: SingleChildScrollView(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('最新版本: ${versionInfo.version}'),
                const SizedBox(height: 8),
                Text('文件大小: ${versionInfo.fileSizeFriendly}'),
                if (versionInfo.updateLog != null) ...[
                  const SizedBox(height: 16),
                  const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
                  const SizedBox(height: 8),
                  Text(versionInfo.updateLog!),
                ],
              ],
            ),
          ),
          actions: [
            if (!versionInfo.forceUpdate)
              TextButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('稍后'),
              ),
            ElevatedButton(
              onPressed: () {
                Navigator.pop(context);
                _startUpdate(context, versionInfo);
              },
              child: const Text('立即更新'),
            ),
          ],
        ),
      ),
    );
  }

  /// 开始下载并安装
  Future<void> _startUpdate(BuildContext context, VersionInfo versionInfo) async {
    if (!context.mounted) return;

    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => _DownloadProgressDialog(
        versionInfo: versionInfo,
        downloadManager: downloadManager,
      ),
    );
  }
}

/// 下载进度对话框
class _DownloadProgressDialog extends StatefulWidget {
  final VersionInfo versionInfo;
  final DownloadManager downloadManager;

  const _DownloadProgressDialog({
    required this.versionInfo,
    required this.downloadManager,
  });

  @override
  State<_DownloadProgressDialog> createState() => _DownloadProgressDialogState();
}

class _DownloadProgressDialogState extends State<_DownloadProgressDialog> {
  double _progress = 0.0;
  String _statusText = '准备下载...';
  bool _isDownloading = true;

  @override
  void initState() {
    super.initState();
    _startDownload();
  }

  Future<void> _startDownload() async {
    setState(() {
      _statusText = '正在下载...';
    });

    final apkFile = await widget.downloadManager.downloadApk(
      url: widget.versionInfo.downloadUrl,
      sha256Expected: widget.versionInfo.sha256,
      onProgress: (received, total) {
        setState(() {
          _progress = received / total;
          final receivedMB = (received / 1024 / 1024).toStringAsFixed(1);
          final totalMB = (total / 1024 / 1024).toStringAsFixed(1);
          _statusText = '正在下载... $receivedMB MB / $totalMB MB';
        });
      },
    );

    if (apkFile == null) {
      setState(() {
        _statusText = '下载失败';
        _isDownloading = false;
      });
      return;
    }

    setState(() {
      _statusText = '下载完成,准备安装...';
    });

    await Future.delayed(const Duration(milliseconds: 500));

    final installed = await ApkInstaller.installApk(apkFile);
    
    if (!mounted) return;

    if (installed) {
      Navigator.pop(context);
    } else {
      setState(() {
        _statusText = '安装失败';
        _isDownloading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async => !_isDownloading,
      child: AlertDialog(
        title: const Text('正在更新'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            LinearProgressIndicator(value: _progress),
            const SizedBox(height: 16),
            Text(_statusText),
          ],
        ),
        actions: [
          if (!_isDownloading)
            TextButton(
              onPressed: () {
                widget.downloadManager.cancelDownload();
                Navigator.pop(context);
              },
              child: const Text('关闭'),
            ),
        ],
      ),
    );
  }
}

六、统一升级服务

6.1 升级服务入口

文件: lib/core/updater/update_service.dart

import 'package:flutter/material.dart';
import 'channels/google_play_updater.dart';
import 'channels/self_hosted_updater.dart';

enum UpdateChannel {
  googlePlay,
  selfHosted,
}

class UpdateService {
  static UpdateService? _instance;
  UpdateService._();
  
  factory UpdateService() {
    _instance ??= UpdateService._();
    return _instance!;
  }

  late UpdateChannel _channel;
  late String _apiBaseUrl;
  SelfHostedUpdater? _selfHostedUpdater;

  /// 初始化
  void initialize({
    required UpdateChannel channel,
    String? apiBaseUrl,
  }) {
    _channel = channel;
    
    if (channel == UpdateChannel.selfHosted) {
      if (apiBaseUrl == null) {
        throw ArgumentError('apiBaseUrl is required for self-hosted channel');
      }
      _apiBaseUrl = apiBaseUrl;
      _selfHostedUpdater = SelfHostedUpdater(apiBaseUrl: apiBaseUrl);
    }

    print('UpdateService initialized with channel: $_channel');
  }

  /// 检查更新
  Future<void> checkForUpdate(BuildContext context) async {
    switch (_channel) {
      case UpdateChannel.googlePlay:
        await _checkGooglePlayUpdate(context);
        break;
      case UpdateChannel.selfHosted:
        await _selfHostedUpdater!.checkAndPromptUpdate(context);
        break;
    }
  }

  Future<void> _checkGooglePlayUpdate(BuildContext context) async {
    final hasUpdate = await GooglePlayUpdater.checkForUpdate();
    
    if (!hasUpdate) {
      print('No update available');
      return;
    }

    if (!context.mounted) return;

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('发现新版本'),
        content: const Text('有新版本可用,是否立即更新?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('稍后'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              GooglePlayUpdater.performFlexibleUpdate();
            },
            child: const Text('更新'),
          ),
        ],
      ),
    );
  }

  /// 手动检查更新
  Future<void> manualCheckUpdate(BuildContext context) async {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => const Center(
        child: CircularProgressIndicator(),
      ),
    );

    await Future.delayed(const Duration(seconds: 1));
    
    if (!context.mounted) return;
    Navigator.pop(context);

    await checkForUpdate(context);
  }
}

6.2 使用示例

在 main.dart 中初始化:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  UpdateService().initialize(
    channel: UpdateChannel.selfHosted,
    apiBaseUrl: 'https://your-api.com',
  );
  
  runApp(const MyApp());
}

在 App 启动时检查更新:

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    _checkUpdate();
  }

  Future<void> _checkUpdate() async {
    await Future.delayed(const Duration(seconds: 3));
    
    if (!mounted) return;
    
    await UpdateService().checkForUpdate(context);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(/* ... */);
  }
}

七、后端接口设计

7.1 版本检测接口

GET /api/app/version/check

Query Parameters:
  - platform: android | ios
  - current_version: 1.1.0
  - current_version_code: 110

Response:
{
  "version": "1.2.0",
  "versionCode": 120,
  "downloadUrl": "https://your-cdn.com/app-release-v1.2.0.apk",
  "fileSize": 26843545,
  "fileSizeFriendly": "25.6 MB",
  "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "forceUpdate": false,
  "updateLog": "1. 修复若干已知问题\n2. 优化用户体验",
  "releaseDate": "2024-01-15T10:00:00Z"
}

7.2 后端实现示例

Node.js + Express:

app.get('/api/app/version/check', async (req, res) => {
  const { platform, current_version_code } = req.query;
  
  const latestVersion = await db.getLatestVersion(platform);
  
  if (parseInt(current_version_code) >= latestVersion.versionCode) {
    return res.json({ needUpdate: false });
  }
  
  res.json({
    needUpdate: true,
    version: latestVersion.version,
    versionCode: latestVersion.versionCode,
    downloadUrl: `https://cdn.example.com/${latestVersion.apkFile}`,
    fileSize: latestVersion.fileSize,
    fileSizeFriendly: formatFileSize(latestVersion.fileSize),
    sha256: latestVersion.sha256,
    forceUpdate: latestVersion.forceUpdate,
    updateLog: latestVersion.updateLog,
    releaseDate: latestVersion.releaseDate,
  });
});

7.3 数据库表设计

CREATE TABLE app_versions (
  id BIGSERIAL PRIMARY KEY,
  platform VARCHAR(10) NOT NULL,
  version VARCHAR(50) NOT NULL,
  version_code INTEGER NOT NULL,
  apk_file VARCHAR(255),
  download_url TEXT NOT NULL,
  file_size BIGINT NOT NULL,
  sha256 VARCHAR(64) NOT NULL,
  force_update BOOLEAN DEFAULT FALSE,
  update_log TEXT,
  release_date TIMESTAMP NOT NULL,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMP DEFAULT NOW(),
  
  INDEX idx_platform (platform),
  INDEX idx_version_code (version_code),
  INDEX idx_active (is_active)
);

八、Android 原生配置

8.1 MainActivity 实现

文件: android/app/src/main/kotlin/com/yourapp/MainActivity.kt

package com.yourapp

import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.core.content.FileProvider
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File

class MainActivity: FlutterActivity() {
    private val INSTALLER_CHANNEL = "com.yourapp/apk_installer"
    private val MARKET_CHANNEL = "com.yourapp/app_market"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        
        // APK 安装器
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "installApk" -> {
                        val apkPath = call.argument<String>("apkPath")
                        if (apkPath != null) {
                            installApk(apkPath)
                            result.success(true)
                        } else {
                            result.error("INVALID_PATH", "APK path is null", null)
                        }
                    }
                    else -> result.notImplemented()
                }
            }
        
        // 应用市场检测
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, MARKET_CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "getInstallerPackageName" -> {
                        try {
                            val installer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                                packageManager.getInstallSourceInfo(packageName).installingPackageName
                            } else {
                                @Suppress("DEPRECATION")
                                packageManager.getInstallerPackageName(packageName)
                            }
                            result.success(installer)
                        } catch (e: Exception) {
                            result.success(null)
                        }
                    }
                    else -> result.notImplemented()
                }
            }
    }

    private fun installApk(apkPath: String) {
        val apkFile = File(apkPath)
        if (!apkFile.exists()) return

        val intent = Intent(Intent.ACTION_VIEW)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

        val apkUri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
            FileProvider.getUriForFile(
                this,
                "${applicationContext.packageName}.fileprovider",
                apkFile
            )
        } else {
            Uri.fromFile(apkFile)
        }

        intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
        startActivity(intent)
    }
}

8.2 Gradle Flavor 配置

文件: android/app/build.gradle

android {
    // 其他配置...

    flavorDimensions "channel"
    
    productFlavors {
        googleplay {
            dimension "channel"
            applicationIdSuffix ".googleplay"
            versionNameSuffix "-gp"
        }
        
        china {
            dimension "channel"
            versionNameSuffix "-china"
        }
    }
}

8.3 Google Play 版本 Manifest

文件: android/app/src/googleplay/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    
    <application>
        <!-- Google Play 版本不需要 FileProvider -->
    </application>
</manifest>

8.4 自建渠道 Manifest

文件: android/app/src/china/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    
    <application>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>
</manifest>

8.5 FileProvider 路径配置

文件: android/app/src/main/res/xml/file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="internal_files" path="." />
    <cache-path name="cache" path="." />
</paths>

8.6 构建命令

# Google Play 版本
flutter build apk --flavor googleplay --dart-define=CHANNEL=googleplay

# 国内版本
flutter build apk --flavor china --dart-define=CHANNEL=china

九、安全性考虑

9.1 HTTPS 强制

所有下载链接必须使用 HTTPS 协议:

if (!downloadUrl.startsWith('https://')) {
  throw Exception('Download URL must use HTTPS');
}

9.2 SHA-256 完整性校验

下载完成后必须校验文件完整性:

final isValid = await _verifySha256(apkFile, expectedSha256);
if (!isValid) {
  await apkFile.delete();
  throw Exception('SHA-256 verification failed');
}

9.3 渠道隔离

  • Google Play 版本:不包含任何安装相关权限和代码
  • 自建渠道版本:包含完整的下载安装功能
  • 使用 Gradle Flavor 在构建层面完全隔离

9.4 应用市场策略

检测应用来源,避免与应用市场升级机制冲突:

final isFromMarket = await AppMarketDetector.isFromAppMarket();
if (isFromMarket) {
  // 引导用户去应用市场更新
  AppMarketDetector.openAppMarketDetail(packageName);
} else {
  // 使用自建更新
  selfHostedUpdate();
}

十、实施步骤

Step 1: 添加依赖

flutter pub add package_info_plus dio in_app_update permission_handler path_provider crypto url_launcher

Step 2: 创建目录结构

mkdir -p lib/core/updater/{models,channels}

Step 3: 创建 Flutter 代码

按照本文档创建以下文件:

  • models/version_info.dart
  • version_checker.dart
  • download_manager.dart
  • apk_installer.dart
  • app_market_detector.dart
  • channels/google_play_updater.dart
  • channels/self_hosted_updater.dart
  • update_service.dart

Step 4: 生成 JSON 序列化代码

flutter pub run build_runner build --delete-conflicting-outputs

Step 5: 配置 Android 原生代码

  • 修改 MainActivity.kt
  • 配置 build.gradle
  • 创建 Flavor 专属 AndroidManifest.xml
  • 添加 file_paths.xml

Step 6: 实现后端 API

实现版本检测接口,返回最新版本信息。

Step 7: 初始化服务

在 main.dart 中初始化 UpdateService。

Step 8: 构建测试

# Google Play 版本
flutter build apk --flavor googleplay --dart-define=CHANNEL=googleplay --release

# 国内版本
flutter build apk --flavor china --dart-define=CHANNEL=china --release

Step 9: 验证权限

# 检查 Google Play 版本(不应有安装权限)
aapt dump permissions build/app/outputs/flutter-apk/app-googleplay-release.apk | grep INSTALL

# 检查国内版本(应有安装权限)
aapt dump permissions build/app/outputs/flutter-apk/app-china-release.apk | grep INSTALL

Step 10: 测试验证

  • 测试下载流程
  • 测试应用市场检测
  • 测试强制更新
  • 测试断网场景

附录

A. 常见问题

问题 解决方案
下载失败 检查网络、确保 HTTPS、添加重试机制
无法安装 确认 china flavor 有安装权限
SHA-256 校验失败 重新下载、检查网络稳定性
Google Play 更新不显示 确保 versionCode 递增

B. 调试命令

# 查看 APK 权限
aapt dump permissions app-release.apk

# 查看 APK 包名
aapt dump badging app-release.apk | grep package

# 生成 SHA-256
sha256sum app-release.apk

C. 参考资源


文档版本: 2.0
最后更新: 2024-01-15