做当前App应用内更新的前提:用户可以在App更新的时候还可以使用软件,不能让他一直等待下载,关闭下载组件后,打开组件还能继续看到他的进度,并且用户锁屏或者短暂退出APP后需要保证任务不能总是挂掉,这里没有用到保活,而是需要用到生命周期去让他暂停和恢复,所以当前app还要有一个全局管理类保存任务id。UI图在最后
(插件文档说明可以通知栏显示,但是我用的这个版本在100%下完后总是failed,而不是complete,就不会很友好,后面我就删掉了,不会详细介绍,大体就是插件文档中配置文件配置后,还需要去授权通知栏权限)
为什么选择flutter_downloader而非ota_update插件 一开始我是选择后者ota的,这个插件用起来十分简单,只需要执行一个下载api就能获取进度和下载状态,但是有一点不好的就是从1%到2%会监听很多次,也就是print打印后会出现十几次1%的打印,这时候我是每4格就setState一次。
坑点:但是他的文档写到他的下载是保存在内部存储空间的,当时我去调用了好几个类似getApplicationDocumentsDirectory的api都无法访问到这个目录中下载的apk,并且没有任务id这种概念的,不能暂停和开始,也不存在这种api,只是单纯的一股劲下载完,所以退出路由后,你就完全不知道他的任务id并继续了,十分适合那种强制升级的UI界面; 因为我当前产品UI逻辑的问题,这个插件就无法做到下载完毕后用户继续安装,导致需要重复下载,对用户不友好,要我是用户,我直接发飙破口大骂了,所以后面看了几个帖子发现了flutter_downloader插件还不错的样子有1k多的点赞
flutter_downloader插件特点:每11%会监听一次进度,并且每一次暂停任务后再启动任务就会生成一个新的任务id这个比较重点,文档写了,并且有暂停,开始,根据id查询任务对象,比较全面,符合我的需求,并且做完后我也有个疑问点,在后台太久了,任务会被cancled,这种情况是无法被恢复的,这时候我新启动一个任务,他居然继承了上个任务的进度!!!比如上个任务是59退到后台然后被杀死了,这时候我逻辑进来判断cancled的话就删除这个任务开启下个任务进度就会是59!这我在文档没看到有相关介绍,难道是同名文件他会被继承进度???有懂的吗?
tip:这个插件在模拟器上好像没效果,我都是wifi真机测试的
有一些xml的文件,直接跟着官方文档来就行了:https://pub.dev/packages/flutter_downloader#android-integration
并且xml里面获取文件管理权限和安装权限还需要以下两者
tools:ignore="ScopedStorage" /> Flutter相关插件: package_info_plus: ^8.0.2 url_launcher: ^6.3.0 flutter_downloader: ^1.11.8 install_plugin: ^2.1.0 crypto: ^3.0.5 //package_info_plus:获取当前构建版本信息 //url_launcher:苹果跳转AppStore的插件 //flutter_downloader:下载文件的插件, //install_plugin:安装apk所需插件 //crypto:对apk进行哈希判断所需, UI相关的代码我就不会贴出来了 实现逻辑:package_info_plus这个插件可以用下面的fromPlatform获取当前包信息,然后我们的当前发布版本信息是保存在服务器上的json文件,我是通过访问这个json文件获取发布版本信息的,一般包含版本号,构建号,apk的哈希值(判断apk包完整性需要用到,一般打完包的同级目录下会有一个sha1后缀的文件,里面就是你这个apk包的哈希值,当然也可以去一些网站里面也可以帮你算出来这个哈希值) /// 获取版本号 Future PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo; } 当前包信息包含这些 ///版本信息 ///[appName]app名称 ///[packageName]包名 ///[version]版本信息 ///[buildNumber]构建名 PackageInfo packageInfo = PackageInfo(appName: '', packageName: '', version: '', buildNumber: ''); 下载所需的代码 我用的这个是flutter的3.2.3版本以后的生命周期新写法,比较直观的看出它存在哪些api吧,这里需要用到的是暂停和恢复。其实这里应该后面说的,因为要从下载那块说起。 @override void initState() { ///监听应用生命周期对下载任务进行暂停和恢复 appLifecycleListener = AppLifecycleListener( onStateChange: (AppLifecycleState state) {}, onResume: onResume, onInactive: () {}, onHide: () {}, onShow: () {}, onPause: onPause, onRestart: () {}, onDetach: () {}, ); ///检查下载状态 fetchDownloadStatus(); super.initState(); } /// 暂停时的回调 onPause() { if (SharedPreferencesUtil.getDownloadTaskId() != '') { FlutterDownloader.pause(taskId: SharedPreferencesUtil.getDownloadTaskId()); } } ///应用从暂停中恢复过来 onResume() async { if (SharedPreferencesUtil.getDownloadTaskId() != '') { ///任务被唤醒后会生成新的任务对象,任务id变化 String? taskId = await FlutterDownloader.resume(taskId: SharedPreferencesUtil.getDownloadTaskId()); SharedPreferencesUtil.setDownloadTaskId(taskId!); } } 偷个懒把所有代码贴出来算了,一步一步讲解太麻烦了,这个代码里面其实每行我都写了注释, 步骤: 1:授权他的文件管理权限Permission.manageExternalStorage.request();否则触发不了安装效果,文档是这么说的要外部存储空间的权限 2:启动下载任务,然后将返回的id存到全局中 3:初始化的时候已经启动了监听,registerDownloadTask方法就会监听到任务已经启动,此时这个监听的数据是从他的回调函数downloadCallback里面被send过来的,大体来说按照文档来的,有不会的可以参考我的写法 4:最后下载完之后我没有判断apk的完整性,因为如果不完整,在安装的时候系统自身就会提醒我们,我就没做了,因为会稍微有点卡顿 5:如果退到后台不暂停任务,就会导致任务canceled,这种状态不允许resume这个任务的,需要重新开始从0下载任务,如果想要他不被cancled,需要用到生命周期监听应用的状态去暂停和恢复,这样子就不会导致任务cancled,但是太久了也会让他挂掉的,这种暂停的方式还是挺简单的,并且如果组件被销毁了,那些端口一定要摧毁掉,不然会存在问题的。保活的话得知道原生咋做的,这个后面我得研究研究 6:下载完后用户不小心点击取消安装了,或者下载到一半退出App了,这时候外部存储空间都会留下一个apk了,所以这个时候哈希值的作用就出来了,专门判断apk是否完整的,如果是完整的,就直接安装它,如果不完整,我采取的逻辑是删除掉这个apk,并且让用户重新下载,我参考了很多主流App除了应用商店都是不会有继续下载的这种动作的,大部分都是后台静默下载或者强制下载完后打开,如果下载到一半退出了就重新下载。有不会的地方可以评论区问出来。 import 'dart:async'; import 'dart:io'; import 'dart:isolate'; import 'dart:ui'; import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:gl_business_platform_flutter/components/custom_bottom_sheet.dart'; import 'package:gl_business_platform_flutter/components/custom_button.dart'; import 'package:gl_business_platform_flutter/managers/settings_manager.dart'; import 'package:gl_business_platform_flutter/utils/notify_util.dart'; import 'package:gl_business_platform_flutter/utils/shared_preferences_util.dart'; import 'package:install_plugin/install_plugin.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:path_provider/path_provider.dart'; import '../../../styles/custom_style.dart'; import '../../../utils/request/request.dart'; import '../util/setting_util.dart'; /// 下载弹窗 /// [packageInfo]版本信息 /// [settingsManager]设置管理类 /// [isIosSystem]是否为ios系统 /// [appId]应用的appId class DownloadSheet extends StatefulWidget { ///[build_name]构建版本 ///[version]版本号 ///[name]软件名 final Map final SettingsManager settingsManager; final bool isIosSystem; final String appId; const DownloadSheet( {super.key, required this.packageInfo, required this.settingsManager, required this.isIosSystem, required this.appId}); @override State } class _DownloadSheetState extends State var progress = 0; ///进度条状态 var progressStatus = ProgressType.examineStatus; final ReceivePort _port = ReceivePort(); late final AppLifecycleListener appLifecycleListener; @override void initState() { ///监听应用生命周期对下载任务进行暂停和恢复 appLifecycleListener = AppLifecycleListener( onStateChange: (AppLifecycleState state) {}, onResume: onResume, onInactive: () {}, onHide: () {}, onShow: () {}, onPause: onPause, onRestart: () {}, onDetach: () {}, ); ///检查下载状态 fetchDownloadStatus(); super.initState(); } /// 暂停时的回调 onPause() { if (SharedPreferencesUtil.getDownloadTaskId() != '') { FlutterDownloader.pause(taskId: SharedPreferencesUtil.getDownloadTaskId()); } } ///应用从暂停中恢复过来 onResume() async { if (SharedPreferencesUtil.getDownloadTaskId() != '') { ///任务被唤醒后会生成新的任务对象,任务id变化 String? taskId = await FlutterDownloader.resume(taskId: SharedPreferencesUtil.getDownloadTaskId()); SharedPreferencesUtil.setDownloadTaskId(taskId!); } } ///下载任务的回调函数 @pragma('vm:entry-point') static void downloadCallback(String id, int status, int progress) { ///定义目标端口的对象 final SendPort? send = IsolateNameServer.lookupPortByName('downloader_send_port'); send?.send([id, status, progress]); } @override void dispose() { ///页面销毁后需要关闭端口,否则再次打开将无法正确监听 _port.close(); ///移除端口映射 IsolateNameServer.removePortNameMapping('downloader_send_port'); ///销毁生命周期监听 appLifecycleListener.dispose(); super.dispose(); } /// 检查是否已经存在下载任务,不存在则检查是否有安装,最后注册任务信息 Future String taskId = SharedPreferencesUtil.getDownloadTaskId(); List if (taskId != '') { ///这处sql中task_id在文档中是varchar类型,需要引号括起来,否则会将-符转义 taskInfo = await FlutterDownloader.loadTasksWithRawQuery(query: "SELECT * FROM task WHERE task_id='$taskId'"); } if (taskInfo != null && taskInfo.isNotEmpty && taskInfo[0].status.index == 2) { ///为了再次打开下拉抽屉时,查询下载任务的进度更新UI, final downLoadProgress = taskInfo[0].progress; // 获取下载进度 setState(() { progressStatus = ProgressType.downloadStatus; progress = downLoadProgress; }); } else { ///不存在任务,判断是否有安装包 SharedPreferencesUtil.setDownloadTaskId(''); installValidation(false); } registerDownloadTask(); } ///注册下载任务 void registerDownloadTask() { ///注册接收的节点 IsolateNameServer.registerPortWithName(_port.sendPort, 'downloader_send_port'); ///注册回调函数 FlutterDownloader.registerCallback(downloadCallback); ///当前对象节点的监听器 _port.listen((dynamic data) async { ///任务id:data[0],任务状态:data[1],下载进度:data[2] ///下载状态的枚举值 DownloadTaskStatus status = DownloadTaskStatus.values[data[1]]; ///下载进度 int downLoadProgress = data[2]; setState(() { if (status == DownloadTaskStatus.running) { progressStatus = ProgressType.downloadStatus; progress = downLoadProgress; } else if (status == DownloadTaskStatus.canceled) { cleanDownloadStatus("下载被中断"); } else if (status == DownloadTaskStatus.failed && progress != 100) { cleanDownloadStatus("下载过于频繁,请稍等"); } }); ///进度达到100%之后自动发起安装提示 if (downLoadProgress.toString() == "100" && status == DownloadTaskStatus.running) { installValidation(true); } }); } ///下载任务报错的时候清空按钮状态 void cleanDownloadStatus(String title) { progressStatus = ProgressType.prepareStatus; SharedPreferencesUtil.setDownloadTaskId(''); progress = 0; NotifyUtil.showToast(title); } ///安装包校验 ///[isDownloadComplete]是否下载完成 Future ///外部存储空间 final Directory externalDir = await getApplicationDocumentsDirectory(); String apkPath = "${externalDir.path}/${widget.settingsManager.apkName}"; ///如果是安装进度100%触发的,直接进行安装, if (isDownloadComplete) { ///暂不在此逻辑做安装包校验,先观察用户是否有网络波动导致下载包有缺失问题 await installEvent(apkPath); setState(() { progressStatus = ProgressType.installStatus; }); return; } File file = File(apkPath); ///如果本地不存在这个文件,用户需要重新下载 if (!file.existsSync()) { setState(() { progressStatus = ProgressType.prepareStatus; }); return; } ///这里方法计算哈希值应该计算量大,导致抽屉渲染卡顿,在此延迟执行 Timer(const Duration(milliseconds: 100), () => judgeApk(file)); } ///判断安装包是否完整 void judgeApk(File file) { computeSha1OfFile(file).then((sha1) { ///当前环境发行的安装包版本哈希值 String publishApkSha1 = widget.packageInfo['apk_sha1']; if (Request.getBaseUrl == "你的请求路径,这里是用来区分正式环境和测试环境的,看你自己") { publishApkSha1 = widget.packageInfo['apk_test_sha1']; } if (sha1 == publishApkSha1) { ///这里是初始化状态进来的时候,判断安装包符合预期,按钮显示继续安装 ///如果用户已经下载过,但是取消安装了后还需要重新下载,就是json文件中的哈希值不正确 setState(() { progressStatus = ProgressType.installStatus; }); } else { ///安装包不完整,直接删了,免得每次打开抽屉都要判断完整性, ///并且flutter_downloader检测到当前有已经存在同名apk,会报更新太快。 file.delete(); setState(() { progressStatus = ProgressType.prepareStatus; }); } }); } Future final res = await InstallPlugin.install(apkPath); if (res['isSuccess']) { NotifyUtil.showToast("安装成功"); } else if (res['errorMessage'] == "Install Cancel") { NotifyUtil.showToast("您取消了安装"); } } ///计算当前下载的apk的哈希值 Future final bytes = await file.readAsBytes(); final digest = sha1.convert(bytes); return digest.toString(); } ///安卓下载方法 void androidDownload() async { var status = await Permission.manageExternalStorage.request(); if (status == PermissionStatus.denied) { NotifyUtil.showToast('存储权限已被拒绝,请在设置中开启存储权限。'); return; } else if (status == PermissionStatus.permanentlyDenied) { NotifyUtil.showToast('存储权限被永久拒绝,请在设置中开启存储权限。'); return; } final Directory externalDir = await getApplicationDocumentsDirectory(); ///如果是按钮枚举状态为继续安装,直接进行安装 if (progressStatus == ProgressType.installStatus) { String apkPath = "${externalDir.path}/${widget.settingsManager.apkName}"; installEvent(apkPath); } else if (progressStatus == ProgressType.prepareStatus && SharedPreferencesUtil.getDownloadTaskId() == '') { setState(() { progressStatus = ProgressType.downloadStatus; }); ///开始下载任务 final taskId = await FlutterDownloader.enqueue( url: widget.settingsManager.androidDownloadUrl, headers: {}, savedDir: externalDir.path, fileName: widget.settingsManager.apkName, showNotification: true, openFileFromNotification: true, ); SharedPreferencesUtil.setDownloadTaskId(taskId.toString()); } } ///苹果下载方法 void iosDownload() async { final appStoreUrl = Uri.parse('https://apps.apple.com/app/id${widget.appId}'); if (await canLaunchUrl(appStoreUrl)) { await launchUrl(appStoreUrl); } else { NotifyUtil.showToast("AppStore打开应用失败。"); } } /// 底部按钮 Widget actionButtons(BuildContext context) { final buttonWidth = (MediaQuery.of(context).size.width - 48); String buttonTitle = progressStatus.description; if (progressStatus == ProgressType.downloadStatus) { buttonTitle = "${progressStatus.description}: $progress%"; } return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ CustomButton( width: buttonWidth, buttonText: widget.isIosSystem ? "前往AppStore更新" : buttonTitle, onPressed: () { widget.isIosSystem ? iosDownload() : androidDownload(); }) ], ); } @override Widget build(BuildContext context) { return CustomBottomSheet( backgroundColor: currentStyle.workBenchCardColor, height: 400, title: '版本更新', contentWidget: SingleChildScrollView( child: Container( color: currentStyle.workBenchCardColor, width: MediaQuery.of(context).size.width, padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ Image.asset('assets/images/update-version.png'), const SizedBox(height: 10), Text("发现新版本:", style: currentStyle.searchBarInputText), Text(widget.packageInfo['name'] + ' ' + widget.packageInfo['version'], style: currentStyle.searchBarInputText), Padding( padding: const EdgeInsets.symmetric(horizontal: 23, vertical: 34), child: Row( children: [ Expanded( child: Text( "主要更新\n 修复了一些已知问题", style: currentStyle.customerTypeText, )), ], )), ], ), ), ), buttonWidget: actionButtons(context), ); } } 这个放在另外的dart文件中,用于下载状态栏文字的枚举的 enum ProgressType { examineStatus(0, "检查本地安装包..."), prepareStatus(1, "立即更新"), downloadStatus(2, "下载中"), installStatus(3, "继续安装"); final int code; final String description; const ProgressType(this.code, this.description); } 在这里插入图片描述