# 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文件加入

199.232.4.133 raw.githubusercontent.com

针对Mac环境,在/etc/hosts下加入

# 依赖

在工程/pubspec.yaml中,加入依赖:

aj_flutter_appsp:
    git:
      url: https://github.com/anji-plus/aj_flutter_appsp.git
      ref: branch_0.0.1        //可选,用名为branch_0.0.1的branch

其中ref表示版本名,对应仓库的branch,不用则用主分支 ^表示用最新版本,如果指定版本,请忽略此符号

# 获取插件

终端输入

flutter packages get

或者点击右上角Packages get

# 引用

import 'package:aj_flutter_appsp/aj_flutter_appsp_lib.dart';

# 调用

详细使用请参考插件的example

  • 初始化 在main.dart加入
    @override
  void initState() {
    super.initState();
    _initAppSp();
  }

  _initAppSp() async {
    //初始化
    var debuggable = !bool.fromEnvironment("dart.vm.product");
    await AjFlutterAppSp.init(
		//appKey,创建应用时生成的,作为和服务端通信的标识
        appKey: "aadcfae6215a4e0f9bf5bc5edccb1045",
		//请求的基础地址
        host: "http://open-appsp.anji-plus.com",
		//是否打开debug开关,非生产默认打开
        debug: debuggable);
  }

  • 版本信息获取
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;
    }
  }	
  • 公告信息获取
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安装的权限

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

2, android目录下,AndroidManifest.xml中添加provider,方便Android7.0+版本升级

       <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.anji.appsp.sdktest.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

authorities的格式是 包名.fileprovider

其中,file_paths.xml若没有则创建,在xml目录下,file_paths.xml内容如下:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="files_root"
        path="Android/data/包名/" />
    <external-path
        name="external_storage_root"
        path="." />
    <root-path
        name="root_path"
        path="" />

</paths>

我们可以在aj_flutter_appsp.dart加入apk安装的逻辑,然后在插件处理, 这里我是直接放在了工程的MainActivity,插件只关心数据的获取, 业务逻辑由开发自行定制,example仅作参考,不作为行业标准

关于版本更新的弹出和下载更新,以及公告的展示,请参考我们的Flutter插件,附有Example

# 插件实现

# 初始化

接口路径:http://open-appsp.anji-plus.com/sp/phone/deviceInit

  • aj_flutter_appsp.dart实现
  static const MethodChannel _channel = const MethodChannel('aj_flutter_appsp');

  ///设置基础地址,如果在开发测试场景会用到,上线时记得改成生产地址,好不要对暴露setHost方法
  ///[appKey] 应用唯一标识
  ///[host] 设置请求基础地址
  ///[debug] 是否打开日志开关,true为打开
  static Future<String> init(
      {String appKey, String host, bool debug = true}) async {
    final String result = await _channel.invokeMethod(
        'init', {"appKey": appKey, "host": host, "debug": debug});
    return result;
  }

# 版本更新

接口路径:http://open-appsp.anji-plus.com/sp/phone/appVersion

  • aj_flutter_appsp.dart实现
import 'aj_flutter_appsp_lib.dart';

import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';

 ///获取版本信息
  static Future<SpRespUpdateModel> getUpdateModel() async {
    final String jsonStr = await _channel.invokeMethod('getUpdateModel');
    SpRespUpdateModel updateModel =
    SpRespUpdateModel.fromJson(json.decode(jsonStr));
    return updateModel;
  }

请求返回数据结构如下


SpRespUpdateModel 数据详情

 {
 	"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 更新日志

# 获取公告

接口路径:http://open-appsp.anji-plus.com/sp/phone/appNotice

  • aj_flutter_appsp.dart实现
import 'aj_flutter_appsp_lib.dart';

import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';

  ///获取公告信息
  static Future<SpRespNoticeModel> getNoticeModel() async {
    final String jsonStr = await _channel.invokeMethod('getNoticeModel');
    SpRespNoticeModel noticeModel =
    SpRespNoticeModel.fromJson(json.decode(jsonStr));
    return noticeModel;
  }

请求返回数据结构如下


SpRespNoticeModel 数据详情

 {
 	"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

	//为了解决并发问题,比如多次点击,异常情况时候容易出问题
    private ConcurrentLinkedQueue<MethodResultWrapper> 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);
    }

  • 初始化
	/**
     * 初始化
     *
     * @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("");
        }
    }
  • 版本信息获取
	/**
     * 版本更新检查
     */
    private void checkVersion() {
        AppSpConfig.getInstance().getVersion(new IAppSpVersionCallback() {
            @Override
            public void update(AppSpModel<AppSpVersion> 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));
                }
            }
        });
    }
  • 公告信息获取
	/**
	 * 公告信息获取
	 */
	private void checkNotice() {
        AppSpConfig.getInstance().getNotice(new IAppSpNoticeCallback() {
            @Override
            public void notice(AppSpModel<List<AppSpNoticeModelItem>> 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));
                }
            }
        });
    }

# iOS Plugin实现

在AjFlutterAppspPlugin.m中

  • 版本初始化
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];
    }
}


  • 版本更新
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]);
  }];
}

  • 获取公告信息
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/aj-appsp/sdk/flutter (opens new window)