记录点滴,提升自我。
文章主要记录自己开发中遇到的问题以及解决办法,有同样问题或者需求的仅供参考
1,引用插件(尽量与本文中的一致)
#网络加载 dio: ^4.0.0 #系统应用版本 信息 package_info: ^2.0.2 #打开文件 open_file: ^3.2.1 #文件存储路径 path_provider: any #权限平台 permission_handler: any #progress_dialogd窗 progress_dialog: ^1.2.4 #网页链接跳转 url_launcher: ^4.0.1
2.在AndroidManifest中添加权限以及provider(可以点击AS中的File-->Open-->选中你的项目下的android-->点击OK-->选择 New Window,在新打开的AS界面中编写Android原生的代码,如下图)
(1)添加如下权限
(2)在级别下添加
(3)在res目录下新建xml文件夹,在xml中新建filepaths.xm.
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) { //服务器版本大于本地版本 需要d出更新 if (update.versionCode! > int.parse(versionCode)) { List? list = update.clientUpdatePatches; if (list != null && list.isNotEmpty) { //d窗显示更新信息 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 FuturecheckPermission() 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_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 d出提示安装框 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 { //没有通过。提醒,关闭d窗 Utils.showToast(context, '安装权限未开启,请开启相关权限'); } } }); } //判断是否允许应用内安装权限 static Future_getInstallPermission() async { final bool Permission_result = await platform.invokeMethod('checkInstallPermission'); if (Permission_result) { return true; } else { return false; } } //判断允许应用内安装权限的开启与否 static Future _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(); /// 断点下载大文件 static Future 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 ( 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 stream = response.data!.stream; StreamSubscription ? 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 _getContentLength(Response 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 FuturefindLocalPath() async { // 因为Apple没有外置存储,所以第一步我们需要先对所在平台进行判断 // 如果是android,使用getExternalStorageDirectory // 如果是iOS,使用getApplicationSupportDirectory final directory = Platform.isAndroid ? await getExternalStorageDirectory() : await getApplicationSupportDirectory(); return directory.path; } // 获取app存储路径 static Future 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 FileIsExisted(String path) async { File file = File(path); var dir_bool = await file.exists(); //返回真假 return dir_bool; } }
utils工具类
class Utils { //获取系统 版本号 static FuturegetPackageInfoVersionCode() async { final PackageInfo info = await PackageInfo.fromPlatform(); return info.buildNumber; } //升级app d窗 // 是否强制更新 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("savePath"); val sign = call.argument ("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) }
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)