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

做当前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 getVersion() async {

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 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 createState() => _DownloadSheetState();

}

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 fetchDownloadStatus() async {

String taskId = SharedPreferencesUtil.getDownloadTaskId();

List? 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 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 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 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文件中,用于下载状态栏文字的枚举的

enum ProgressType {

examineStatus(0, "检查本地安装包..."),

prepareStatus(1, "立即更新"),

downloadStatus(2, "下载中"),

installStatus(3, "继续安装");

final int code;

final String description;

const ProgressType(this.code, this.description);

}

在这里插入图片描述