flutter_xupdate实现Android版本一键更新

2,722 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

XUpdate可以实现一键更新Flutter应用,是一款国产的开源作品,从作者github的记录上看,在app更新方面已经开发了有3、4年的时间了,不但有flutter的产品,还有android版本的更新组件(我想flutter应该也是从android的版本升级过来的吧)并且提供了在线更新的后端服务系统,有兴趣的朋友可以去他的github账号了解一下。(后台版本更新管理服务后台版本更新管理系统

在了解flutter在线更新插件时,在pub上还发现一个国外的产品in_app_update,这个产品的likes、popularity都高于xupdate,看了看文档,发现这个东西在国内可能会水土不服啊,还是来支持国产软件吧。

这个插件只支持android,今天就使用android给大家演示一下。

pub地址:pub.dev/packages/fl…(如果你觉得好用,请给作者一个likes)

github地址:github.com/xuexiangjys…(如果你觉得好用,请给作者一个star)

安装依赖

flutter pub add flutter_xupdate
or
dependencies:
  flutter_xupdate: ^2.0.2

配置需要的周边环境

xupdate需要我们将android主题修改为AppCompat,打开文件:android/app/src/main/res/values/styles.xml,将LaunchTheme主题修改为Theme.AppCompat.Light.NoActionBar

<resources>
    <style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowBackground">@drawable/launch_background</item>
    </style>
</resources>

如果不配置会报这个错误

E/ThemeUtils(11695): View class androidx.appcompat.widget.AppCompatImageView is an AppCompat widget that can only be used with a Theme.AppCompat theme (or descendant).
W/System.err(11695): java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
W/System.err(11695): 	at androidx.appcompat.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:846)
W/System.err(11695): 	at androidx.appcompat.app.AppCompatDelegateImpl.ensureSubDecor(AppCompatDelegateImpl.java:809)
W/System.err(11695): 	at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:687)
W/System.err(11695): 	at androidx.appcompat.app.AppCompatDialog.setContentView(AppCompatDialog.java:100)
W/System.err(11695): 	at com.xuexiang.xupdate.widget.BaseDialog.init(BaseDialog.java:78)
W/System.err(11695): 	at com.xuexiang.xupdate.widget.BaseDialog.init(BaseDialog.java:74)
W/System.err(11695): 	at com.xuexiang.xupdate.widget.BaseDialog.<init>(BaseDialog.java:63)
W/System.err(11695): 	at com.xuexiang.xupdate.widget.BaseDialog.<init>(BaseDialog.java:50)
W/System.err(11695): 	at com.xuexiang.xupdate.widget.UpdateDialog.<init>(UpdateDialog.java:129)
W/System.err(11695): 	at com.xuexiang.xupdate.widget.UpdateDialog.newInstance(UpdateDialog.java:120)
W/System.err(11695): 	at com.xuexiang.xupdate.proxy.impl.DefaultUpdatePrompter.showPrompt(DefaultUpdatePrompter.java:62)
W/System.err(11695): 	at com.xuexiang.xupdate.UpdateManager.findNewVersion(UpdateManager.java:351)
W/System.err(11695): 	at com.xuexiang.xupdate.utils.UpdateUtils.processUpdateEntity(UpdateUtils.java:91)
W/System.err(11695): 	at com.xuexiang.xupdate.proxy.impl.DefaultUpdateChecker.processCheckResult(DefaultUpdateChecker.java:143)
W/System.err(11695): 	at com.xuexiang.xupdate.proxy.impl.DefaultUpdateChecker.onCheckSuccess(DefaultUpdateChecker.java:106)
W/System.err(11695): 	at com.xuexiang.xupdate.proxy.impl.DefaultUpdateChecker.access$000(DefaultUpdateChecker.java:46)
W/System.err(11695): 	at com.xuexiang.xupdate.proxy.impl.DefaultUpdateChecker$1.onSuccess(DefaultUpdateChecker.java:67)
W/System.err(11695): 	at com.xuexiang.flutter_xupdate.OKHttpUpdateHttpService$1.onResponse(OKHttpUpdateHttpService.java:83)
W/System.err(11695): 	at com.xuexiang.flutter_xupdate.OKHttpUpdateHttpService$1.onResponse(OKHttpUpdateHttpService.java:75)
W/System.err(11695): 	at com.zhy.http.okhttp.OkHttpUtils$3.run(OkHttpUtils.java:186)
W/System.err(11695): 	at android.os.Handler.handleCallback(Handler.java:883)
W/System.err(11695): 	at android.os.Handler.dispatchMessage(Handler.java:100)
W/System.err(11695): 	at android.os.Looper.loop(Looper.java:214)
W/System.err(11695): 	at android.app.ActivityThread.main(ActivityThread.java:7386)
W/System.err(11695): 	at java.lang.reflect.Method.invoke(Native Method)
W/System.err(11695): 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
W/System.err(11695): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:980)
I/flutter (11695): {code: 2006, detailMsg: Code:2006, msg:查询失败:解析Json错误!(You need to use a Theme.AppCompat theme (or descendant) with this activity.), message: 查询失败:解析Json错误!(You need to use a Theme.AppCompat theme (or descendant) with this activity.)}

初始化插件

作者的Example对初始化的注释也是很丰富。

//初始化XUpdate
void initXUpdate() {
  if (Platform.isAndroid) {
    FlutterXUpdate.init(
      //是否输出日志
      debug: true,
      //是否使用post请求
      isPost: false,
      //post请求是否是上传json
      isPostJson: false,
      //请求响应超时时间
      timeout: 25000,
      //是否开启自动模式
      isWifiOnly: false,
      //是否开启自动模式
      isAutoMode: false,
      //需要设置的公共参数
      supportSilentInstall: false,
      //在下载过程中,如果点击了取消的话,是否弹出切换下载方式的重试提示弹窗
      enableRetry: false)
      .then((value) {
        print("初始化成功: $value");
      }).catchError((error) {
      print(error);
    });
    
    FlutterXUpdate.setErrorHandler(
      onUpdateError: (Map<String, dynamic>? message) async {
        print(message);
      });
  } else {
    debugPrint("ios暂不支持XUpdate更新");
  }
}

更新应用

xupdate最主要的方法就是checkUpdate,在做好初始化和服务端的处理后,只需要一行代码即可完成在线更新,支持后台更新,静默安装需要有root权限,静默安装就算了,现在Android 的权限控制越来越多,很难做到静默安装了。我们有一些应用和一些设备的小厂合作,在出厂时就内置,这个时候用这个静默安装就很舒服了。

 ///默认App更新
  void checkUpdateDefault() {
    FlutterXUpdate.checkUpdate(url: _updateUrl);
  }

  ///默认App更新 + 支持后台更新
  void checkUpdateSupportBackground() {
    FlutterXUpdate.checkUpdate(url: _updateUrl, supportBackgroundUpdate: true);
  }

  ///自动模式, 如果需要完全无人干预,自动更新,需要root权限【静默安装需要】
  void checkUpdateAutoMode() {
    FlutterXUpdate.checkUpdate(url: _updateUrl, isAutoMode: true);
  }

准备数据

对于在线更新的插件基本流程都是一致的,客户端在向服务器请求获取更新数据,服务端将最新的版本信息返回给客户端(一般都是json格式),客户端分析json文件,查看本地版本是不是已经过时了,如果过时则下载apk并进行安装,如果没有过时则放弃。这里能够扩展的业务有更新内容,是否强制更新等。xupdate也是这样一个过程,作者默认提供了一个json文件的格式包括对应的model,如果有特殊需求也支持自定义json格式。一般的业务默认的json就够用了。

默认的json内容

{
  "Code": 0, //0代表请求成功,非0代表失败
  "Msg": "", //请求出错的信息
  "UpdateStatus": 1, //0代表不更新,1代表有版本更新,不需要强制升级,2代表有版本更新,需要强制升级
  "VersionCode": 3,
  "VersionName": "1.0.2",
  "ModifyContent": "1、优化api接口。\r\n2、添加使用demo演示。\r\n3、新增自定义更新服务API接口。\r\n4、优化更新提示界面。",
  "DownloadUrl": "https://raw.githubusercontent.com/xuexiangjys/XUpdate/master/apk/xupdate_demo_1.0.2.apk",
  "ApkSize": 2048
  "ApkMd5": "..."  //md5值没有的话,就无法保证apk是否完整,每次都会重新下载。框架默认使用的是md5加密。
}

这里有一个apk的md5,作者对md5值的描述是这样的:

这里需要说明的是,这里填写的MD5值是APK文件进行MD5加密后的值,并不是对APK签名的MD5。框架默认使用的是MD5加密,如果你觉得不够安全,也可以使用其他加密方式,不过这可能涉及到原生的编码,详情参见:自定义文件加密校验器.

如果不想使用MD5的话就不需要配置这个字段,不过这样每次检查的话都会去重新下载APK,建议配置。

原来只是文件的md5,并不是apk签名的md5,那我们获取这个md5值就很方便了,我准备了3个办法来获取md5值。我放到文章的后边了,大家翻一下目录去看吧。

这里我我们准备了这样的一个版本描述文件,我随意编译了一个apk的版本,一起放到服务器上了。

{
  "Code": 0, 
  "Msg": "", 
  //0代表不更新,1代表有版本更新,不需要强制升级,2代表有版本更新,需要强制升级
  "UpdateStatus": 1, 
  "VersionCode": 2,
  "VersionName": "2.0.0",
  "ModifyContent": "1、优化api接口。\r\n2、添加使用demo演示。\r\n3、新增自定义更新服务API接口。\r\n4、优化更新提示界面。",
  "DownloadUrl": "https://static-9852f929-d814-4bc5-942a-db0005fdf887.bspapp.com/app-release-1.apk",
  "ApkSize": 40448,
  //md5值没有的话,就无法保证apk是否完整,每次都会重新下载。框架默认使用的是md5加密。
  "ApkMd5": "e2eedae8d50b4454dbf219ea426548a3"  
}

开始更新

final _updateUrl = "http://192.168.2.114:15806/xupdate-2.json";

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('XUpdate演示'),
      ),
      body: Center(
        child: Column(
          children: [
            ElevatedButton(
              onPressed: () {
                FlutterXUpdate.checkUpdate(url: _updateUrl);
              },
              child: const Text('默认更新'),
            ),
            ElevatedButton(
              onPressed: () {
                FlutterXUpdate.checkUpdate(
                  url: _updateUrl,
                  supportBackgroundUpdate: true,
                );
              },
              child: const Text('支持后台更新'),
            ),
          ],
        ),
      ),
    );
  }

\

默认APP更新

\

支持后台更新

启用后台更新

下载完成后,显示安装界面

一切ok,作者在pub主页中的demo还演示了更多的功能,包括调整宽高比,强制更新,切换下载方式,使用自定义json解析,有需要的小伙伴可以自己跑一些demo试试。

文件MD5值计算

java实现(1)

参考xupdate的校验部分,可以自己写一个获取apk文件md5值的方法。

参考文章:github.com/xuexiangjys…

参考源码:github.com/xuexiangjys…

package cn.iocoder.yudao;

import cn.hutool.core.io.FileUtil;
import cn.smallbun.screw.core.util.FileUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.MessageDigest;

/**
* @author Radium
* @date 2022/6/7 3:41 PM
*/
public class Md5Test {
    public static void main(String[] args) {
        File file = new File("<apk文件地址>");
        String fileMD5 = getFileMD5(file);
        System.out.println(fileMD5);
    }
    /**
    * 获取文件的MD5值
    *
    * @param file
    * @return
    */
    public static String getFileMD5(File file) {
        if (!FileUtil.exist(file)) {
            return "";
        }
        try (InputStream fis = new FileInputStream(file)){
            MessageDigest digest = MessageDigest.getInstance("MD5");
            byte[] buffer = new byte[8192];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                digest.update(buffer, 0, len);
            }
            return bytes2Hex(digest.digest());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
    
    /**
    * 一个byte转为2个hex字符
    *
    * @param src byte数组
    * @return 16进制大写字符串
    */
    private static String bytes2Hex(byte[] src) {
        char[] res = new char[src.length << 1];
        final char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        for (int i = 0, j = 0; i < src.length; i++) {
            res[j++] = hexDigits[src[i] >>> 4 & 0x0F];
            res[j++] = hexDigits[src[i] & 0x0F];
        }
        return new String(res);
    }
}

java实现(2)

如果有Hutool工具会更方便一点,一行代码就搞定。

String fileMD5 = MD5.create().digestHex(new File(<apk文件地址>));
System.out.println(fileMD5);

Dart实现

使用Dart获取MD5也很简单。

import 'dart:io';
import 'package:crypto/crypto.dart';

void main() async {
  var file = File(<Apk文件地址>);
  print(md5.convert(file.readAsBytesSync()));
}

\

无法访问http地址

错误提示:CLEARTEXT communication to " " not permitted by network security policy。这个主要是因为Android P以后的网络访问安全策略升级,限制了非加密的流量请求。有3个办法处理。

  1. 减低目标版本,把targetSdkVersion设置到27一下,没有特殊需求的用这个办法就行了。
  2. http请求改成https,对于真正要上线的项目,修改一下https还是很有必要的。
  3. 禁止Android的这个限制。在res/xml/目录下新建个network_security_config.xml的文件,文件名随意只要能和Androidmanifest.xml中配置的文件名一致就行。。
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <base-config cleartextTrafficPermitted="true" />
</network-security-config>

在Androidmanifest.xml中设置networkSecurityConfig只想到上述文件。

查看版本信息

引入插件

$ flutter pub add package_info_plus
  or
dependencies:
  package_info_plus: ^1.4.2

编写获取信息的方法。

void initVersionInfo() async {
    PackageInfo packageInfo = await PackageInfo.fromPlatform();
    appName = packageInfo.appName;
    packageName = packageInfo.packageName;
    version = packageInfo.version;
    buildNumber = packageInfo.buildNumber;
  }

初始化数据

@override
  void initState() {
    super.initState();
    Future.delayed(
      Duration.zero,
      () => setState(
        () {
          initVersionInfo();
        },
      ),
    );
    initXUpdate();
  }

展示数据

Text("appName: $appName"),
Text("packageName: $packageName"),
Text("version: $version"),
Text("buildNumber: $buildNumber"),

版本名称对应关系

packageInfo插件属性local.properties文件属性pubspec.yaml
packageInfo.versionversion“+”前边的内容flutter.versionName
packageInfo.buildNumberversion“+”后边的内容flutter.versionCode

源码地址:gitee.com/radium/flut…

视频地址:space.bilibili.com/1159595523