flutter 优雅的版本更新处理

5,975 阅读2分钟

关注公众号: 微信搜索 前端工具人 ; 收货更多的干货

一、思路步骤:

  1. 获取后台接口返回的最新版本的 vsersion 值! (这需要每次APP发版提审后,同时更新后台数据库Appversion);
  2. 使用 package_info 插件获取项目当前APP运行的 的版本号 version
  3. 后台返回的版本 version 大于 package_info 获取的 version!则弹出提示更新;
  4. ios 跳转 AppStore 进行更新; android 则App内下载更新;

二、说明:

  1. android 版本的 apk 上传到公司 oss, 生成的链接就是下载链接
  2. 可能存在的坑有, 本地开发模式下 android 下载好的apk安装不了, 提示应用签名不一致! (原因 本地开发模式并没有走 keystore签名;但是apk有; 用户从应用商店下载不会出现这情况; 或者你通过apk安装应用,在走流程测试就不会了)
  3. 使用的第三方插件有 package_info 用于获取当前版本号;flutter_xupdate 用于 android App内下载更新;
  4. 效果图见尾部

三、具体代码:

注: android 实现 App 内下载更新,我选用的是 flutter_xupdate插件并且使用的是自定义JSON的格式(灵活性较好); 插件使用方式请点击 flutter_xupdate

3.1、 homePage.dart 文件

    import './CheckUpdate.dart';
    CheckUpdate checkUpdate = CheckUpdate();
  
    // 获取版本
    Future<dynamic> initPackageInfo () async {
      PackageInfo packageInfo = await PackageInfo.fromPlatform();
      version = packageInfo.version;
    }
    
     // 检查是否需要版本更新
    void _checkUpdateVersion () async {
      await initPackageInfo();
      try {
        var response = await HttpUtil().post("app/version/detail?", 'mobile', 'json', false);
        if (response["code"] != 0) {
          setState(() {
            versionData = response["data"];
          });
          // 后台返回的版本号是带小数点的(2.8.1)所以去除小数点用于做对比
          var targetVersion = response["data"]["versionCode"].replaceAll('.', '');
          // 当前App运行版本
          var currentVersion = version.replaceAll('.', '');
          if (int.parse(targetVersion) > int.parse(currentVersion)) {
            if (Platform.isAndroid) { // 安卓弹窗提示本地下载, 交由flutter_xupdate 处理,不用我们干嘛。
              await checkUpdate.initXUpdate();
              checkUpdate.checkUpdateByUpdateEntity(versionData); // flutter_xupdate 自定义JSON 方式, 
            } else if (Platform.isIOS) { // IOS 跳转 AppStore
              showIOSDialog(); // 弹出ios提示更新框
            }
          }
        }
      } catch (e) {
        print(e);
      }
    }
  

3.2、CheckUpdate.dart 文件


import 'dart:convert';
import 'package:flutter_xupdate/flutter_xupdate.dart';

class CheckUpdate{
  // 将自定义的json内容解析为UpdateEntity实体类
  UpdateEntity customParseJson(String json) {
    AppInfo appInfo = AppInfo.fromJson(json);
    return UpdateEntity(
      isForce: appInfo.isForce, // 是否强制更新
      hasUpdate: appInfo.hasUpdate, // 是否需要更新  默认true, 手动自行判断
      isIgnorable: appInfo.isIgnorable, // 是否显示 “忽略该版本”
      versionCode: appInfo.versionCode, // 新版本号
      versionName: appInfo.versionName, // 新版名称
      updateContent: appInfo.updateLog, // 新版更新日志
      downloadUrl: appInfo.apkUrl, // 新版本下载链接
      apkSize: appInfo.apkSize); // 新版本大小
  }
    // 自定义JSON更新
  checkUpdateByUpdateEntity(Map jsonData) async {
    var versionCode = jsonData["versionCode"].replaceAll('.', '');
    var updateText = jsonData["updateLog"].split('。');
    var updateLog = '';
    updateText.forEach((t) {
      updateLog += '\r\n$t';
    });
    var rusultJson = {
      "isForce": jsonData["isForce"] == 1,
      "hasUpdate": true,
      "isIgnorable": false,
      "versionCode": int.parse(versionCode),
      "versionName": jsonData["versionName"],
      "updateLog": updateLog,
      "apkUrl": jsonData["apkUrl"],
      "apkSize":jsonData["apkSize"]
    };
    FlutterXUpdate.updateByInfo(updateEntity: customParseJson(json.encode(rusultJson)));
  }

  // 初始化插件
  Future<dynamic> initXUpdate () async {
    FlutterXUpdate.init(
      //是否输出日志
      debug: true,
      //是否使用post请求
      isPost: false,
      //post请求是否是上传json
      isPostJson: false,
      //是否开启自动模式
      isWifiOnly: false,
      ///是否开启自动模式
      isAutoMode: false,
      //需要设置的公共参数
      supportSilentInstall: false,
      //在下载过程中,如果点击了取消的话,是否弹出切换下载方式的重试提示弹窗
      enableRetry: false)
    .then((value) {
      print("初始化成功: $value");
    }).catchError((error) {
      print(error);
    });
    FlutterXUpdate.setUpdateHandler(
      onUpdateError: (Map<String, dynamic> message) async {
        print("初始化成功: $message");
      }, onUpdateParse: (String json) async {
        //这里是自定义json解析
        return customParseJson(json);
      });
  }
}

// 使用Dart Data Class Generator插件进行创建  使用命令: Generate from JSON
class AppInfo {
  final bool isForce;
  final bool hasUpdate;
  final bool isIgnorable;
  final int versionCode;
  final String versionName;
  final String updateLog;
  final String apkUrl;
  final int apkSize;

  AppInfo({
    this.isForce,
    this.hasUpdate,
    this.isIgnorable,
    this.versionCode,
    this.versionName,
    this.updateLog,
    this.apkUrl,
    this.apkSize,
  });

  Map<String, dynamic> toMap() {
    return {
      'isForce': isForce,
      'hasUpdate': hasUpdate,
      'isIgnorable': isIgnorable,
      'versionCode': versionCode,
      'versionName': versionName,
      'updateLog': updateLog,
      'apkUrl': apkUrl,
      'apkSize': apkSize,
    };
  }

  static AppInfo fromMap(Map<String, dynamic> map) {
    if (map == null) return null;

    return AppInfo(
      isForce: map['isForce'],
      hasUpdate: map['hasUpdate'],
      isIgnorable: map['isIgnorable'],
      versionCode: map['versionCode']?.toInt(),
      versionName: map['versionName'],
      updateLog: map['updateLog'],
      apkUrl: map['apkUrl'],
      apkSize: map['apkSize']?.toInt(),
    );
  }

  String toJson() => json.encode(toMap());

  static AppInfo fromJson(String source) => fromMap(json.decode(source));

  @override
  String toString() {
    return 'AppInfo isForce: $isForce, hasUpdate: $hasUpdate, isIgnorable: $isIgnorable, versionCode: $versionCode, versionName: $versionName, updateLog: $updateLog, apkUrl: $apkUrl, apkSize: $apkSize';
  }
}

3.3 IOS showIOSDialog 方法

  // 跳转 AppStore 更新 iOSUrl APP 在 AppStore 的链接
  Future<void> showIOSDialog() async {
    showDialog<bool>(
      context: context,
      barrierDismissible: false,
      builder: (context) {
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
                padding: const EdgeInsets.fromLTRB(40.0, 0, 40.0, 0),
                child: Image(
                  width: MediaQuery.of(context).size.width,
                  image: AssetImage("images/bg_update_top.png"),
                  fit: BoxFit.fill,
                )),
            Container(
                width: MediaQuery.of(context).size.width,
                margin: const EdgeInsets.fromLTRB(40.0, 0, 40.0, 0),
                decoration: new BoxDecoration(
                    color: Color(0xffffffff),
                    borderRadius: BorderRadius.only(
                      bottomLeft: Radius.circular(10),
                      bottomRight: Radius.circular(10),
                    )),
                child: Container(
                    padding: const EdgeInsets.fromLTRB(20.0, 0, 20.0, 0.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Padding(
                          padding:
                              const EdgeInsets.only(bottom: 8.0, top: 10.0),
                          child: Text(
                            '是否升级到${versionData["versionName"]}版本',
                            style: TextStyle(
                              fontSize: 14.0,
                              color: Color(0xff555555),
                              decoration: TextDecoration.none,
                            ),
                            textAlign: TextAlign.left,
                          ),
                        ),
                        Padding(
                          padding: const EdgeInsets.only(bottom: 15.0),
                          child: Text(
                            '新版本大小: ${versionData["apkSize"]}MB',
                            style: TextStyle(
                              fontSize: 13.0,
                              color: Color(0xff777777),
                              decoration: TextDecoration.none,
                            ),
                            textAlign: TextAlign.left,
                          ),
                        ),
                        Container(
                            height: 110.0,
                            child: ListView(
                                itemExtent: null,
                                shrinkWrap: true,
                                children: _getData())),
                        Container(
                          margin: const EdgeInsets.only(bottom: 6.0),
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: <Widget>[
                              FlatButton(
                                child: Text(
                                  "下次再说",
                                  style: TextStyle(
                                      color: Color(0xffffbb5b), fontSize: 18.0),
                                ),
                                onPressed: () =>
                                    Navigator.of(context).pop(), //关闭对话框
                              ),
                              FlatButton(
                                child: Text(
                                  "立即前往",
                                  style: TextStyle(
                                      color: Color(0xffffbb5b), fontSize: 18.0),
                                ),
                                onPressed: () async {
                                  if (await canLaunch(iOSUrl)) {
                                    await launch(iOSUrl);
                                  }
                                }, //关闭对话框
                              ),
                            ],
                          ),
                        )
                      ],
                    ))),
          ],
        );
      },
    );
  }

四、 效果图

android:

ios: