Flutter应用内更新App版本使用flutter_downloader而非ota_update,很全面的 ...

打印 上一主题 下一主题

主题 1006|帖子 1006|积分 3018

做当前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里面获取文件管理权限和安装权限还需要以下两者
  1.     <!--请求下载软件包权限-->
  2.     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
  3.     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
  4.         tools:ignore="ScopedStorage" />
复制代码
Flutter相干插件:

  1.   package_info_plus: ^8.0.2
  2.   url_launcher: ^6.3.0
  3.   flutter_downloader: ^1.11.8
  4.   install_plugin: ^2.1.0
  5.   crypto: ^3.0.5
  6.   
  7. //package_info_plus:获取当前构建版本信息
  8. //url_launcher:苹果跳转AppStore的插件
  9. //flutter_downloader:下载文件的插件,
  10. //install_plugin:安装apk所需插件
  11. //crypto:对apk进行哈希判断所需,
复制代码
UI相干的代码我就不会贴出来了
实现逻辑:package_info_plus这个插件可以用下面的fromPlatform获取当前包信息,然后我们的当前发布版本信息是保存在服务器上的json文件,我是通过访问这个json文件获取发布版本信息的,一般包含版本号,构建号,apk的哈希值(判定apk包完整性需要用到,一般打完包的同级目录下会有一个sha1后缀的文件,里面就是你这个apk包的哈希值,当然也可以去一些网站里面也可以帮你算出来这个哈希值)

  1.   /// 获取版本号
  2.   Future<PackageInfo> getVersion() async {
  3.     PackageInfo packageInfo = await PackageInfo.fromPlatform();
  4.     return packageInfo;
  5.   }
复制代码
当前包信息包含这些
  1.   ///版本信息
  2.   ///[appName]app名称
  3.   ///[packageName]包名
  4.   ///[version]版本信息
  5.   ///[buildNumber]构建名
  6.   PackageInfo packageInfo = PackageInfo(appName: '', packageName: '', version: '', buildNumber: '');
复制代码
下载所需的代码

我用的这个是flutter的3.2.3版本以后的生命周期新写法,比较直观的看出它存在哪些api吧,这里需要用到的是暂停和规复。着实这里应该后面说的,因为要从下载那块说起。
  1.   @override
  2.   void initState() {
  3.     ///监听应用生命周期对下载任务进行暂停和恢复
  4.     appLifecycleListener = AppLifecycleListener(
  5.       onStateChange: (AppLifecycleState state) {},
  6.       onResume: onResume,
  7.       onInactive: () {},
  8.       onHide: () {},
  9.       onShow: () {},
  10.       onPause: onPause,
  11.       onRestart: () {},
  12.       onDetach: () {},
  13.     );
  14.     ///检查下载状态
  15.     fetchDownloadStatus();
  16.     super.initState();
  17.   }
  18.   /// 暂停时的回调
  19.   onPause() {
  20.     if (SharedPreferencesUtil.getDownloadTaskId() != '') {
  21.       FlutterDownloader.pause(taskId: SharedPreferencesUtil.getDownloadTaskId());
  22.     }
  23.   }
  24.   ///应用从暂停中恢复过来
  25.   onResume() async {
  26.     if (SharedPreferencesUtil.getDownloadTaskId() != '') {
  27.       ///任务被唤醒后会生成新的任务对象,任务id变化
  28.       String? taskId = await FlutterDownloader.resume(taskId: SharedPreferencesUtil.getDownloadTaskId());
  29.       SharedPreferencesUtil.setDownloadTaskId(taskId!);
  30.     }
  31.   }
复制代码
偷个懒把所有代码贴出来算了,一步一步讲解太麻烦了,这个代码里面着实每行我都写了注释,
步调:
1:授权他的文件管理权限Permission.manageExternalStorage.request();否则触发不了安装效果,文档是这么说的要外部存储空间的权限
2:启动下载任务,然后将返回的id存到全局中
3:初始化的时候已经启动了监听,registerDownloadTask方法就会监听到任务已经启动,此时这个监听的数据是从他的回调函数downloadCallback里面被send过来的,大要来说按照文档来的,有不会的可以参考我的写法
4:最后下载完之后我没有判定apk的完整性,因为如果不完整,在安装的时候系统自身就会提醒我们,我就没做了,因为会稍微有点卡顿
5:如果退到后台不暂停任务,就会导致任务canceled,这种状态不允许resume这个任务的,需要重新开始从0下载任务,如果想要他不被cancled,需要用到生命周期监听应用的状态去暂停和规复,这样子就不会导致任务cancled,但是太久了也会让他挂掉的,这种暂停的方式照旧挺简朴的,而且如果组件被销毁了,那些端口一定要摧毁掉,不然会存在问题的。保活的话得知道原生咋做的,这个后面我得研究研究
6:下载完后用户不小心点击取消安装了,大概下载到一半退出App了,这时候外部存储空间都会留下一个apk了,所以这个时候哈希值的作用就出来了,专门判定apk是否完整的,如果是完整的,就直接安装它,如果不完整,我采取的逻辑是删撤除这个apk,而且让用户重新下载,我参考了很多主流App除了应用市肆都是不会有继承下载的这种动作的,大部分都是后台静默下载大概强制下载完后打开,如果下载到一半退出了就重新下载。有不会的地方可以评论区问出来。
  1. 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]应用的appIdclass DownloadSheet extends StatefulWidget {  ///[build_name]构建版本  ///[version]版本号  ///[name]软件名  final Map<String, dynamic> packageInfo;  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<DownloadSheet> createState() => _DownloadSheetState();}class _DownloadSheetState extends State<DownloadSheet> {  var progress = 0;  ///进度条状态  var progressStatus = ProgressType.examineStatus;  final ReceivePort _port = ReceivePort();  late final AppLifecycleListener appLifecycleListener;  @override
  2.   void initState() {
  3.     ///监听应用生命周期对下载任务进行暂停和恢复
  4.     appLifecycleListener = AppLifecycleListener(
  5.       onStateChange: (AppLifecycleState state) {},
  6.       onResume: onResume,
  7.       onInactive: () {},
  8.       onHide: () {},
  9.       onShow: () {},
  10.       onPause: onPause,
  11.       onRestart: () {},
  12.       onDetach: () {},
  13.     );
  14.     ///检查下载状态
  15.     fetchDownloadStatus();
  16.     super.initState();
  17.   }
  18.   /// 暂停时的回调
  19.   onPause() {
  20.     if (SharedPreferencesUtil.getDownloadTaskId() != '') {
  21.       FlutterDownloader.pause(taskId: SharedPreferencesUtil.getDownloadTaskId());
  22.     }
  23.   }
  24.   ///应用从暂停中恢复过来
  25.   onResume() async {
  26.     if (SharedPreferencesUtil.getDownloadTaskId() != '') {
  27.       ///任务被唤醒后会生成新的任务对象,任务id变化
  28.       String? taskId = await FlutterDownloader.resume(taskId: SharedPreferencesUtil.getDownloadTaskId());
  29.       SharedPreferencesUtil.setDownloadTaskId(taskId!);
  30.     }
  31.   }
  32.   ///下载任务的回调函数  @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<void> fetchDownloadStatus() async {    String taskId = SharedPreferencesUtil.getDownloadTaskId();    List<DownloadTask>? taskInfo;    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<void> installValidation(bool isDownloadComplete) async {    ///外部存储空间    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<void> installEvent(String apkPath) async {    final res = await InstallPlugin.install(apkPath);    if (res['isSuccess']) {      NotifyUtil.showToast("安装成功");    } else if (res['errorMessage'] == "Install Cancel") {      NotifyUtil.showToast("您取消了安装");    }  }  ///盘算当前下载的apk的哈希值  Future<String> computeSha1OfFile(File file) async {    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文件中,用于下载状态栏文字的罗列的
  1. enum ProgressType {
  2.   examineStatus(0, "检查本地安装包..."),
  3.   prepareStatus(1, "立即更新"),
  4.   downloadStatus(2, "下载中"),
  5.   installStatus(3, "继续安装");
  6.   final int code;
  7.   final String description;
  8.   const ProgressType(this.code, this.description);
  9. }
复制代码


在这里插入图片描述

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

杀鸡焉用牛刀

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表