前因
自從在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.參考資料