在移动应用开发中,定期为用户提供更新以修复 bug 或添加新功能是非常重要的。而在用户体验上,应用内更新无疑能让用户在不离开应用的情况下完成更新操作,从而提升用户的留存率。本文将详细介绍如何在 Flutter 中实现应用内更新的功能。
更新时机
通过网络请求获取服务器的配置信息并与当前应用的版本号进行比较。如果发现新版本,提示用户更新;否则可显示提示信息。
/// 检查更新
static Future<void> checkUpdate({bool showMsg = false}) {
var currentVersion = await _getCurrentAppVersion();
if (basicConfiguration!.currentVersion! ==
basicConfiguration!.auditVersion! &&
_compareVersion(currentVersion, basicConfiguration!.currentVersion!)) {
// 显示更新对话框
...
} else {
if (showMsg) {
Fluttertoast.showToast(
msg: '当前版本已是最新版本,无需更新', gravity: ToastGravity.CENTER);
}
}
}
我们可以通过 PackageInfo 库来获取当前应用的版本号。
// 获取当前应用版本号
static Future<String> _getCurrentAppVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo.version;
}
将本地版本号与服务器返回的版本号进行对比,确保格式一致后判断是否需要更新。
以下代码片段均为更新管理类的成员方法(为篇幅省略了类声明与 import,
static方法需放在类体内使用)。
// 比较版本号
static bool _compareVersion(String localVersion, String serverVersion) {
List<String> localVersionSplit = localVersion.split('.');
List<String> serverVersionSplit = serverVersion.split('.');
// 取较长的段数,缺的段按 0 处理(如 "1.2" vs "1.2.1")
final len = localVersionSplit.length > serverVersionSplit.length
? localVersionSplit.length
: serverVersionSplit.length;
for (int i = 0; i < len; i++) {
int localPart =
i < localVersionSplit.length ? int.parse(localVersionSplit[i]) : 0;
int serverPart =
i < serverVersionSplit.length ? int.parse(serverVersionSplit[i]) : 0;
if (localPart < serverPart) {
return true; // 需要更新
} else if (localPart > serverPart) {
return false; // 不需要更新
}
}
return false; // 版本相同,不需要更新
}
版本更新
跟新的话还是离不开一个存储权限的问题,更新流程一般是,到目标位置,检查权限,然后做个app下载,打开文件进行更新,当然ios的话就只能跳转apptore了
请求存储权限
在执行下载之前,安卓设备需要请求存储权限,以便保存更新文件。
// 请求存储权限
Future<bool> _requestStoragePermission() async {
var status = await Permission.storage.status;
if (!status.isGranted) {
status = await Permission.storage.request();
}
return status.isGranted;
}
下载文件
对于 Android 设备,我们会下载 APK 并安装;对于 iOS 设备,更新只能跳转到 App Store。
开始下载文件之前我们需要去对这个设备进行一个区分,然后编写下载进度条动画
/// 开始下载
void _startDownloadAndInstall(String apkUrl) async {
if (Platform.isAndroid) {
// 安卓设备:下载并安装 APK
_startAndroidDownloadAndInstall(apkUrl);
} else if (Platform.isIOS) {
// iOS设备:跳转到 App Store
_openAppStore();
}
}
// 下载 APK 文件
Future<String?> _downloadApk(String url) async {
Dio dio = Dio();
Directory? appDocDir = await getTemporaryDirectory();
String savePath = "${appDocDir.path}/update.apk";
try {
await dio.download(url, savePath, onReceiveProgress: (received, total) {
setState(() {
_downloadProgress = received / total;
});
});
_installApk(savePath);
return savePath;
} catch (e) {
print("Download failed: $e");
return null;
}
}
apk安装
安装应用的话主要就是针对android系统,这边推荐使用的是这个OpenFile 去对这个apk文件进行打开并安装
https://pub.dev/packages/open_file
实现就比较简单直接将filePath 传递进入就可以了,需要注意AndroidManifest 有没有配置所需权限,可能据返回的OpenResult 进行调整
注:原 open_file 包已停更,社区普遍改用 open_filex。但 open_filex 为通过 Google Play 审核去掉了 REQUEST_INSTALL_PACKAGES 权限,企业内分发自装 APK 的场景要在 AndroidManifest 里自行声明该权限。
/// OpenFile.open 参数速查(插件 API 签名示意,非可运行代码):
/// open(String? filePath, {String? type, bool isIOSAppOpen = false,
/// String linuxDesktopName = "xdg", bool linuxUseGio = false,
/// bool linuxByProcess = true}) -> Future<OpenResult>
iOS 更新
在 iOS 系统中,无法直接通过应用安装更新,只能跳转至 App Store 下载最新版本。
/// 常量
class Constants {
// ... 其余常量省略
// 把 YOUR_APP_ID 换成 App Store Connect 里的数字 ID
static const appStoreUrl = "https://apps.apple.com/app/idYOUR_APP_ID";
// ... 其余常量省略
}
///打开AppStore
void _openAppStore() async {
final uri = Uri.parse(Constants.appStoreUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
print("无法打开 App Store 链接");
}
}
实战案例
应用内更新我还没在项目里完整做过,下面是按 upgrader、in_app_update 等插件文档提炼的关键点。先说路线选择,这是最大的 trade-off:走应用商店分发的话,直接用 upgrader 这类插件做”版本检查 + 跳商店”最省事,它本质就是个提醒壳;只有企业内分发、测试包分发这种不上商店的场景,才值得自己写”下载 APK + 调安装器”这套,正文走的就是后者。自装 APK 有三个硬门槛:Android 8+ 安装未知应用要 REQUEST_INSTALL_PACKAGES 权限,并引导用户打开”允许安装未知应用”;Android 7+ 分享 APK 文件要用 FileProvider,直接传 file:// 会崩;下载放 getTemporaryDirectory,别放外部存储,省得跟分区存储打架。iOS 没有第二条路,只能跳 App Store,这点正文写对了。
参考:upgrader、in_app_update 插件文档
