背景
每个App应用中一定会包含的功能升级
,App的运行平台不同实现升级功能的方式也就不同,基本分类如下所示。本文主要聚焦在使用Flutter框架开发的应用在安卓系统内如何更新,以及所涉及要点梳理。
- IOS系统
- Andriod系统
- 官网下载
- 各类应用市场
App内更新
(本文聚焦点)
功能清单(按步骤)
- UpgradeCard组件(展示升级内容)
- 进行新旧版本对比判断是否需要更新
- 下载更新然后安装应用
UpgradeCard组件
组件需要包含基本标题、弹框内容、确认按钮、取消按钮以及最为关键的进度条显示。不过这个组件还不具备下载安装功能。
UpgradeCard组件代码
//upgrade_card.dart文件
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本地版本号。
// main.dart文件
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await StorageManager.init(); // 本地数据存储配置
PackageInfo.fromPlatform().then((PackageInfo packageInfo) { //储存App本地版本号
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 {
/// apk更新url
final String downLoadUrl;
/// apk更新描述
final String message;
/// apk是否强制更新
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;
}
// 停止下载器运行(注销当前taskID)
closeCallback() {
Navigator.of(context).pop();
if (downloadId != '') FlutterDownloader.cancel(taskId: downloadId);
}
//第一步:点击更新按钮
_updateApplication() async {
if (status == DownloadTaskStatus.failed) return againDownloader();
_requestDownload(context, widget.downLoadUrl);
}
//第二步:flutterDownLoader新建Task
void _requestDownload(BuildContext context, url) async {
if (await _checkPermission()) {
initGeneral();
downloadId = (await FlutterDownloader.enqueue(
url: url, // 服务端提供apk下载路径
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包名称">
<!-- 安装.apk文件 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:label="这里是你的App包名称"
android:icon="@mipmap/ic_launcher">
<!-- app_installer配置 -->
<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>
<!-- 版本更新配置 (number是可选配置最大并发任务数)-->
<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>