1389 lines
37 KiB
Markdown
1389 lines
37 KiB
Markdown
# Flutter Android APK 在线升级完整方案
|
||
|
||
> 支持 Google Play 和自建服务器双渠道的 Flutter Android 应用升级解决方案
|
||
|
||
## 目录
|
||
|
||
- [一、方案概述](#一方案概述)
|
||
- [二、技术栈与依赖](#二技术栈与依赖)
|
||
- [三、架构设计](#三架构设计)
|
||
- [四、Google Play 应用内更新](#四google-play-应用内更新)
|
||
- [五、自建服务器 APK 升级](#五自建服务器-apk-升级)
|
||
- [六、统一升级服务](#六统一升级服务)
|
||
- [七、后端接口设计](#七后端接口设计)
|
||
- [八、Android 原生配置](#八android-原生配置)
|
||
- [九、安全性考虑](#九安全性考虑)
|
||
- [十、实施步骤](#十实施步骤)
|
||
|
||
---
|
||
|
||
## 一、方案概述
|
||
|
||
### 1.1 升级渠道对比
|
||
|
||
| 渠道 | 适用场景 | 升级体验 | 审核要求 |
|
||
|------|---------|---------|---------|
|
||
| Google Play | 海外市场、正规渠道 | 应用内更新,体验流畅 | 需要审核 |
|
||
| 自建服务器 | 国内市场、企业内部 | 下载 APK 安装 | 无需审核 |
|
||
|
||
### 1.2 核心特性
|
||
|
||
- 支持 Google Play 应用内更新(灵活更新、强制更新)
|
||
- 支持自建服务器 APK 下载安装
|
||
- 使用应用专属目录,无需外部存储权限
|
||
- SHA-256 文件完整性校验
|
||
- 支持应用市场来源检测
|
||
- 渠道构建层面完全隔离
|
||
- 强制 HTTPS 下载
|
||
|
||
---
|
||
|
||
## 二、技术栈与依赖
|
||
|
||
### 2.1 Flutter 依赖
|
||
|
||
```yaml
|
||
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`**
|
||
|
||
```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 使用示例
|
||
|
||
```dart
|
||
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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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 中初始化:**
|
||
|
||
```dart
|
||
void main() async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
|
||
UpdateService().initialize(
|
||
channel: UpdateChannel.selfHosted,
|
||
apiBaseUrl: 'https://your-api.com',
|
||
);
|
||
|
||
runApp(const MyApp());
|
||
}
|
||
```
|
||
|
||
**在 App 启动时检查更新:**
|
||
|
||
```dart
|
||
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:
|
||
|
||
```javascript
|
||
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 数据库表设计
|
||
|
||
```sql
|
||
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`**
|
||
|
||
```kotlin
|
||
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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<paths>
|
||
<files-path name="internal_files" path="." />
|
||
<cache-path name="cache" path="." />
|
||
</paths>
|
||
```
|
||
|
||
### 8.6 构建命令
|
||
|
||
```bash
|
||
# Google Play 版本
|
||
flutter build apk --flavor googleplay --dart-define=CHANNEL=googleplay
|
||
|
||
# 国内版本
|
||
flutter build apk --flavor china --dart-define=CHANNEL=china
|
||
```
|
||
|
||
---
|
||
|
||
## 九、安全性考虑
|
||
|
||
### 9.1 HTTPS 强制
|
||
|
||
所有下载链接必须使用 HTTPS 协议:
|
||
|
||
```dart
|
||
if (!downloadUrl.startsWith('https://')) {
|
||
throw Exception('Download URL must use HTTPS');
|
||
}
|
||
```
|
||
|
||
### 9.2 SHA-256 完整性校验
|
||
|
||
下载完成后必须校验文件完整性:
|
||
|
||
```dart
|
||
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 应用市场策略
|
||
|
||
检测应用来源,避免与应用市场升级机制冲突:
|
||
|
||
```dart
|
||
final isFromMarket = await AppMarketDetector.isFromAppMarket();
|
||
if (isFromMarket) {
|
||
// 引导用户去应用市场更新
|
||
AppMarketDetector.openAppMarketDetail(packageName);
|
||
} else {
|
||
// 使用自建更新
|
||
selfHostedUpdate();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 十、实施步骤
|
||
|
||
### Step 1: 添加依赖
|
||
|
||
```bash
|
||
flutter pub add package_info_plus dio in_app_update permission_handler path_provider crypto url_launcher
|
||
```
|
||
|
||
### Step 2: 创建目录结构
|
||
|
||
```bash
|
||
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 序列化代码
|
||
|
||
```bash
|
||
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: 构建测试
|
||
|
||
```bash
|
||
# Google Play 版本
|
||
flutter build apk --flavor googleplay --dart-define=CHANNEL=googleplay --release
|
||
|
||
# 国内版本
|
||
flutter build apk --flavor china --dart-define=CHANNEL=china --release
|
||
```
|
||
|
||
### Step 9: 验证权限
|
||
|
||
```bash
|
||
# 检查 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. 调试命令
|
||
|
||
```bash
|
||
# 查看 APK 权限
|
||
aapt dump permissions app-release.apk
|
||
|
||
# 查看 APK 包名
|
||
aapt dump badging app-release.apk | grep package
|
||
|
||
# 生成 SHA-256
|
||
sha256sum app-release.apk
|
||
```
|
||
|
||
### C. 参考资源
|
||
|
||
- [Flutter 官方文档](https://flutter.dev/docs)
|
||
- [in_app_update 插件](https://pub.dev/packages/in_app_update)
|
||
- [Android FileProvider 指南](https://developer.android.com/reference/androidx/core/content/FileProvider)
|
||
|
||
---
|
||
|
||
文档版本: 2.0
|
||
最后更新: 2024-01-15
|