37 KiB
Flutter Android APK 在线升级完整方案
支持 Google Play 和自建服务器双渠道的 Flutter Android 应用升级解决方案
目录
- 一、方案概述
- 二、技术栈与依赖
- 三、架构设计
- 四、Google Play 应用内更新
- 五、自建服务器 APK 升级
- 六、统一升级服务
- 七、后端接口设计
- 八、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