当前位置: 首页 > article >正文

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"]);
}

在这里插入图片描述
点击更新后,下载会计算下载进度。
在这里插入图片描述

在这里插入图片描述


http://www.kler.cn/a/470892.html

相关文章:

  • 虚表 —— 隐藏行(简单版)
  • Functions
  • 【C++】B2099 矩阵交换行
  • 深入了解 ES6 Map:用法与实践
  • 练习(继承)
  • Cyber Security 101-Web Hacking-Burp Suite: The Basics(Burp Suite:基础知识)
  • 使用Python构建远程医疗平台:从零开始的实现指南
  • 【错误记录】HarmonyOS 编译报错 ( DevEco Studio 开发环境 与 API 版本 与 HarmonyOS 版本 的配套关系 )
  • 君正T41交叉编译ffmpeg、opencv并做h264软解,利用君正SDK做h264硬件编码
  • Angular由一个bug说起之十三:Cross Origin
  • C++二十三种设计模式之外观模式
  • Nginx不使用域名如何配置证书
  • 谷歌浏览器的高级开发者工具使用指南
  • Ubuntu下安装Android Sdk
  • HarmonyOS NEXT 应用开发练习:AI智能语音播报
  • 云开发 Copilot:AI 赋能的低代码革命
  • leetcode(hot100)8、9
  • java设计模式 单例模式
  • 【python】json库处理JSON数据
  • 论文复现6:
  • 微服务框架,Http异步编程中,如何保证数据的最终一致性
  • Linux高并发服务器开发 第九天(gdb调试器/基础指令 栈帧)
  • latex学习记录
  • 网络安全漏洞防护技术原理与应用
  • 【RK3588 Linux 5.x 内核编程】-Misc设备驱动
  • 【JMeter】单接口