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

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

相关推荐
还鮟1 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡3 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi003 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
zhangphil4 小时前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android
你过来啊你4 小时前
Android View的绘制原理详解
android
瓜子三百克6 小时前
十、高级概念
flutter
移动开发者1号7 小时前
使用 Android App Bundle 极致压缩应用体积
android·kotlin
移动开发者1号7 小时前
构建高可用线上性能监控体系:从原理到实战
android·kotlin
ii_best12 小时前
按键精灵支持安卓14、15系统,兼容64位环境开发辅助工具
android
美狐美颜sdk12 小时前
跨平台直播美颜SDK集成实录:Android/iOS如何适配贴纸功能
android·人工智能·ios·架构·音视频·美颜sdk·第三方美颜sdk