如何給Flutter寫一個高德地圖定位外掛 | 掘金技術徵文

bg7lgb發表於2018-07-25

前因

自從在solidot看到介紹Flutter的文章後,對這個框架產生了很大的興趣。關於跨平臺的app開發,前面也瞭解了H5+,公司也有同事做在5+App的開發。google的產品對我來說是比較有吸引力的,比如golang。在看了教程後,決定拿一個小東西來練手,於是選擇了高德地圖定位,打算用flutter來實現一個高德定位的外掛。

如何給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。

如何給Flutter寫一個高德地圖定位外掛 | 掘金技術徵文
在AS中把外掛的安卓介面模組開啟。注意這是一個坑,原來直接編輯檔案,引入高德sdk,怎麼折騰都不成功(請原諒,我真的很笨,沒搞過安卓開發,不會)。

  • 引入高德定位sdk

開啟android模組後,點File->Project Structure, 左側欄選擇modules->flutter_amap_location,右邊dependencies頁。

如何給Flutter寫一個高德地圖定位外掛 | 掘金技術徵文
點下面的+號,選第一個Library Dependency。 輸入com.amap.api:location,查詢出來後,選中後點ok。

如何給Flutter寫一個高德地圖定位外掛 | 掘金技術徵文

完成高德定位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 技術實踐 | 掘金技術徵文,徵文活動正在進行中

相關文章