Flutter作为备受关注的跨平台的开发框架,长远来看,前景肯定是比较好的。
在其基础组件还未完善与成熟之前,能够高效的复用现有的native组件,是比较合适的方案。官方提供了Plugin的方式,允许将一个成熟的native组件(比如mapview),封装成一个可用dart来操作的widget。本文以封装一个腾讯地图组件为例,介绍一下整个过程。具体也可以参照一下谷歌官方封装的地图组件google_maps_flutter
整体框架图
Dart侧
首先,我们需要创建一个正常的MapView widget,该widget就是供外部展示native地图的widget。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| typedef void MapViewCreatedCallback(MapViewController controller);
class MapView extends StatefulWidget { final MapViewCreatedCallback onMapCreated; MapType mapType = MapType.standard; @override State<StatefulWidget> createState() { return _MapViewState(); } }
|
这里和普通的widget大体一致,需要注意的有2个点:
- 定义一些初始化参数,后面会在_MapViewState里面传递给native侧做初始化
- 定义了一个create回调,参数是一个controller,这个controller其实就是和native侧做交互的对象,后面详细介绍
_MapViewState State
下面看一下State的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class _MapViewState extends State<MapView> {
final Completer<MapViewController> _controller = Completer<MapViewController>();
@override Widget build(BuildContext context) {
final Map<String, dynamic> creationParams = <String, dynamic>{ 'mapType': widget.mapType.index, };
if (defaultTargetPlatform == TargetPlatform.iOS) { return UiKitView( viewType: 'qq_maps', onPlatformViewCreated: onPlatformViewCreated, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), ); }
return Text('$defaultTargetPlatform is not yet supported'); }
Future<void> onPlatformViewCreated(int id) async { final MapViewController controller = await MapViewController.init(id, this);
_controller.complete(controller); if (widget.onMapCreated != null) { widget.onMapCreated(controller); } } }
|
核心就在build这里了,Flutter提供了一个UIKitView(iOS侧,安卓对应的是AndroidView)的组件,这个组件就是桥接native view的关键,我们看看其参数。
- viewType 这个是传递给native侧,用作view factory的key,后面讲native代码时我们再看
- creationParams 这里是允许传递给native侧的初始化参数
- onPlatformViewCreated platformView创建成功回调,注意回调参数是viewId,通常会在这里初始化Controller,并将controller作为上面MapView onCreateCallback的参数。这样子外部在使用MapView这个widget的时候,就能够拿到其对应的Controller
MapViewController
下面是dart侧最后一个类:controller。前面我们说过,组件使用者在创建MapView这个widget的时候,就能在onCreate回调拿到这个controller,然后后续就能够通过controller来与native做一些交互,比如说开启地图定位,搜索附近poi的列表等等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| typedef void MapViewRegionDidChange();
class MapViewController {
final MethodChannel channel; final _MapViewState _googleMapState;
MapViewRegionDidChange regionDidChange;
MapViewController._( this.channel, this._googleMapState, ) { channel.setMethodCallHandler(_handleMethodCall); }
static Future<MapViewController> init( int id, _MapViewState mapViewState, ) async { assert(id != null);
final MethodChannel channel = MethodChannel('qq_maps_$id');
return MapViewController._(channel, mapViewState); }
Future<dynamic> _handleMethodCall(MethodCall call) async { switch (call.method) { case 'map#regionDidChange': regionDidChange(); break; } }
Future<void> backToCurLocation() async { await channel.invokeMethod( 'map#backToCurLocation', ); }
Future<List> getRecentPoiList({String keyword = "大厦"}) async { final Map data = await channel.invokeMethod( 'map#getRecentPoiList', keyword, );
int result = data["result"]; if (result == 0) { return data["poiList"]; } else { return List(); } } }
|
首先我们观察到,controller维护了一个channel对象,这个对象就负责收发native端的消息。
注意一个tips:这个channel对象的name是qq_maps_$id
,id是UIKitView的create回调带过来的,表示viewID。这就是说如果有多个地图实例的话,每一个地图实例都对应一个自己的channel,保证消息收发不会串掉。
然后,controller提供了2个方法,这两个方法都是直接桥接native侧的:
第三个注意点,就是接收native侧的事件回调,主要是通过channel的回调函数_handleMethodCall
来统一处理的
- regionDidChange native侧如果发现地图的视窗有变化(比如拖拽地图),flutter侧就能收到这个回调
Native侧
下面看下native侧对应的代码
MapviewPlugin
1 2 3 4 5 6 7 8 9 10
| @interface MapviewPlugin : NSObject<FlutterPlugin> @end
@implementation MapviewPlugin + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { QQMapViewFactory *factory = [[QQMapViewFactory alloc] initWithRegistrar:registrar]; [registrar registerViewFactory:factory withId:@"qq_maps"]; } @end
|
首先,MapviewPlugin继承自FlutterPlugin对象,该对象主要是用来向flutter注册我们的plugin,具体代码在GeneratedPluginRegistrant
类里面,这里未列出。
这里主要关注我们创建了一个QQMapViewFactory,FlutterPluginRegistrar
注册了这个mapview工厂,其对应的id实际上就是Dart侧里面UIKitView的viewType
,表明我这个工厂管理的就是mapview这个类型的view对象。
QQMapViewFactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @interface QQMapViewFactory : NSObject<FlutterPlatformViewFactory> @end
@implementation QQMapViewFactory
- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args { return [[QQMapViewController alloc] initWithFrame:frame viewIdentifier:viewId arguments:args registrar:_registrar]; }
|
工厂的核心就是这个createWithFrame方法了,这个方法是由dart侧来驱动的。dart侧使用MapView的widget,配合flutter的布局widget,来计算出何处需要一个mapview的native view,其frame和viewid,都是dart侧传递过来。我们看下这个函数的参数:
- frame dart侧通过其布局widget来计算得来
- viewId 由于可能有多个地图组件同时展示,每个地图实例都有各自的viewId来区分
- args 对应dart侧UIKitView的creationParams参数
- 返回值 FlutterPlatformView协议,这个协议实际上就一个接口,返回一个UIView对象
QQMapViewController
最后就是controller对象了,controller继承自FlutterPlatformView协议,工厂调用Controller对象来创建真正的view实例。
代码部分稍多一点,我们分两部分来说,下面是第一部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| @interface QQMapViewController : NSObject<FlutterPlatformView>
@end
@implementation QQMapViewController
- (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args registrar:(NSObject<FlutterPluginRegistrar> *)registrar { if (self = [super init]) {
_mapView = [[QMapView alloc] initWithFrame:frame]; _mapView.delegate = self; [self mapArgs:args toView:_mapView]; NSString *channelName = [NSString stringWithFormat:@"qq_maps_%lld", viewId]; _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:registrar.messenger];
__weak __typeof__(self) weakSelf = self; [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { if (weakSelf) { [weakSelf onMethodCall:call result:result]; } }]; } return self; }
- (void)mapArgs:(id _Nullable)args toView:(QMapView *)view { if ([args isKindOfClass:[NSDictionary class]] && view != nil) { view.mapType = [args[@"mapType"] intValue]; } }
- (nonnull UIView *)view { return _mapView; }
|
第一部分很简单,我们主要关注如下点:
- 真正的QMapView对象初始化,然后在
FlutterPlatformView
协议的view方法里面返回。这个view对象就是真正的和dart层MapView widget对应的view了
- 与dart侧的controller相对应,native侧的controller也管理了channel对象,channel的name与dart侧一致。native侧与dart侧的消息收发同样通过这个channel
- dart侧传递过来的初始化参数,
mapArgs:toView
方法里面我们传递给了mapview对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([call.method isEqualToString:@"map#backToCurLocation"]) { [_mapView setCenterCoordinate:_mapView.userLocation.location.coordinate animated:YES]; result(nil); } else if ([call.method isEqualToString:@"map#getRecentPoiList"]) { QMSPoiSearchOption *option = [[QMSPoiSearchOption alloc] init]; option.keyword = call.arguments; [option setBoundaryByNearbyWithCenterCoordinate:_mapView.centerCoordinate radius:1000 autoExtend:1]; [_searcher searchWithPoiSearchOption:option]; _searchResult = result; } }
#pragma mark - mapview delegate - (void)mapView:(QMapView *)mapView regionDidChangeAnimated:(BOOL)animated gesture:(BOOL)bGesture { [_channel invokeMethod:@"map#regionDidChange" arguments:nil]; }
#pragma mark - QMSSearchDelegate - (void)searchWithPoiSearchOption:(QMSPoiSearchOption *)poiSearchOption didReceiveResult:(QMSPoiSearchResult *)poiSearchResult { NSMutableArray *list = [NSMutableArray new]; for (QMSPoiData *poi in poiSearchResult.dataArray) { NSDictionary *poiDic = @{ @"id": poi.id_, @"title": poi.title, @"distance": @(QMetersBetweenCoordinates(poi.location, _mapView.centerCoordinate)), @"address": poi.address }; [list addObject:poiDic]; } _searchResult(@{ @"result": @(0), @"poiList": list }); }
|
第二部分就是和dart侧相关交互的代码了,基本和dart的controller代码相对应:
- onMethodCall dart侧发起的函数调用,首先会到这里,然后再分发给具体的实现函数。我们可以看到刚刚dart侧的2个接口(
map#backToCurLocation
和map#getRecentPoiList
),在native侧具体是怎么实现的。
mapView:regionDidChangeAnimated
这个是地图sdk给的回调,这里面我们可以看到是直接将该回调通过channel的invokeMethod
方法传递到dart端。
本篇主要是使用介绍,具体platform view的原理,可以看看下一篇原理篇。