Flutter:打包apk,安卓版本更新(二)

Flutter:打包apk,详细图文介绍(一)基础上,实现安卓端的版本更新功能。

haskell 复制代码
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

js 复制代码
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

js 复制代码
<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文件夹

js 复制代码
<?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

js 复制代码
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

js 复制代码
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;
  }
}

测试一下:

js 复制代码
_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"]);
}

点击更新后,下载会计算下载进度。

相关推荐
怀君29 分钟前
Flutter——数据库Drift开发详细教程(四)
数据库·flutter
JhonKI1 小时前
【MySQL】存储引擎 - CSV详解
android·数据库·mysql
开开心心_Every1 小时前
手机隐私数据彻底删除工具:回收或弃用手机前防数据恢复
android·windows·python·搜索引擎·智能手机·pdf·音视频
大G哥2 小时前
Kotlin Lambda语法错误修复
android·java·开发语言·kotlin
鸿蒙布道师5 小时前
鸿蒙NEXT开发动画案例2
android·ios·华为·harmonyos·鸿蒙系统·arkui·huawei
androidwork5 小时前
Kotlin Android工程Mock数据方法总结
android·开发语言·kotlin
xiangxiongfly9157 小时前
Android setContentView()源码分析
android·setcontentview
人间有清欢9 小时前
Android开发补充内容
android·okhttp·rxjava·retrofit·hilt·jetpack compose
人间有清欢9 小时前
Android开发报错解决
android
每次的天空11 小时前
Android学习总结之kotlin协程面试篇
android·学习·kotlin