# 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)