背景
每个App应用中一定会包含的功能升级
,App的运行平台不同实现升级功能的方式也就不同,基本分类如下所示。本文主要聚焦在使用Flutter框架开发的应用在安卓系统内如何更新,以及所涉及要点梳理。
- IOS系统
- Andriod系统
- 官网下载
- 各类应用市场
App内更新
(本文聚焦点)
功能清单(按步骤)
- UpgradeCard组件(展示升级内容)
- 进行新旧版本对比判断是否需要更新
- 下载更新然后安装应用
UpgradeCard组件
组件需要包含基本标题、弹框内容、确认按钮、取消按钮以及最为关键的进度条显示。不过这个组件还不具备下载安装功能。
UpgradeCard组件代码
1import 'package:flutter/material.dart';
2import 'package:flutter_svg/flutter_svg.dart';
3
4class UpgradeCard extends StatefulWidget {
5
6 String title;
7
8 String message;
9
10 String positiveBtn;
11
12 String negativeBtn;
13
14 final GestureTapCallback positiveCallback;
15
16 final GestureTapCallback negativeCallback;
17
18 bool hasLinearProgress;
19
20 UpgradeCard(
21 {this.title = "",
22 this.message = "",
23 this.positiveBtn = "",
24 this.negativeBtn = "",
25 required this.positiveCallback,
26 required this.negativeCallback,
27 this.hasLinearProgress = false});
28
29 final _upgradeCardState = _UpgradeCardState();
30
31 @override
32 _UpgradeCardState createState() => _upgradeCardState;
33
34
35 void updateProgress(String title, String message, String positiveBtn,
36 String negativeBtn, bool hasLinearProgress, double progress) =>
37 _upgradeCardState.updateProgress(title, message, positiveBtn,
38 negativeBtn, hasLinearProgress, progress);
39}
40
41class _UpgradeCardState extends State<UpgradeCard> {
42
43 double progress = 0;
44
45
46 void updateProgress(String title, String message, String positiveBtn,
47 String negativeBtn, bool hasLinearProgress, double progress) {
48 setState(() {
49 widget.title = title;
50 widget.message = message;
51 widget.positiveBtn = positiveBtn;
52 widget.negativeBtn = negativeBtn;
53 widget.hasLinearProgress = hasLinearProgress;
54 progress = progress;
55 });
56 }
57
58 @override
59 Widget build(BuildContext context) {
60 var messageStyle = TextStyle(
61 fontSize: 14,
62 fontWeight: FontWeight.w400,
63 decoration: TextDecoration.none,
64 color: Color(0xFF333130),
65 height: 1.6);
66
67 return Center(
68 child: Container(
69 width: 320,
70 padding: EdgeInsets.only(bottom: 17),
71 decoration: BoxDecoration(
72 color: Colors.white, borderRadius: BorderRadius.circular(8)),
73 child: Column(
74 mainAxisSize: MainAxisSize.min,
75 children: <Widget>[
76
77 Container(
78 padding: EdgeInsets.only(bottom: 8),
79 child: ClipRRect(
80 borderRadius: BorderRadius.circular(8),
81 child: Image.asset(
82 "assets/images/updateBg.png",
83 width: 320,
84 ),
85 ),
86 ),
87
88
89 Visibility(
90 visible: widget.title.isNotEmpty,
91 child: Container(
92 padding: EdgeInsets.only(top: 25),
93 child: Text(
94 widget.title,
95 style: TextStyle(
96 fontSize: 18,
97 fontWeight: FontWeight.w600,
98 decoration: TextDecoration.none,
99 color: Color(0xFF333130)),
100 maxLines: 1,
101 overflow: TextOverflow.ellipsis,
102 ),
103 ),
104 ),
105
106
107 Container(
108 width: 280,
109 margin: EdgeInsets.only(top: 20, bottom: 20),
110 child: Text(
111 widget.message,
112 style: messageStyle,
113 textAlign: TextAlign.left,
114 )
115 ),
116
117
118 Visibility(
119 visible: widget.hasLinearProgress,
120 child: Container(
121 height: 6,
122 width: 290,
123 margin: EdgeInsets.only(bottom: 20),
124 child: ClipRRect(
125 borderRadius: BorderRadius.circular(8),
126 child: LinearProgressIndicator(
127 value: progress,
128 backgroundColor: Color(0xFFFAF8F7),
129 valueColor: new AlwaysStoppedAnimation<Color>(
130 Color.fromARGB(255, 64, 75, 130)),
131 ),
132 )),
133 ),
134
135
136 Container(
137 height: 60,
138 child: Row(
139 children: <Widget>[
140 Visibility(
141 visible: widget.negativeBtn.isNotEmpty,
142 child: Expanded(
143 child: Container(
144 child: TextButton(
145 onPressed: widget.negativeCallback,
146 child: Text(
147 widget.negativeBtn,
148 style: TextStyle(
149 fontSize: 16,
150 fontWeight: FontWeight.w500,
151 color: Color(0xffDFE2F0)),
152 )),
153 )),
154 ),
155 Container(
156 height: 60,
157 width: 0.5,
158 color: Color(0xffC0C5D6),
159 ),
160 Visibility(
161 visible: widget.positiveBtn.isNotEmpty,
162 child: Expanded(
163 child: Container(
164 child: TextButton(
165 onPressed: widget.positiveBtn != "下载中"
166 ? widget.positiveCallback
167 : () {},
168 child: Text(
169 widget.positiveBtn,
170 style: TextStyle(
171 fontSize: 16,
172 fontWeight: FontWeight.w500,
173 color: Color.fromARGB(255, 64, 75, 130),
174 )),
175 )),
176 )
177 ],
178 ),
179 ),
180 ],
181 ),
182 ),
183 );
184 }
185}
186
需要说明的是updateProgress方法
可以更改UpgradeCard组件显示内容,下文会用到这个方法。
组件展示效果如下图:
进行新旧版本对比
- 配置和获取App本地版本号,首先需要更改项目pubspec.yaml文件中的version字段
1//pubspec.yaml
2version: 0.0.3
- 在项目入口文件中利用package_info_plus库来读取pubspec.yaml配置文件,然后通过shared_preferences库储存App本地版本号。
1void main() async {
2 WidgetsFlutterBinding.ensureInitialized();
3
4 await StorageManager.init();
5
6 PackageInfo.fromPlatform().then((PackageInfo packageInfo) {
7 String version = packageInfo.version;
8 StorageManager.setString("appVersion", version);
9 });
10
11 runApp(MyApp());
12}
- 现在我们已经配置并存储好本地的版本号了,服务端的版本号可以通过检查更新的接口获取到,然后将两者做一个对比来判断是否需要提示用户进行版本更新。
1bool isNewVersion(String netVersion) {
2 String localAppVersion = StorageManager.getString("appVersion");
3 if (netVersion.isEmpty || localAppVersion.isEmpty) return false;
4 try {
5 List<String> arr1 = netVersion.split('.');
6 List<String> arr2 = localAppVersion.split('.');
7 int length1 = arr1.length;
8 int length2 = arr2.length;
9 int minLength = length1 < length2 ? length1 : length2;
10 int i = 0;
11 for (i; i < minLength; i++) {
12 int a = int.parse(arr1[i]);
13 int b = int.parse(arr2[i]);
14 if (a > b) {
15 return true;
16 } else if (a < b) {
17 return false;
18 }
19 }
20 if (length1 > length2) {
21 for (int j = i; j < length1; j++) {
22 if (int.parse(arr1[j]) != 0) {
23 return true;
24 }
25 }
26 return false;
27 } else if (length1 < length2) {
28 for (int j = i; j < length2; j++) {
29 if (int.parse(arr2[j]) != 0) {
30 return false;
31 }
32 }
33 return false;
34 }
35 return false;
36 } catch (err) {
37 return false;
38 }
39}
下载更新和安装应用
现在我们需要对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()
核心代码如下所示(非完整版本)
1class UpdateDownloader extends StatefulWidget {
2
3 final String downLoadUrl;
4
5
6 final String message;
7
8
9 final bool isForce;
10
11 UpdateDownloader(
12 {required this.downLoadUrl,
13 required this.message,
14 required this.isForce});
15
16 @override
17 _UpdateDownloaderState createState() => _UpdateDownloaderState();
18}
19
20class _UpdateDownloaderState extends State<UpdateDownloader> {
21 int progress = 0;
22 String _localPath = '';
23 String downloadId = '';
24 DownloadTaskStatus? status;
25 ReceivePort _port = ReceivePort();
26 UpgradeCard? _upgradeCard;
27
28 @override
29 void initState() {
30 super.initState();
31 _bindBackgroundIsolate();
32 FlutterDownloader.registerCallback((
33 String id, DownloadTaskStatus status, int progress) {
34 IsolateNameServer.lookupPortByName("downloader_send_port")
35 ?.send([id, status, progress]);
36 });
37 }
38
39 @override
40 void dispose() {
41 IsolateNameServer.removePortNameMapping(download_key);
42 super.dispose();
43 }
44
45
46 void _bindBackgroundIsolate() {
47 bool isSuccess =
48 IsolateNameServer.registerPortWithName(_port.sendPort, "downloader_send_port");
49 if (!isSuccess) {
50 _unbindBackgroundIsolate();
51 _bindBackgroundIsolate();
52 return;
53 }
54 _port.listen((dynamic data) {
55 final taskId = (data as List<dynamic>)[0] as String;
56 status = data[1] as DownloadTaskStatus;
57 progress = data[2] as int;
58
59 setState(() {
60 downloadId = taskId;
61 });
62
63
64 _upgradeCard?.updateProgress("检查更新", widget.message, "进行升级", "取消", true,
65 double.parse((progress / 100).toStringAsFixed(1)));
66
67
68 if (status == DownloadTaskStatus.failed) {
69 _upgradeCard?.updateProgress("失败确认", "应用程序下载失败,请重试", "重新下载",
70 "取消", false, 0);
71 }
72
73 if (status == DownloadTaskStatus.complete) {
74 Future.delayed(new Duration(milliseconds: 500), () async {
75 Navigator.of(context).pop();
76 openAPK();
77 });
78 }
79 });
80 }
81
82
83 void _unbindBackgroundIsolate() {
84 IsolateNameServer.removePortNameMapping(download_key);
85 }
86
87
88 initGeneral() {
89 _upgradeCard?.updateProgress("检查更新", "widget.description", "进行升级",
90 "取消", widget.isForce, 0);
91 }
92
93
94 Future<String> get _apkLocalPath async {
95 final directory = await getExternalStorageDirectory();
96 _localPath = directory!.path.toString();
97 return _localPath;
98 }
99
100
101 Future<bool> _checkPermission() async {
102 PermissionStatus statuses = await Permission.storage.status;
103 if (statuses != PermissionStatus.granted) {
104 Map<Permission, PermissionStatus> statuses =
105 await [Permission.storage].request();
106 if (statuses[Permission.storage] == PermissionStatus.granted) {
107 return true;
108 }
109 } else {
110 return true;
111 }
112 return false;
113 }
114
115
116 closeCallback() {
117 Navigator.of(context).pop();
118 if (downloadId != '') FlutterDownloader.cancel(taskId: downloadId);
119 }
120
121
122
123 _updateApplication() async {
124 if (status == DownloadTaskStatus.failed) return againDownloader();
125 _requestDownload(context, widget.downLoadUrl);
126 }
127
128
129 void _requestDownload(BuildContext context, url) async {
130 if (await _checkPermission()) {
131 initGeneral();
132 downloadId = (await FlutterDownloader.enqueue(
133 url: url,
134 savedDir: await _apkLocalPath,
135 showNotification: true,
136 openFileFromNotification: true,
137 ))!;
138 } else {
139 ToastUtil.show("权限获取失败");
140 }
141 }
142
143
144 void againDownloader() async {
145 initGeneral();
146 downloadId = (await FlutterDownloader.retry(taskId: downloadId))!;
147 }
148
149 openAPK() async {
150 String path = await _apkLocalPath;
151 String last =
152 widget.downLoadUrl.substring(widget.downLoadUrl.lastIndexOf("/") + 1);
153 AppInstaller.installApk(path + "/" + last)
154 .then((result) {})
155 .catchError((error) {
156 ToastUtil.show(error);
157 });
158 }
159
160
161 @override
162 Widget build(BuildContext context) {
163 if (_upgradeCard != null) {
164 return _upgradeCard!;
165 }
166 return _upgradeCard = UpgradeCard(
167 title: "检查更新",
168 message: widget.message,
169 positiveBtn: "进行升级",
170 negativeBtn: widget.isForce ? "" : "取消",
171 positiveCallback: () => _updateApplication(),
172 negativeCallback: () => closeCallback(),
173 closeCallback: () => closeCallback(),
174 );
175 }
176}
配置系统权限
位于根目录下android/app/src/main/AndroidManifest.xml
1<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2 package="com.这里是你的App包名称">
3
4 <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
5
6 <application
7 android:label="这里是你的App包名称"
8 android:icon="@mipmap/ic_launcher">
9
10
11 <provider
12 android:name="androidx.core.content.FileProvider"
13 android:authorities="${applicationId}.fileProvider"
14 android:exported="false"
15 android:grantUriPermissions="true">
16 <meta-data
17 android:name="android.support.FILE_PROVIDER_PATHS"
18 android:resource="@xml/provider_paths" />
19 </provider>
20
21
22 <provider
23 android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
24 android:authorities="${applicationId}.flutter_downloader.provider"
25 android:exported="false"
26 android:grantUriPermissions="true">
27 <meta-data
28 android:name="android.support.FILE_PROVIDER_PATHS"
29 android:resource="@xml/provider_paths"/>
30 </provider>
31 <provider
32 android:name="androidx.work.impl.WorkManagerInitializer"
33 android:authorities="${applicationId}.workmanager-init"
34 android:enabled="false"
35 android:exported="false" />
36 <provider
37 android:name="vn.hunghd.flutterdownloader.FlutterDownloaderInitializer"
38 android:authorities="${applicationId}.flutter-downloader-init"
39 android:exported="false">
40 <meta-data
41 android:name="vn.hunghd.flutterdownloader.MAX_CONCURRENT_TASKS"
42 android:value="5" />
43 </provider>
44 <service android:name="androidx.work.impl.background.systemjob.SystemJobService"
45 android:permission="android.permission.BIND_JOB_SERVICE"
46 android:exported="true"/>
47
48
49 </application>
50</manifest>
位于根目录下android/app/src/main下新建文件夹xml,在xml下新建provider_paths.xml
1<?xml version="1.0" encoding="utf-8"?>
2<paths>
3 <external-path path="Android/data/com.这里是你的App包名称/" name="files_root" />
4 <external-path path="." name="external_storage_root" />
5</paths>
个人笔记记录 2021 ~ 2025