如何给Flutter写一个高德地图定位插件 | 掘金技术征文

4,255 阅读4分钟

前因

自从在solidot看到介绍Flutter的文章后,对这个框架产生了很大的兴趣。关于跨平台的app开发,前面也了解了H5+,公司也有同事做在5+App的开发。google的产品对我来说是比较有吸引力的,比如golang。在看了教程后,决定拿一个小东西来练手,于是选择了高德地图定位,打算用flutter来实现一个高德定位的插件。

先声明一下,本人专业打杂,开发为业余爱好,java、dart、flutter均是初次学习。写这个文章的目的,是记录一下学习flutter过程的点滴,错漏之处,敬请各位予以批评指正。

工作原理

image

在写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版本等我学会了再补充上。

  1. 创建Flutter plugin项目

新建项目的过程略过。

  1. 插件的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。

  1. 插件的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 );
  }

}
  1. 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

  1. 待完善的内容
  • 高德定位权限,可参考高德的sdk帮助文档
  • 高德定位apikey设置
  • 插件的iOS实现(还要学呀:()

6.参考资料

从 0 到 1:我的 Flutter 技术实践 | 掘金技术征文,征文活动正在进行中