前因
自从在solidot看到介绍Flutter的文章后,对这个框架产生了很大的兴趣。关于跨平台的app开发,前面也了解了H5+,公司也有同事做在5+App的开发。google的产品对我来说是比较有吸引力的,比如golang。在看了教程后,决定拿一个小东西来练手,于是选择了高德地图定位,打算用flutter来实现一个高德定位的插件。
先声明一下,本人专业打杂,开发为业余爱好,java、dart、flutter均是初次学习。写这个文章的目的,是记录一下学习flutter过程的点滴,错漏之处,敬请各位予以批评指正。
工作原理
在写flutter插件的过程,需要了解以下工作原理:
- flutter通过MethodChannel与native通信,主要用于执行一些控制指令或者同步调用native的方法;
- native端则通过EventChannel将发生的事件或消息传递给flutter,主要是异步调用结果或者native持续产生的数据流。
高德地图定位SDK,发起定位的调用都异步的,定位请求的发起由flutter通过MethodChannel的invokeMethod()来发起,定位成功后通过高德定位SDK中AMapLocationListen接口的onLocationChanged()来获取定位信息,然后将定位信息通过EventChannel发送给Flutter。
开发过程
本次开发使用的环境是macOS El Capitan,Android studio 3.1.3, Flutter SDK 0.5.1。
建议大家使用mac来写Flutter,原来我使用windows,那个FormatException: Bad UTF-8 encoding 0xb4 错误搞到我欲仙欲死,差点放弃。我没钱买Mac Book,正好家里有块SSD,就安装了黑苹果。我不会写iOS代码,这个定位的插件只实现了android版本,iOS版本等我学会了再补充上。
- 创建Flutter plugin项目
新建项目的过程略过。
- 插件的native端开发
- 打开安卓模块
在AS中项目视图中选择android,点右键->Flutter->Open Android module in android studio。
在AS中把插件的安卓接口模块打开。注意这是一个坑,原来直接编辑文件,引入高德sdk,怎么折腾都不成功(请原谅,我真的很笨,没搞过安卓开发,不会)。- 引入高德定位sdk
打开android模块后,点File->Project Structure, 左侧栏选择modules->flutter_amap_location,右边dependencies页。
点下面的+号,选第一个Library Dependency。 输入com.amap.api:location,查找出来后,选中后点ok。完成高德定位sdk引入。
final MethodChannel channel = new MethodChannel(registrar.messenger(), METHOD_CHANNEL_NAME);
channel.setMethodCallHandler(plugin);
final EventChannel positionChannel = new EventChannel(registrar.messenger(), STREAM_CHANNEL_NAME );
positionChannel.setStreamHandler( plugin);
复制代码
定义了一个MethodChannel和一个EventChannel。
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else if (call.method.equals("getLocationOnce")) {
// 设置定位场景为签到模式
mapLocationClientOption.setLocationPurpose(AMapLocationClientOption.AMapLocationPurpose.SignIn);
if (null != mapLocationClient) {
mapLocationClient.setLocationOption(mapLocationClientOption);
mapLocationClient.stopLocation();
mapLocationClient.startLocation();
}
} else if (call.method.equals("getLocation")) {
// 设置定位间隔5秒,默认2秒
mapLocationClientOption.setInterval(5000);
// 设置定位场景为出行模式
mapLocationClientOption.setLocationPurpose(AMapLocationClientOption.AMapLocationPurpose.Transport);
if (null != mapLocationClient) {
mapLocationClient.setLocationOption(mapLocationClientOption);
//设置场景模式后最好调用一次stop,再调用start以保证场景模式生效
mapLocationClient.stopLocation();
mapLocationClient.startLocation();
}
} else if (call.method.equals("stopLocation")) {
// 停止定位
mapLocationClient.stopLocation();
} else {
result.notImplemented();
}
}
复制代码
复写了onMethodCall()方法,用来接受flutter的调用指令。一共定义了三个命令:getLocationOnce(使用高德的签到场景,只定位一次)、getLocation(使用高德的出行模式,连续定位)、stopLocation(停止定位)。
@Override
public void onListen(Object o, EventSink eventSink) {
event = eventSink;
}
@Override
public void onCancel(Object o) {
event = null;
}
复制代码
复写了StreamHandler接口的onListen()和onCancel()方法。
private FlutterAmapLocationPlugin(Activity activity) {
this.activity = activity;
mapLocationClientOption = new AMapLocationClientOption();
mapLocationClient = new AMapLocationClient(activity.getApplicationContext());
mapLocationListener = new AMapLocationListener() {
@Override
public void onLocationChanged(AMapLocation aMapLocation) {
HashMap<String, Object> loc = new HashMap<>();
if (aMapLocation != null) {
if (aMapLocation.getErrorCode() == 0) {
// 定位成功
loc.put("latitude", aMapLocation.getLatitude());
loc.put("longitude", aMapLocation.getLongitude());
loc.put("country", aMapLocation.getCountry());
loc.put("province", aMapLocation.getProvince());
loc.put("city", aMapLocation.getCity());
loc.put("district", aMapLocation.getDistrict());
loc.put("street", aMapLocation.getStreet());
loc.put("streetnum", aMapLocation.getStreetNum());
loc.put("adcode", aMapLocation.getAdCode());
loc.put("poiname", aMapLocation.getPoiName());
loc.put("address", aMapLocation.getAddress());
event.success(loc);
} else {
// 定位失败
loc.put("errorcode", aMapLocation.getErrorCode());
loc.put("errorinfo", aMapLocation.getErrorInfo());
event.error("error", "get location error", loc);
}
}
}
};
mapLocationClient.setLocationListener(mapLocationListener);
}
复制代码
由于高德定位是异步调用,定位成功后回调AMapLocationListener的onLocationChanged()方法。复写这个方法,从中获取定位信息,然后通过eventchannel发送回给flutter。
- 插件的flutter端开发
这一部分的内容比较简单。
class FlutterAmapLocation {
static const String METHOD_CHANNEL_NAME = "bg7lgb/amap_location";
static const String EVENT_CHANNEL_NAME = "bg7lgb/amap_location_stream";
// 创建MethodChannel,用于调用native端方法
static const MethodChannel _channel =
const MethodChannel(METHOD_CHANNEL_NAME);
// 创建EventChannel,用于接收native端发送的数据
static const EventChannel _stream =
const EventChannel(EVENT_CHANNEL_NAME);
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
// 一次定位
static Future<void> getLocationOnce() async {
await _channel.invokeMethod("getLocationOnce");
}
// 连续定位
static Future<void> getLocation() async {
await _channel.invokeMethod("getLocation");
}
// 停止定位
static Future<void> stopLocation() async {
await _channel.invokeMethod("stopLocation");
}
// 接收stream消息,listen时传入返回void的两个函数,分别是接收到数据时的处理
// 和发生错误时的处理
// EventHandler的定义
// typedef void EventHandler(Object event);
static listenLocation(EventHandler onEvent, EventHandler onError) {
_stream.receiveBroadcastStream().listen(onEvent, onError: onError );
}
}
复制代码
- example程序开发 插件的初始化
@override
void initState() {
super.initState();
initPlatformState();
FlutterAmapLocation.listenLocation(_onLocationEvent, _onLocationError);
}
// 接收到数据的处理方法
void _onLocationEvent(Object event) {
Map<String, Object> loc = Map.castFrom(event);
setState(() {
_longitude = loc['longitude'];
_latitude = loc['latitude'];
_address = loc['address'];
});
}
// 接收到错误的处理方法
void _onLocationError(Object event) {
print(event);
}
复制代码
插件中需要自己编写两个函数,一个用于接收到数据时的处理,一个用于接收错误时的处理。然后将这两个函数作为FlutterAmapLocation.listenLocation()的参数。
至此,一个高德定位的flutter插件便完成了,代码已经放到github。
- 待完善的内容
- 高德定位权限,可参考高德的sdk帮助文档
- 高德定位apikey设置
- 插件的iOS实现(还要学呀:()
6.参考资料