Flutter中如何实现APP升级功能背景
2024-11-03
1159 字约 4 分钟
...背景
每个App应用中一定会包含的功能升级,App的运行平台不同实现升级功能的方式也就不同,基本分类如下所示。本文主要聚焦在使用Flutter框架开发的应用在安卓系统内如何更新,以及所涉及要点梳理。
- IOS系统
- Andriod系统
- 官网下载
- 各类应用市场
App内更新(本文聚焦点)
功能清单(按步骤)
- UpgradeCard组件(展示升级内容)
- 进行新旧版本对比判断是否需要更新
- 下载更新然后安装应用
UpgradeCard组件
组件需要包含基本标题、弹框内容、确认按钮、取消按钮以及最为关键的进度条显示。不过这个组件还不具备下载安装功能。
UpgradeCard组件代码
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class UpgradeCard extends StatefulWidget {
String title;
String message;
String positiveBtn;
String negativeBtn;
final GestureTapCallback positiveCallback;
final GestureTapCallback negativeCallback;
bool hasLinearProgress;
UpgradeCard(
{this.title = "",
this.message = "",
this.positiveBtn = "",
this.negativeBtn = "",
required this.positiveCallback,
required this.negativeCallback,
this.hasLinearProgress = false});
final _upgradeCardState = _UpgradeCardState();
@override
_UpgradeCardState createState() => _upgradeCardState;
void updateProgress(String title, String message, String positiveBtn,
String negativeBtn, bool hasLinearProgress, double progress) =>
_upgradeCardState.updateProgress(title, message, positiveBtn,
negativeBtn, hasLinearProgress, progress);
}
class _UpgradeCardState extends State<UpgradeCard> {
double progress = 0;
void updateProgress(String title, String message, String positiveBtn,
String negativeBtn, bool hasLinearProgress, double progress) {
setState(() {
widget.title = title;
widget.message = message;
widget.positiveBtn = positiveBtn;
widget.negativeBtn = negativeBtn;
widget.hasLinearProgress = hasLinearProgress;
progress = progress;
});
}
@override
Widget build(BuildContext context) {
var messageStyle = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
decoration: TextDecoration.none,
color: Color(0xFF333130),
height: 1.6);
return Center(
child: Container(
width: 320,
padding: EdgeInsets.only(bottom: 17),
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(8)),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.only(bottom: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
"assets/images/updateBg.png",
width: 320,
),
),
),
Visibility(
visible: widget.title.isNotEmpty,
child: Container(
padding: EdgeInsets.only(top: 25),
child: Text(
widget.title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
color: Color(0xFF333130)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
Container(
width: 280,
margin: EdgeInsets.only(top: 20, bottom: 20),
child: Text(
widget.message,
style: messageStyle,
textAlign: TextAlign.left,
)
),
Visibility(
visible: widget.hasLinearProgress,
child: Container(
height: 6,
width: 290,
margin: EdgeInsets.only(bottom: 20),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LinearProgressIndicator(
value: progress,
backgroundColor: Color(0xFFFAF8F7),
valueColor: new AlwaysStoppedAnimation<Color>(
Color.fromARGB(255, 64, 75, 130)),
),
)),
),
Container(
height: 60,
child: Row(
children: <Widget>[
Visibility(
visible: widget.negativeBtn.isNotEmpty,
child: Expanded(
child: Container(
child: TextButton(
onPressed: widget.negativeCallback,
child: Text(
widget.negativeBtn,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xffDFE2F0)),
)),
)),
),
Container(
height: 60,
width: 0.5,
color: Color(0xffC0C5D6),
),
Visibility(
visible: widget.positiveBtn.isNotEmpty,
child: Expanded(
child: Container(
child: TextButton(
onPressed: widget.positiveBtn != "下载中"
? widget.positiveCallback
: () {},
child: Text(
widget.positiveBtn,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color.fromARGB(255, 64, 75, 130),
)),
)),
)
],
),
),
],
),
),
);
}
}
需要说明的是updateProgress方法可以更改UpgradeCard组件显示内容,下文会用到这个方法。
组件展示效果如下图:

进行新旧版本对比
- 配置和获取App本地版本号,首先需要更改项目pubspec.yaml文件中的version字段
//pubspec.yaml
version: 0.0.3
- 在项目入口文件中利用package_info_plus库来读取pubspec.yaml配置文件,然后通过shared_preferences库储存App本地版本号。
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await StorageManager.init();
PackageInfo.fromPlatform().then((PackageInfo packageInfo) {
String version = packageInfo.version;
StorageManager.setString("appVersion", version);
});
runApp(MyApp());
}
- 现在我们已经配置并存储好本地的版本号了,服务端的版本号可以通过检查更新的接口获取到,然后将两者做一个对比来判断是否需要提示用户进行版本更新。
bool isNewVersion(String netVersion) {
String localAppVersion = StorageManager.getString("appVersion");
if (netVersion.isEmpty || localAppVersion.isEmpty) return false;
try {
List<String> arr1 = netVersion.split('.');
List<String> arr2 = localAppVersion.split('.');
int length1 = arr1.length;
int length2 = arr2.length;
int minLength = length1 < length2 ? length1 : length2;
int i = 0;
for (i; i < minLength; i++) {
int a = int.parse(arr1[i]);
int b = int.parse(arr2[i]);
if (a > b) {
return true;
} else if (a < b) {
return false;
}
}
if (length1 > length2) {
for (int j = i; j < length1; j++) {
if (int.parse(arr1[j]) != 0) {
return true;
}
}
return false;
} else if (length1 < length2) {
for (int j = i; j < length2; j++) {
if (int.parse(arr2[j]) != 0) {
return false;
}
}
return false;
}
return false;
} catch (err) {
return false;
}
}
下载更新和安装应用
现在我们需要对UpgradeCard组件进行二次封装,增加权限判断、路径获取、apk下载、隔离间通信、apk安装等步骤。先列举一下以上功能用到插件是:
- 权限判断:permission_handler
- 路径获取:path_provider
- 文件下载:flutter_downloader
- apk安装:app_installer
- 首先处理两个隔离之间的通信(因为UI渲染在主隔离上,而下载时间来自后台隔离)主要用到的是dart内置的
dart:isolate库,_bindBackgroundIsolate() 和 _unbindBackgroundIsolate()。 - 然后获取手机内可存放apk文件的地址利用
path_provider库,_apkLocalPath() - 初始化更新标题文案,利用
permission_handler库提供一个检查许可权限方法**_checkPermission** - 组建初始化的时候注册一个下载器的回调函数 FlutterDownloader.registerCallback(...)
- 用户点击确认按钮后检查权限,利用
flutter_downloader库创建下载任务FlutterDownloader.enqueue(...) - 如果下载失败,则利用记录的downloadId重新发起下载,下载完成利用
app_installer库完成apk安装过程 openAPK()
核心代码如下所示(非完整版本)
class UpdateDownloader extends StatefulWidget {
final String downLoadUrl;
final String message;
final bool isForce;
UpdateDownloader(
{required this.downLoadUrl,
required this.message,
required this.isForce});
@override
_UpdateDownloaderState createState() => _UpdateDownloaderState();
}
class _UpdateDownloaderState extends State<UpdateDownloader> {
int progress = 0;
String _localPath = '';
String downloadId = '';
DownloadTaskStatus? status;
ReceivePort _port = ReceivePort();
UpgradeCard? _upgradeCard;
@override
void initState() {
super.initState();
_bindBackgroundIsolate();
FlutterDownloader.registerCallback((
String id, DownloadTaskStatus status, int progress) {
IsolateNameServer.lookupPortByName("downloader_send_port")
?.send([id, status, progress]);
});
}
@override
void dispose() {
IsolateNameServer.removePortNameMapping(download_key);
super.dispose();
}
void _bindBackgroundIsolate() {
bool isSuccess =
IsolateNameServer.registerPortWithName(_port.sendPort, "downloader_send_port");
if (!isSuccess) {
_unbindBackgroundIsolate();
_bindBackgroundIsolate();
return;
}
_port.listen((dynamic data) {
final taskId = (data as List<dynamic>)[0] as String;
status = data[1] as DownloadTaskStatus;
progress = data[2] as int;
setState(() {
downloadId = taskId;
});
_upgradeCard?.updateProgress("检查更新", widget.message, "进行升级", "取消", true,
double.parse((progress / 100).toStringAsFixed(1)));
if (status == DownloadTaskStatus.failed) {
_upgradeCard?.updateProgress("失败确认", "应用程序下载失败,请重试", "重新下载",
"取消", false, 0);
}
if (status == DownloadTaskStatus.complete) {
Future.delayed(new Duration(milliseconds: 500), () async {
Navigator.of(context).pop();
openAPK();
});
}
});
}
void _unbindBackgroundIsolate() {
IsolateNameServer.removePortNameMapping(download_key);
}
initGeneral() {
_upgradeCard?.updateProgress("检查更新", "widget.description", "进行升级",
"取消", widget.isForce, 0);
}
Future<String> get _apkLocalPath async {
final directory = await getExternalStorageDirectory();
_localPath = directory!.path.toString();
return _localPath;
}
Future<bool> _checkPermission() async {
PermissionStatus statuses = await Permission.storage.status;
if (statuses != PermissionStatus.granted) {
Map<Permission, PermissionStatus> statuses =
await [Permission.storage].request();
if (statuses[Permission.storage] == PermissionStatus.granted) {
return true;
}
} else {
return true;
}
return false;
}
closeCallback() {
Navigator.of(context).pop();
if (downloadId != '') FlutterDownloader.cancel(taskId: downloadId);
}
_updateApplication() async {
if (status == DownloadTaskStatus.failed) return againDownloader();
_requestDownload(context, widget.downLoadUrl);
}
void _requestDownload(BuildContext context, url) async {
if (await _checkPermission()) {
initGeneral();
downloadId = (await FlutterDownloader.enqueue(
url: url,
savedDir: await _apkLocalPath,
showNotification: true,
openFileFromNotification: true,
))!;
} else {
ToastUtil.show("权限获取失败");
}
}
void againDownloader() async {
initGeneral();
downloadId = (await FlutterDownloader.retry(taskId: downloadId))!;
}
openAPK() async {
String path = await _apkLocalPath;
String last =
widget.downLoadUrl.substring(widget.downLoadUrl.lastIndexOf("/") + 1);
AppInstaller.installApk(path + "/" + last)
.then((result) {})
.catchError((error) {
ToastUtil.show(error);
});
}
@override
Widget build(BuildContext context) {
if (_upgradeCard != null) {
return _upgradeCard!;
}
return _upgradeCard = UpgradeCard(
title: "检查更新",
message: widget.message,
positiveBtn: "进行升级",
negativeBtn: widget.isForce ? "" : "取消",
positiveCallback: () => _updateApplication(),
negativeCallback: () => closeCallback(),
closeCallback: () => closeCallback(),
);
}
}
配置系统权限
位于根目录下android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.这里是你的App包名称">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:label="这里是你的App包名称"
android:icon="@mipmap/ic_launcher">
<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/provider_paths" />
</provider>
<provider
android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
android:authorities="${applicationId}.flutter_downloader.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:enabled="false"
android:exported="false" />
<provider
android:name="vn.hunghd.flutterdownloader.FlutterDownloaderInitializer"
android:authorities="${applicationId}.flutter-downloader-init"
android:exported="false">
<meta-data
android:name="vn.hunghd.flutterdownloader.MAX_CONCURRENT_TASKS"
android:value="5" />
</provider>
<service android:name="androidx.work.impl.background.systemjob.SystemJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
</application>
</manifest>
位于根目录下android/app/src/main下新建文件夹xml,在xml下新建provider_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path path="Android/data/com.这里是你的App包名称/" name="files_root" />
<external-path path="." name="external_storage_root" />
</paths>
如果您觉得这篇文章有帮助,请点个赞吧~
相关文章
更多文章 →flutter2026-07-01
Dart 入门回顾
1\. Dart 是什么 Dart 是一门类型安全、支持面向对象、支持异步编程的语言。它的语法和 JavaScript、TypeScript、Java、C# 都有一些相似之处。 Dart 常见使用场景: Flutter 跨平台应用开发 命令行工具 Web 应用 服务端接口或脚本 作为学习现代类型语言的入口 Dart 的几个核心特点: 静态类型:变量、函数返回值、参数都有类型。 类型推断:很多时候可以用 ,让 Dart 自动推断类型。 空...
学习
flutter2026-07-01
Flutter 常用 Widget 入门
Flutter 的核心思想是:一切皆 Widget。文字、图片、按钮、页面结构、布局、间距、滚动、动画、手势,几乎都由 Widget 组合出来。 1\. Widget 先分成几类理解 | 类别 | 常用 Widget | 解决什么问题 | | | | | | 应用入口 | 、 | 搭建应用外壳、主题、路由 | | 页面骨架 | 、 、 | 标准页面结构 | | 基础显示 | 、 、 | 显示文字、图标、图片 | | 布局容器 | 、 、...
学习
flutter2024-11-21
在 Flutter 中使用 webview_flutter 4.0
1. 2. ,已入选 3. 4. 本文是第 4 篇,定位是新手入门,介绍和演示 webview 的基础用法。最后还介绍了事件处理的技巧。因为是补写的,所以本来应该首发的却发在最后了。 环境准备已经在 说过了,不再赘述。既然是新手入门,就会本着详尽的原则,而且会多配示例。 获取页面信息 获取页面 title 核心方法:controller.getTitle 完整示例,放在 main.dart 就能运行。运行示例,点击获取 title 的按...
学习
flutter2024-11-06
flutter android端运行报错Namespace not specified. Specify a namespace in the module‘s build file
在使用flutterSDK 3.24之后发现新建项目,项目默认都是基于AGP8了,由于 AGP 开始强制要求 namespace 配置,不仅仅是主工程,也包括第三方库都强制要求 namespace 。但是我们在引入第三方库的时候会发现部分库没能及时的去做相关配置,这也就导致了我们会出现标题错误: 出现这个问题,我们可能会通过去找到本地缓存的第三方库代码去修改它的build.gradle和AndroidManifest.xml来解决缺失...
学习
flutter2024-11-03
Flutter应用内更新App版本使用flutter_downloader而非ota_update
做当前App应用内更新的前提:用户可以在App更新的时候还可以使用软件,不能让他一直等待下载,关闭下载组件后,打开组件还能继续看到他的进度,并且用户锁屏或者短暂退出APP后需要保证任务不能总是挂掉,这里没有用到保活,而是需要用到生命周期去让他暂停和恢复,所以当前app还要有一个全局管理类保存任务id。UI图在最后 (插件文档说明可以通知栏显示,但是我用的这个版本在100%下完后总是failed,而不是complete,就不会很友好,后面...
学习
flutter2024-11-03
强大的Flutter App升级功能
注意:无特殊说明,Flutter版本及Dart版本如下: Flutter版本: 1.12.13+hotfix.5 Dart版本: 2.7.0 应用程序升级功能是App的基础功能之一,如果没有此功能会造成用户无法升级,应用程序的bug或者新功能老用户无法触达,甚至损失这部分用户。 对于应用程序升级功能的重要性就无需赘言了,下面介绍下应用程序升级功能的几种方式,从平台方面来说: IOS平台,应用程序升级功能只能通过跳转到app store进...
学习
评论
请登录后发表评论
去登录