Flutter 应用内自动升级更新(包含Android8.0安装权限处理,下载安装包的断点续传)

记录点滴,提升自我。

文章主要记录自己开发中遇到的问题以及解决办法,有同样问题或者需求的仅供参考

1,引用插件(尽量与本文中的一致)

#网络加载
dio: ^4.0.0
#系统应用版本 信息 
package_info: ^2.0.2
#打开文件 
open_file: ^3.2.1
#文件存储路径 
path_provider: any
#权限平台 
permission_handler: any
#progress_dialog弹窗 
progress_dialog: ^1.2.4
#网页链接跳转 
url_launcher: ^4.0.1

2.在AndroidManifest中添加权限以及provider(可以点击AS中的File-->Open-->选中你的项目下的android-->点击OK-->选择 New Window,在新打开的AS界面中编写Android原生的代码,如下图)

(1)添加如下权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 这个权限用于app安装 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
(2)在<application>级别下添加
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true"
    tools:replace="android:authorities">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/filepaths"
        tools:replace="android:resource" />
</provider>
(3)在res目录下新建xml文件夹,在xml中新建filepaths.xm.
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:tools="http://schemas.android.com/tools"
    tools:ignore="ResourceName">
    <root-path
        name="root_path"
        path="." />
</paths>

3.创建升级更新工具类,并在使用处调用

UpdateAppUtils.loadUpdateApp(context, false);

class UpdateAppUtils {
//是否更新app,isShow用于判断是否显示当前是最新版本,无需更新(一般用于个人中心--检查更新)
static loadUpdateApp(BuildContext context, bool isShow) async {
    //判断平台,如果是Android走以下的自动更新。IOS跳转到appstore
     if (Platform.isAndroid) {
        updataForAndroid();
    }

     if (Platform.isIOS) {
      final url =
          "https://itunes.apple.com/cn/app/idxxxxxx"; // id 后面的数字换成自己的应用 id 就行了
      if (await canLaunch(url)) {
        await launch(url, forceSafariVC: false);
      } else {
        throw 'Could not launch $url';
      }
    }
}
}

(1)在updataForAndroid中(Utils,FileUtils工具类会在下方统一贴出)

//设置Flutter与原生交互的插件
static const platform =const MethodChannel('com.xxx.xxx.xxx/plugin');
static updataForAndroid() async{
//获取当前应用版本code
 String versionCode = await Utils.getPackageInfoVersionCode();

//检查相应权限
 bool approve = await checkPermission();

 if (approve) {//权限通过,接口请求获取服务器版本信息
 HttpGo.getInstance().get(PathUtils.getAppUpdateUrl, (result, msg) {
          AppUpdate update = AppUpdate.fromJson(result);

          if (update != null) {
            //服务器版本大于本地版本 需要弹出更新
            if (update.versionCode! > int.parse(versionCode)) {
              List<ClientUpdatePatches>? list = update.clientUpdatePatches;

              if (list != null && list.isNotEmpty) {
                //弹窗显示更新信息
                Utils.showUpdateDialog(context, () {
                   //立即更新点击效果
                    //获取创建新安装包存储地址
                  FileUtils.savePath().then((value) {
                    if (value != null) {
                      //判断新的安装包是否存在且完整
                      var savePath =
                          value + '/' + update.versionName.toString() + '.apk';
                       //服务器返回的安装包MD5值,用于判断安装目录下安装包的完整性
                      var sign = list[0].sign;
                       //判断安装目录下,安装文件是否存在
                      FileUtils.FileIsExisted(savePath).then((value) async {
                        if (value != null) {
                          if (value as bool) {
                            //存在,判断文件完整性
                            bool reslut =
                                await _getFileComplete(savePath, sign!);
                            if (reslut) {
                              //完整,直接安装
                              install(context, savePath);
                            } else { 
                              //不完整,重新下载或继续下载
                              downloadApk(
                                  context, list[0].downloadUrl!, savePath);
                            }
                          } else {
                            //不存在,去下载

                            downloadApk(
                                context, list[0].downloadUrl!, savePath);
                          }
                        }
                      });
                    }
                  });
                },
                    update.versionName.toString(),
                    update.updateContent.toString(),
                    update.constraintType.toString());
              }
            } else {
              if (isShow) {
                Utils.showToast(context, '已是最新版本,无需更新');
              }
            }
          }
        });
   }
}

判断文件存储权限

 //获取权限
  static Future<bool> checkPermission() async {
    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
    if (Platform.isAndroid) {
      final status = await Permission.storage.status;
      if (status != PermissionStatus.granted) {
        final result = await Permission.storage.request();
        if (result == PermissionStatus.granted) {
          return true;
        }
      } else {
        return true;
      }
    } else {
      return true;
    }
    return false;
  }

判断文件完整性(通过与原生的交互,利用后台返回的MD5签名,对比下载的安装包的MD5签名,如果一样则是完整的。这样也可以用于校验安装包的安全性)Android原生代码会在后面统一贴出

  //判断文件完整性
  Future<bool> _getFileComplete(String savePath, String sign) async {
    final bool file_result = await platform.invokeMethod(
        'checkFileComplete', {"savePath": savePath, "sign": sign});
    if (file_result) {
      return true;
    } else {
      return false;
    }
  }

安装APK

  static install(BuildContext context, String savePath) async {
    //TODO 弹出提示安装框
    Utils.showEnsureCancelDialog(context, "安装包已下载完成,请点击确认安装",() async {
      //判断是否有安卓8.0安装权限
      bool reslut = await _getInstallPermission();
      if (reslut) {
        //有权限直接安装
        OpenFile.open(savePath).then((value) {
          print(value);
        });
      } else {
        //没有权限申请权限
        bool reslut = await _postInstallPermission();
        if (reslut) {
          //申请通过,开始安装
          OpenFile.open(savePath).then((value) {
            print(value);
          });
        } else {
          //没有通过。提醒,关闭弹窗
          Utils.showToast(context, '安装权限未开启,请开启相关权限');
        }
      }
    });
  }

//判断是否允许应用内安装权限
  static Future<bool> _getInstallPermission() async {
    final bool Permission_result =
        await platform.invokeMethod('checkInstallPermission');
    if (Permission_result) {
      return true;
    } else {
      return false;
    }
  }

  //判断允许应用内安装权限的开启与否
  static Future<bool> _postInstallPermission() async {
    final bool result = await platform.invokeMethod('setInstallPermission');
    if (result) {
      return true;
    } else {
      return false;
    }
  }

下载APK


  //下载APK
  static downloadApk(
      BuildContext context, String downloadUrl, String savePath) async {
    var pr = new ProgressDialog(
      context,
      type: ProgressDialogType.Download,
      isDismissible: false,
      showLogs: true,
    );
    pr.style(message: '准备下载...');
    if (!pr.isShowing()) {
      pr.show();
    }
    await DownLoadManage.download(
      url: downloadUrl,
      savePath: savePath,
      onReceiveProgress: (received, total) {
        if (total != -1) {
          ///当前下载的百分比例
          print((received / total * 100).toStringAsFixed(0) + "%");
          var s = (received / total * 100).toStringAsFixed(0);
          pr.update(progress: double.parse(s.toString()), message: "下载中,请稍后…");
        }
      },
      done: () {
        print("下载完成");
        if (pr.isShowing()) {
          pr.hide();
        }
        install(context, savePath);
      },
      failed: (e) {
        if (pr.isShowing()) {
          pr.hide();
        }
        Utils.showToast(context, '更新下载失败,请重试');
        print("下载1失败:" + e.toString());
      },
    );
  }

文件下载downloadmanage

/*
 * 文件下载
 */
class DownLoadManage {
  //用于记录正在下载的url,避免重复下载(每次启动都是新建的队列,起不到记录作用,所以作罢)
  // var downloadingUrls = new Map<String, CancelToken>();
  /// 断点下载大文件
  static Future<void> download({
    required String url,
    required String savePath,
    ProgressCallback? onReceiveProgress,
    void Function()? done,
    void Function(Exception)? failed,
  }) async {
    CancelToken cancelToken= CancelToken();
    int downloadStart = 0;
      // if(downloadingUrls.containsKey(url)){//判断下载任务是否在下载队列,如果存在说明下载过程中断,从断点处继续下载。
      //   print("此链接已在下载队列,断点续传");
        //判断需要下载的文件在本地是否存在
        File f = File(savePath);
        if (await f.exists()as bool) {
          print("下载文件存在,断点续传");
          //将本地文件的大小作为下载任务的开头,断点续传上
          downloadStart = f.lengthSync();
        }else{
          //文件不存在了,就重新下载
          print("下载文件不存在,重新下载");
          downloadStart = 0;
        }
      // }else{//没在下载队列表明此为新的下载任务,从头开始
      //   print("此链接没有在下载队列,添加进去,从头下载");
      //   downloadStart = 0;
      //   //并将下载任务加入到下载队列,并对应相应的取消令牌
      //   downloadingUrls[url] = cancelToken;
      // }
    print("开始位置:$downloadStart");
    var dio = Dio();
    try {
      var response = await dio.get<ResponseBody>(
        url,
        options: Options(
          /// 以流的方式接收响应数据
          responseType: ResponseType.stream,
          followRedirects: false,
          headers: {
            /// 分段下载重点位置
            "range": "bytes=$downloadStart-",
          },
        ),
      );
      print("下载接口返回结果:$response");
      File file = File(savePath);
      RandomAccessFile raf = file.openSync(mode: FileMode.append);
      int received = downloadStart;
      int total = await _getContentLength(response);
      Stream<Uint8List> stream = response.data!.stream;
      StreamSubscription<Uint8List>? subscription;
      subscription = stream.listen(
        (data) {
          /// 写入文件必须同步
          raf.writeFromSync(data);
          received += data.length;
          onReceiveProgress?.call(received, total);
        },
        onDone: () async {
          //下载完毕,关闭文件流,并将下载任务移除
          print("文件下载完成了");
          await raf.close();
          // downloadingUrls.remove(url);
          done?.call();
        },
        onError: (e) async {
          //下载失败,关闭文件流
          print("文件下载失败了");
          await raf.close();
          // downloadingUrls.remove(url);
          failed?.call(e);
        },
        cancelOnError: true,
      );
      cancelToken.whenCancel.then((_) async {
        print("下载中断了");
        await subscription?.cancel();
        await raf.close();
      });
    } on DioError catch (error) {
      print("下载出问题了");
      /// 请求已发出,服务器用状态代码响应它不在200的范围内
      if (CancelToken.isCancel(error)) {
        print("下载取消");
      } else {
        failed?.call(error);
      }
      //下载任务没有响应就从队列中移除
      // downloadingUrls.remove(url);
    }
  }

  /// 获取下载的文件大小
  static Future<int> _getContentLength(Response<ResponseBody> response) async {
    try {
      var headerContent =
          response.headers.value(HttpHeaders.contentRangeHeader);
      print("下载文件$headerContent");
      if (headerContent != null) {
        return int.parse(headerContent.split('/').last);
      } else {
        return 0;
      }
    } catch (e) {
      return 0;
    }
  }
}

flieUtils工具类

class FileUtils {
  // 获取存储路径
  static Future<String> findLocalPath() async {
    // 因为Apple没有外置存储,所以第一步我们需要先对所在平台进行判断
    // 如果是android,使用getExternalStorageDirectory
    // 如果是iOS,使用getApplicationSupportDirectory

    final directory = Platform.isAndroid
        ? await getExternalStorageDirectory()
        : await getApplicationSupportDirectory();
    return directory.path;
  }

  // 获取app存储路径
  static Future<String> savePath() async {
    // 获取存储路径
    var _localPath =
        (await findLocalPath()) + Platform.pathSeparator + 'Download';

    final savedDir = Directory(_localPath);

    // 判断下载路径是否存在
    bool hasExisted = await savedDir.exists();
    // 不存在就新建路径
    if (!hasExisted) {
      savedDir.create();
    }

    return _localPath;
  }

  //判断文件是否存在
  static Future<bool> FileIsExisted(String path) async {
    File file = File(path);
    var dir_bool = await file.exists(); //返回真假
    return dir_bool;
  }

}

utils工具类

class Utils {
  //获取系统 版本号
  static Future<String> getPackageInfoVersionCode() async {
    final PackageInfo info = await PackageInfo.fromPlatform();
    return info.buildNumber;
  }


  //升级app 弹窗  // 是否强制更新 1是 2否
  static Future showUpdateDialog(BuildContext context,Function ensure,String versionName,String updateContent, String constraintType) async {
    var result = await showDialog(
        barrierDismissible: false,
        context: context,
        builder: (BuildContext context) {
          return WillPopScope(
            onWillPop: () async => !(constraintType=='1'),
            child:UpdateDialog(ensure,versionName,updateContent,constraintType),
          );
        });
    return result;
  }


  //toast 信息
  static void showToast(BuildContext context, String msg) {
    FToast fToast = new FToast();
    fToast.init(context);

    fToast.showToast(
      child: Container(
        padding: EdgeInsets.fromLTRB(15.w, 13.h, 15.w, 13.h),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.r),
          color: MyColors.FF333B49,
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Expanded(
              child: Text(
                msg,
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 16.sp, color: MyColors.FFFFFFFF),
              ),
            ),
          ],
        ),
      ),
      gravity: ToastGravity.CENTER,
      toastDuration: Duration(seconds: 2),
    );
  }
}

Android原生代码

private val INSTALL_PERMISSION = "com.xxx.xxx.xxx/plugin" 

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        //各种Android权限的交互
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALL_PERMISSION)
                .setMethodCallHandler(
                MethodChannel.MethodCallHandler { call, result ->
                    run {
                        Log.i("kotlin 收到 flutter 请求:", "${call.method}");
                            //检查是否具有安装安装权限
                        if (call.method.contentEquals("checkInstallPermission")) {
                            result.success(canRequestPackageInstalls(activity));
                            //跳转设置应用安装权限
                        } else if (call.method.contentEquals("setInstallPermission")) {
                            RequestPackageInstalls(activity, result);
                            //检查文件完整性
                        } else if (call.method.contentEquals("checkFileComplete")) {
                            val path = call.argument<String>("savePath");
                            val sign = call.argument<String>("sign");
                            Log.i("传递过来的文件签名:", sign.toString())
                            if (path.toString().isNotEmpty()) {
                                Log.i("传递过来的文件路径不为空,继续检查", "")
                                FileIsComplete(path.toString(), result, activity, sign.toString())
                            } else {
                                Log.i("传递过来的文件路径为空直接返回", "")
                                result.success(false);
                            };
                        }
                    }
                }
        )
    }



    //检测是否有安装权限
    private fun canRequestPackageInstalls(activity: Activity): Boolean {
        return Build.VERSION.SDK_INT <= Build.VERSION_CODES.O || activity.packageManager.canRequestPackageInstalls()
    }

    //检测是否有安装权限
    private fun RequestPackageInstalls(activity: Activity, result: MethodChannel.Result) {
        //注意这个是8.0新API
        val packageURI: Uri = Uri.parse("package:$packageName")
        val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI)
        activity.startActivityForResult(intent, 10086)
        this.setOnPermissionChangeListener(object : OnPermissionChangeListener {
            override fun onChange(groupPosition: Boolean) {
                result.success(groupPosition)
            }
        })

    }

    //通过MD5值检查文件是否完整
    private fun FileIsComplete(savePath: String, result: MethodChannel.Result,
                               activity: Activity, sign: String) {
        Log.i("进来检查方法了", "")
        Thread {
            try {
                var msgDigest: MessageDigest? = null
                msgDigest = MessageDigest.getInstance("MD5")
                var bytes: ByteArray = ByteArray(1024)
                var byteCount: Int = 0;
                val fis = FileInputStream(File(savePath))
                while (fis.read(bytes).also({ byteCount = it }) > 0) {
                    msgDigest.update(bytes, 0, byteCount)
                }
                val bi = BigInteger(1, msgDigest.digest())
                val sha: String = bi.toString(16)
                fis.close()
                Log.i("文件的MD5值:", sha);
//                if ("74:2B:83:46:8D:74:93:9C:96:42:38:39:04:65:E7:6B:D3:30:71:94".equals(sha)) {
                if (sign.equals(sha)) {
                    Log.i("对比结果:", "true")
                    activity.runOnUiThread(Runnable {
                        result.success(true);
                    })

                } else {
                    Log.i("对比结果:", "false")
                    activity.runOnUiThread(Runnable {
                        result.success(false);
                    })

                }
            } catch (e: Exception) {
                Log.i("对比失败:", e.toString());
                activity.runOnUiThread(Runnable {
                    result.success(false);
                })
            }
        }.start()

    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == RESULT_OK) {
            Log.d("开启结果:", "true")
            if (requestCode == 10086) {
                onPermissionChangeListener?.onChange(true)
            } else if (requestCode == 10010) {
                onPermissionChangeListener?.onChange(true)
            }else if(requestCode==10099){
                onPermissionChangeListener?.onChange(true)
            }
        } else {
            Log.d("开启结果:", "false")
            if (requestCode == 10086) {
                onPermissionChangeListener?.onChange(false)
            } else if (requestCode == 10010) {
                onPermissionChangeListener?.onChange(false)
            }else if(requestCode==10099){
                onPermissionChangeListener?.onChange(false)
            }

        }
    }

    //设置各种点击事件
    private var onPermissionChangeListener: OnPermissionChangeListener? = null

    fun setOnPermissionChangeListener(onPermissionChangeListener: OnPermissionChangeListener?) {
        this.onPermissionChangeListener = onPermissionChangeListener
    }

    interface OnPermissionChangeListener {
        fun onChange(groupPosition: Boolean)
    }

Flutter 提供了一种方便的方式来实现应用更新,使得应用程序在用户不需要去应用商店下载新版本时,可以直接在应用内部更新。 实现应用更新的一种常见方式是使用 "flutter_updater" 插件。该插件允许开发者将应用的新版本发布在服务器上,并在应用自动检查新版本的可用性。 以下是实现应用更新的大致步骤: 1. 集成 "flutter_updater" 插件:在项目的 `pubspec.yaml` 文件中添加 "flutter_updater" 的依赖。然后执行 `flutter packages get` 命令来下载插件。 2. 在应用启动时检查更新:在应用的入口函数中调用 `checkVersion()` 方法,该方法会异步检查服务器上是否有新版本可用。 3. 解析版本信息:根据服务器返回的版本信息,与当前应用的版本进行比较。如果有新版本可用,则显示更新提示。 4. 下载安装新版本:用户点击更新提示后,使用 "flutter_updater" 插件中的下载安装方法来下载新版本的应用安装包,并自动安装新版本。 5. 强制用户更新:为了确保用户不会继续使用旧版本,可以在新版本安装完成后,使用 "flutter_updater" 插件中的 `launchApp()` 方法来重启应用。 需要注意的是,为了保证用户数据的安全性和应用的稳定性,更新流程应该经过充分的测试和验证。另外,为了提高用户体验,可以在应用设置中提供选项,允许用户自定义更新检查的频率。 总结起来,Flutter 提供了 "flutter_updater" 插件来实现应用更新,使得用户可以在应用内部直接获取和安装新版本。这为开发者和用户都带来了极大的便利。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值