Flutter:打包apk,安卓版本更新(二)
在Flutter:打包apk,详细图文介绍(一)基础上,实现安卓端的版本更新功能。
1、把自己的demo文件复制到空项目中
2、生成APP图标:dart run icons_launcher:create
3、生成启动图:dart run flutter_native_splash:create
只是查看怎么在安卓端更新apk可忽略1-3步骤,
这些是安装更新需要用到的依赖
# apk安装插件
app_installer: ^1.3.1
# 获取安装包路径
path_provider: ^2.1.5
# 接口请求
dio: ^5.7.0
pubspec.yaml
name: demo
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.5.4
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# 多语言开启
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
# 状态管理
get: ^4.6.6
# apk安装插件
app_installer: ^1.3.1
# 获取安装包路径
path_provider: ^2.1.5
# 包信息
package_info_plus: ^8.1.1
# 离线存储
shared_preferences: ^2.3.3
# 接口请求
dio: ^5.7.0
# 猫哥封装基础组件,已包含配模适配ScreenUtil插件,可直接设置宽高.w,字体大小.sp
ducafe_ui_core: ^1.0.4
# 图片缓存
cached_network_image: ^3.4.1
# svg
flutter_svg: ^2.0.16
#轮播
carousel_slider: ^5.0.0
# ui
tdesign_flutter: ^0.1.7
# 下拉刷新
pull_to_refresh_flutter3: ^2.0.2
# 加载动画
flutter_easyloading: ^3.0.5
# 城市选择
city_pickers: ^1.3.0
# 徽章
badges: ^3.1.2
# 主题切换
adaptive_theme: ^3.6.0
# 图片、视频选取
extended_image: ^8.3.1
# wechat_assets_picker: ^9.3.2
# wechat_camera_picker: ^4.3.2
# 图片预览
photo_view: ^0.15.0
# 网页
# webview_flutter: ^4.10.0
# 二维码
qr_flutter: ^4.1.0
# 二维码扫描
mobile_scanner: ^6.0.2
# rename:https://pub.dev/packages/rename
# 修改包名:flutter pub global run rename setBundleId --value app.demo.com
# 修改程序名:flutter pub global run rename setAppName --value demo
rename: ^3.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# 启动屏
flutter_native_splash: ^2.4.1
# 启动图标
# 图标设计:https://www.canva.com/logos/templates/
# 图标工具:https://icon.kitchen/
# 生成APP图标执行:dart run icons_launcher:create
icons_launcher: ^3.0.0
flutter_lints: ^4.0.0
# app 图标
icons_launcher:
# 默认图标的路径
image_path: "assets/icons/ic_logo.png"
platforms:
android:
enable: true
# 消息图片,手机顶部状态栏弹出消息时
notification_image: "assets/icons/ic_foreground.png"
# adaptive_background_color: '#ffffff'
# 图标背景色
adaptive_background_image: "assets/icons/ic_background.png"
# 图标前景色(透明背景+图标)
adaptive_foreground_image: "assets/icons/ic_foreground.png"
ios:
enable: true
# 启动图适配 android 11 及以下, 12 以上,IOS
# 生成:dart run flutter_native_splash:create
# 删除:dart run flutter_native_splash:remove
flutter_native_splash:
web: false
color_android: "#ffffff"
# background_image_android: "assets/launcher/background.png"
background_image_ios: "assets/launcher/background.png"
# image_ios: "assets/launcher/android.png"
android_12:
image: "assets/launcher/android12.png"
# icon_background_color: "#324ea1"
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/svgs/
- assets/styleWidget/
- assets/img/
fonts:
- family: Montserrat
fonts:
- asset: assets/fonts/Montserrat/Montserrat-Light.ttf
weight: 300
- asset: assets/fonts/Montserrat/Montserrat-Regular.ttf
weight: 400
- asset: assets/fonts/Montserrat/Montserrat-Medium.ttf
weight: 500
- asset: assets/fonts/Montserrat/Montserrat-Bold.ttf
weight: 700
一、配置权限
android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 权限声明部分 -->
<uses-permission android:name="android.permission.INTERNET"/> <!-- 允许应用访问网络,用于下载APK -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <!-- 允许应用请求安装APK包 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <!-- 允许应用写入外部存储 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <!-- 允许应用读取外部存储 -->
<application
android:label="zhongmuyun"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- FileProvider配置 -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<!-- FileProvider是Android 7.0后推出的文件访问机制,用于安全地分享文件 -->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> <!-- 指定可访问路径的配置文件 -->
</provider>
<activity>
...
...
...
</activity>
</application>
</manifest>
二、创建file_paths.xml,
android/app/src/main/res/xml/file_paths.xml
如果没有xml
,就创建个xml
文件夹
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="."/>
<cache-path name="cache" path="."/>
<external-cache-path name="external_cache" path="."/>
</paths>
三、components封装
version_update_dialog.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:demo/common/index.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
class VersionUpdateDialog extends StatelessWidget {
final String? version;
final String? description;
final String? apkUrl;
final VoidCallback? onCancel;
final RxBool isDownloading;
final RxDouble downloadProgress;
final Function() onUpdate;
const VersionUpdateDialog({
Key? key,
this.version,
this.description,
this.apkUrl,
this.onCancel,
required this.isDownloading,
required this.downloadProgress,
required this.onUpdate,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: TDPopupCenterPanel(
closeUnderBottom: true,
closeClick: () {
onCancel?.call();
Get.back();
},
child: SizedBox(
width: 590.w,
height: 680.w,
child: <Widget>[
TDImage(
assetUrl: 'assets/img/update.png',
width: 590.w,
height: 280.w,
fit: BoxFit.contain,
),
SizedBox(height: 20.w),
const TextWidget.body(
'发现新版本',
textAlign: TextAlign.center,
),
SizedBox(height: 10.w),
TextWidget.body(
version ?? '',
textAlign: TextAlign.center,
),
SizedBox(height: 10.w),
<Widget>[
TextWidget.body(
description ?? '',
size: 24.sp,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
).width(460.w),
].toRow(
mainAxisAlignment: MainAxisAlignment.center,
).paddingAll(30.w).card(color: Color(0xffF6F7F9)).width(530.w),
SizedBox(height: 30.w),
Obx(() => isDownloading.value ?
<Widget>[
TextWidget.body(
'下载中...${downloadProgress.value.toInt()}%',
color: Colors.white,
textAlign: TextAlign.center,
),
].toRow(
mainAxisAlignment: MainAxisAlignment.center,
).card(color: Colors.blue).tight(width: 530.w,height: 88.w)
: TDButton(
text: '立即更新',
isBlock: true,
width: 530.w,
height: 88.w,
margin: const EdgeInsets.all(0),
style: TDButtonStyle(
backgroundColor: Colors.blue,
textColor: Colors.white,
radius: BorderRadius.circular(20.w),
),
onTap: onUpdate,
),
),
].toColumn(
crossAxisAlignment: CrossAxisAlignment.center,
),
),
),
);
}
}
version_update_utils.dart
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:app_installer/app_installer.dart';
import 'package:demo/common/index.dart';
import 'version_update_dialog.dart';
class VersionUpdateUtil {
static final RxDouble downloadProgress = 0.0.obs;
static final RxBool isDownloading = false.obs;
// 检查并显示更新
static void checkUpdate({
required String currentVersion,
required String latestVersion,
required String description,
required String apkUrl,
}) {
if (_shouldUpdate(currentVersion, latestVersion)) {
_showUpdateDialog(
version: latestVersion,
description: description,
apkUrl: apkUrl,
);
}
}
// 显示更新弹窗
static void _showUpdateDialog({
required String version,
required String description,
required String apkUrl,
}) {
Get.dialog(
VersionUpdateDialog(
version: version,
description: description,
apkUrl: apkUrl,
isDownloading: isDownloading,
downloadProgress: downloadProgress,
onUpdate: () => _downloadAndInstallApk(apkUrl),
),
barrierDismissible: false,
barrierColor: Get.theme.dividerColor.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 200),
transitionCurve: Curves.easeInOut,
useSafeArea: true,
);
}
// 下载并安装APK
static Future<void> _downloadAndInstallApk(String apkUrl) async {
if (isDownloading.value) return;
try {
isDownloading.value = true;
final dir = await getExternalStorageDirectory();
if (dir == null) {
Loading.error('无法获取存储目录');
return;
}
final apkPath = '${dir.path}/app-update.apk';
await Dio().download(
apkUrl,
apkPath,
onReceiveProgress: (received, total) {
if (total != -1) {
downloadProgress.value = ((received / total) * 100).roundToDouble();
}
},
);
if (Platform.isAndroid) {
await AppInstaller.installApk(apkPath);
} else {
Loading.error('仅支持Android设备');
}
isDownloading.value = false;
Get.back();
} catch (e) {
isDownloading.value = false;
downloadProgress.value = 0;
Loading.error('下载失败:$e');
}
}
// 比较版本号
static bool _shouldUpdate(String currentVersion, String latestVersion) {
List<int> current = currentVersion.split('.').map((e) => int.parse(e)).toList();
List<int> latest = latestVersion.split('.').map((e) => int.parse(e)).toList();
for (int i = 0; i < current.length && i < latest.length; i++) {
if (latest[i] > current[i]) return true;
if (latest[i] < current[i]) return false;
}
return latest.length > current.length;
}
}
测试一下:
_initData() async {
// 接口拿到更新数据
versionUpdateModel = await SystemApi.versionUpdate();
// 使用工具类检查更新,为了方便展示,把更新数据写死测试安装
VersionUpdateUtil.checkUpdate(
currentVersion: '1.0.0',
latestVersion: '1.0.1',
description: '更新内容',
apkUrl: 'http://oss.***/files/23b16eaa75eb942d12e9bdb0cabae8b1.apk',
// apkUrl: versionUpdateModel?.akpUrl ?? '',
);
update(["version_update"]);
}
点击更新后,下载会计算下载进度。