# Flutter手册 ## 简介 本文档主要介绍加加移动服务平台Flutter 插件的开发和集成,适用于有一定Flutter基础的开发人员。 * 插件开发思路 Android/iOS开发好原生SDK后发布到远程仓库github/maven/jcenter,插件原生部分依赖SDK,如此精简代码,便于维护 想看SDK实现思路,可查看Android SDK * SDK文件结构 ``` lib ├── aj_flutter_appsp.dart // 定义MethodChannel和与原生交互的方法,比如初始化、获取版本信息 ├── aj_flutter_appsp_lib.dart // dart文件统一管理,归整为library aj_flutter_appsp; ├── sp_notice_model_item.dart // 每条公告的信息 ├── sp_resp_notice_model.dart // 公告数据 ├── sp_resp_update_model.dart // 版本数据 ``` * 支持版本说明 > * Flutter SDK >= 1.5.4 > * Android Studio 3.0+ / Xcode 11+ > * JDK 1.8 / Swift 5.0+ ## 插件集成 ### host配置 ``` 针对Windows环境,C:\Windows\System32\drivers\etc 的host文件加入 185.199.108.133 raw.githubusercontent.com 针对Mac环境,在/etc/hosts下加入 ``` ### 依赖 在工程/pubspec.yaml中,加入依赖: 目前依赖的是anji.sdk.appsp:aar:0.0.2(之前版本是0.0.1) ``` aj_flutter_appsp: git: url: https://gitee.com/anji-plus/aj_flutter_plugins.git path: aj_flutter_appsp ``` ### 获取插件 终端输入 ``` flutter packages get ``` 或者点击右上角Packages get ### 引用 ```java import 'package:aj_flutter_appsp/aj_flutter_appsp_lib.dart'; ``` ### 调用 详细使用请参考插件的example * 初始化 在main.dart加入 ```java @override void initState() { super.initState(); _initAppSp(); } _initAppSp() async { //初始化 var debuggable = !bool.fromEnvironment("dart.vm.product"); await AjFlutterAppSp.init( //appKey,创建应用时生成的,作为和服务端通信的标识 appKey: "860dc3bd5faa489bb7ab6d087f8ee6e5", //请求的基础地址 host: "https://openappsp.anji-plus.com", //是否打开debug开关,非生产默认打开 debug: debuggable); } ``` * 版本信息获取 ```java import 'package:aj_flutter_appsp/aj_flutter_appsp_lib.dart'; _update() async { //关键代码 SpRespUpdateModel updateModel = await AjFlutterAppSp.getUpdateModel(); if (!mounted) { return; } if (updateModel == null) { Scaffold.of(context).showSnackBar( SnackBar(content: Text("没有更新信息")), ); return; } } ``` * 公告信息获取 ```java import 'package:aj_flutter_appsp/aj_flutter_appsp_lib.dart'; _requestNoticeType() async { //无需改造数据,用服务器返回数据,下面的都是模拟的数据 //ignore SpRespNoticeModel noticeModel = await AjFlutterAppSp.getNoticeModel(); if (!mounted) { return; } if (noticeModel == null || noticeModel.repData == null || noticeModel.repData.isEmpty) { var snackBar = SnackBar(content: Text("没有公告信息")); _scaffoldkey.currentState.showSnackBar(snackBar); return; } ``` * 版本更新Android配置 关于版本更新的弹出和下载更新,需要注意: 1, android目录下,AndroidManifest.xml中加入权限,其中包括网络访问、文件访问、apk安装的权限 ```java ``` 2, android目录下,AndroidManifest.xml中添加provider,方便Android7.0+版本升级 ```java ``` `authorities`的格式是 包名.fileprovider 其中,file_paths.xml若没有则创建,在xml目录下,file_paths.xml内容如下: ```java ``` 3,针对http,尤其在测试阶段,AndroidManifest.xml要注意加上networkSecurityConfig和usesCleartextTraffic,改成 ``` ``` 4,记得用embedding V2 ``` ``` 关于版本更新的弹出和下载更新,以及公告的展示,请参考我们的Flutter插件,附有Example ## 插件实现 ### 初始化 接口路径:https://openappsp.anji-plus.com/sp/phone/deviceInit * aj_flutter_appsp.dart实现 ```java static const MethodChannel _channel = const MethodChannel('aj_flutter_appsp'); ///设置基础地址,如果在开发测试场景会用到,生产时候记得改成生产地址,或者最好不要对暴露set方法 ///[appKey] 应用唯一标识 ///[host] 设置请求基础地址 ///[debug] 是否打开日志开关,true为打开 static Future init( {String appKey = '', String host = '', bool debug = true}) async { final String result = await _channel .invokeMethod('init', {"appKey": appKey, "host": host, "debug": debug}); return result; } ``` ### 版本更新 接口路径:https://openappsp.anji-plus.com/sp/phone/appVersion * aj_flutter_appsp.dart实现 ```java import 'aj_flutter_appsp_lib.dart'; import 'dart:async'; import 'dart:convert'; import 'package:flutter/services.dart'; ///获取版本信息 static Future getUpdateModel() async { final String jsonStr = await _channel.invokeMethod('getUpdateModel'); SpRespUpdateModel updateModel = SpRespUpdateModel.fromJson(json.decode(jsonStr)); return updateModel; } ``` **请求返回数据结构如下** *** **SpRespUpdateModel** 数据详情 ```java { "repCode": "0000", //业务返回码,0000表示成功 "repMsg": "成功", //业务日志 "repData": { "downloadUrl": "app下载地址", "mustUpdate": false, //是否强制更新,true为强制更新 "showUpdate": true, //是否允许弹出更新 "updateLog": "更新日志" } } ``` | 字段| 类型| 说明 | | :-----| :---- | :---- | | repCode |String | 业务返回码,0000表示成功 | | repMsg |String | 业务日志、异常信息 | | repData |Object | 请求业务数据包、详情见下 | **repData** 数据详情 | 字段| 类型| 说明 | | :-----| :---- | :---- | | downloadUrl |String | app下载地址 | | mustUpdate | boolean | 是否强制更新,true为强制更新; false为非强制更新 | | showUpdate | boolean | 是否提示更新:允许弹出更新 | | updateLog |String | 更新日志 | *** * 其他方法 ``` ///下载apk并且安装 ///返回 ”UrlFormatError“ =》url格式错误 ///返回 ”notSamePackage“ =》不是统一包名 ///返回 ”downloadApkFailed“ =》apk下载失败 Future downloadApkAndInstall(String url, String path) async { final String result = await _channel .invokeMethod('downloadApkAndInstall', {"url": url, "path": path}); return result; } ///apk安装 ///返回 ”FilePathFormatError“ =》文件格式错误 Future installApk(String path) async { final String result = await _channel.invokeMethod('installApk', {"path": path}); return result; } ///apk下载取消 Future cancelDownload() async { final String result = await _channel.invokeMethod('cancelDownload'); return result; } ///是否同一包名 Future checkSamePackage(String path) async { final String result = await _channel.invokeMethod('checkSamePackage', {"path": path}); return result; } ``` * 我们在插件实现文件下载的原因是, 用Dio插件下载文件默认可以dio.download(...),但是如果返回的不是静态资源而是文件流,那么这个下载是无效的, 但是用原生的HttpURLConnection下载是没问题的,为了做兼容,写在了插件。 * 相比之前版本,插件实现了几个功能: 1,避免重复下载,当下载完安装取消,再次安装无需下载 2,校验是否是APK文件,不是则弹出错误 3,校验是否同一包名,不同则报错 4,当点击取消按钮或者返回,下载任务取消 * 文件下载关键逻辑 ``` // 获取本地文档目录 String dir = (await getExternalStorageDirectory()).path; //保证唯一 file = new File('$dir/AJ_' + new DateTime.now().millisecondsSinceEpoch.toString() + '.apk'); String errorMsg = ''; String result = await appspPlugin.downloadApk(apkUrl, file.path); print('downloadApkAndInstall result $result'); if ("UrlFormatError" == result) { errorMsg = 'url格式错误'; } else if ("notSamePackage" == result) { errorMsg = '需下载的包名不一致'; } else if ("downloadApkFailed" == result) { errorMsg = 'apk下载失败'; } else if ("notApk" == result) { errorMsg = '非APK文件'; } if (errorMsg.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(errorMsg)), ); } ``` * 因为进度条是在flutter层,文件下载进度监听 ``` try { appspPlugin.getApkDownloadProgressCallback((result) { setState(() { widget.rate = result; if (result >= 1) { setState(() { updateButtonEnable = true; }); _cacheApkPath(); } }); }); } on PlatformException { print('PlatformException'); } ``` * flutter主动触发apk安装 ``` _pushAndInstall() async { try { String result = await appspPlugin.installApk(_fileMap['path']); if ("FilePathFormatError" == result) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("文件格式错误")), ); } } on PlatformException catch (e) { print("dart -PlatformException "); } finally {} } ``` * apk安装的原生逻辑 ``` //安装apk private void installApk(String path) { Intent intent = new Intent(Intent.ACTION_VIEW); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Uri contentUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", new File(path)); intent.setDataAndType(contentUri, "application/vnd.android.package-archive"); } else { intent.setDataAndType(Uri.fromFile(new File(path)), "application/vnd.android.package-archive"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } MethodResultWrapper wrapper = peekWraper(); if (wrapper != null) { wrapper.success("installApkSuccess"); } if (context != null) { context.startActivity(intent); } } ``` ### 获取公告 接口路径:https://openappsp.anji-plus.com/sp/phone/appNotice * aj_flutter_appsp.dart实现 ```java import 'aj_flutter_appsp_lib.dart'; import 'dart:async'; import 'dart:convert'; import 'package:flutter/services.dart'; ///获取公告信息 static Future getNoticeModel() async { final String jsonStr = await _channel.invokeMethod('getNoticeModel'); SpRespNoticeModel noticeModel = SpRespNoticeModel.fromJson(json.decode(jsonStr)); return noticeModel; } ``` **请求返回数据结构如下** *** **SpRespNoticeModel** 数据详情 ```java { "repCode": "0000", //业务返回码,0000表示成功 "repMsg": "成功", //业务日志 "repData": { "title": "公告标题", "details": "公告内容", "templateType": "dialog", //公告类型( 弹窗:dialog; 水平滚动:horizontal_scroll) "templateTypeName": "公告"//公告模板名称 } } ``` | 字段| 类型| 说明 | | :-----| :---- | :---- | | repCode |String | 业务返回码,0000表示成功 | | repMsg |String | 业务日志、异常信息 | | repData |Object | 请求业务数据包、详情见下 | **repData** 数据详情 | 字段| 类型| 说明 | | :-----| :---- | :---- | | title |String | 公告标题 | | details |String | 公告内容 | | templateType |String | 公告类型( 弹窗:dialog; 水平滚动:horizontal_scroll)| | templateTypeName |String | 公告模板名称 | *** ### Android Plugin实现 在AjFlutterAppspPlugin.java ```java //为了解决并发问题,比如多次点击,异常情况时候容易出问题 private ConcurrentLinkedQueue wrappers = new ConcurrentLinkedQueue<>(); @Override public void onMethodCall(MethodCall call, Result result) { removeAllWrapper(); addWraper(new MethodResultWrapper(result)); //初始化 if (call.method.equals("init")) { String appKey = null; String host = null; boolean debug = true; Object parameter = call.arguments(); if (parameter instanceof Map) { appKey = (String) ((Map) parameter).get("appKey"); host = (String) ((Map) parameter).get("host"); debug = (Boolean) ((Map) parameter).get("debug"); init(appKey, host, debug); } //版本请求 } else if (call.method.equals("getUpdateModel")) { checkVersion(); //公告获取 } else if (call.method.equals("getNoticeModel")) { checkNotice(); } else { MethodResultWrapper wrapper = peekWraper(); if (wrapper != null) { wrapper.notImplemented(); } } } /** * 获取当前的result * * @return */ private MethodResultWrapper peekWraper() { if (wrappers == null || wrappers.isEmpty()) { return null; } return wrappers.remove(); } /** * 只考虑最后一次 */ private void removeAllWrapper() { if (wrappers == null) { return; } wrappers.clear(); } /** * 加入唯一的result * * @param wrapper */ private void addWraper(MethodResultWrapper wrapper) { if (wrappers == null) { return; } wrappers.add(wrapper); } ``` * 初始化 ```java /** * 初始化 * * @param appKey 创建应用时生成的,作为和服务端通信的标识 * @param host 如果为空,认为用SDK默认请求地址 * @param debug 日志开关是否打开,默认打开 */ private void init(String appKey, String host, boolean debug) { AppSpConfig.getInstance() .init(registrar.activity(), appKey) //可修改基础请求地址 .setHost(host) //正式环境可以禁止日志输出,通过Tag APP-SP过滤看日志 .setDebuggable(debug) //务必要初始化,否则后面请求会报错 .deviceInit(); MethodResultWrapper wrapper = peekWraper(); if (wrapper != null) { wrapper.success(""); } } ``` * 版本信息获取 ```java /** * 版本更新检查 */ private void checkVersion() { AppSpConfig.getInstance().getVersion(new IAppSpVersionCallback() { @Override public void update(AppSpModel spModel) { AppSpLog.d("Test updateModel is " + spModel); MethodResultWrapper wrapper = peekWraper(); if (spModel == null) { if (wrapper != null) { wrapper.notImplemented(); } } else { //先转成json if (spModel.getRepData() != null) {//有更新数据 if (wrapper != null) { wrapper.success(new Gson().toJson(spModel)); } } else {//无更新数据 AppSpModel tempModel = new AppSpModel<>(); tempModel.setRepCode(spModel.getRepCode()); tempModel.setRepMsg(spModel.getRepMsg()); if (wrapper != null) { wrapper.success(new Gson().toJson(tempModel)); } } } } @Override public void error(String code, String msg) { //无更新数据 MethodResultWrapper wrapper = peekWraper(); AppSpModel spModel = new AppSpModel<>(); spModel.setRepCode(code); spModel.setRepMsg(msg); if (wrapper != null) { wrapper.success(new Gson().toJson(spModel)); } } }); } ``` * 公告信息获取 ```java /** * 公告信息获取 */ private void checkNotice() { AppSpConfig.getInstance().getNotice(new IAppSpNoticeCallback() { @Override public void notice(AppSpModel> noticeModel) { AppSpLog.d("Test noticeModel is " + noticeModel); MethodResultWrapper wrapper = peekWraper(); if (noticeModel == null) { if (wrapper != null) { wrapper.notImplemented(); } } else if (noticeModel.getRepData() != null) {//有公告信息 if (wrapper != null) { wrapper.success(new Gson().toJson(noticeModel)); } } else {//无公告信息 //先转成json AppSpModel tempModel = new AppSpModel<>(); tempModel.setRepCode(noticeModel.getRepCode()); tempModel.setRepMsg(noticeModel.getRepMsg()); if (wrapper != null) { wrapper.success(new Gson().toJson(tempModel)); } } } @Override public void error(String code, String msg) { //无公告信息 AppSpModel noticeModel = new AppSpModel<>(); noticeModel.setRepCode(code); noticeModel.setRepMsg(msg); MethodResultWrapper wrapper = peekWraper(); if (wrapper != null) { wrapper.success(new Gson().toJson(noticeModel)); } } }); } ``` ### Android 混淆 主工程build.gradle的release打包配置中,加入混淆配置 ```json buildTypes { release { signingConfig signingConfigs.release minifyEnabled false //混淆 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } //测试 debug { signingConfig signingConfigs.debug } } ``` 主工程下proguard-rules.pro文件加入 ```java -dontwarn com.anji.appsp.sdk.** -keep class com.anji.appsp.sdk.**{*;} ``` ### iOS Plugin实现 在AjFlutterAppspPlugin.m中 * 版本初始化 ```swift if ([call.method isEqualToString: @"init"]){ NSString *appKey = call.arguments[@"appKey"]; NSString *host = call.arguments[@"host"]; NSString *debug = call.arguments[@"debug"]; if (host != nil && host.length > 0) { [[AppSpService shareService] initConfigWithAppkey:appKey debug:debug :host]; } else { [[AppSpService shareService] initConfigWithAppkey:appKey debug:debug :nil]; } } ``` *** * 版本更新 ```swift if ([call.method isEqualToString: @"getUpdateModel"]) { __weak typeof(self) weakSelf = self; [[AppSpService shareService] checkVersionUpdateWithSuccess:^(NSDictionary* repData) { result([weakSelf formateDictToJSonString:repData]); } failure:^(NSDictionary* errorData) { result([weakSelf formateDictToJSonString:errorData]); }]; } ``` * 获取公告信息 ```swift if ([call.method isEqualToString:@"getNoticeModel"]) { __weak typeof(self) weakSelf = self; [[AppSpService shareService] getNoticeInfoWithSuccess:^(NSDictionary* repData) { result([weakSelf formateDictToJSonString:repData]); } failure:^(NSDictionary* errorData) { result([weakSelf formateDictToJSonString:errorData]); }]; } ``` ## Flutter插件下载 地址: [https://gitee.com/anji-plus/appsp/tree/master/sdk/flutter/aj_flutter_appsp](https://gitee.com/anji-plus/appsp/tree/master/sdk/flutter/aj_flutter_appsp)