Flutter作为备受关注的跨平台的开发框架,长远来看,前景肯定是比较好的。
在其基础组件还未完善与成熟之前,能够高效的复用现有的native组件,是比较合适的方案。官方提供了Plugin的方式,允许将一个成熟的native组件(比如mapview),封装成一个可用dart来操作的widget。本文以封装一个腾讯地图组件为例,介绍一下整个过程。具体也可以参照一下谷歌官方封装的地图组件google_maps_flutter
首先,我们需要创建一个正常的MapView widget,该widget就是供外部展示native地图的widget。
typedef void MapViewCreatedCallback(MapViewController controller);
class MapView extends StatefulWidget {
final MapViewCreatedCallback onMapCreated;
// 地图类型
MapType mapType = MapType.standard;
// 其他的地图初始化参数,需要传递给native侧,提供给native的地图对象初始用
// ...
@override
State<StatefulWidget> createState() {
return _MapViewState();
}
}
这里和普通的widget大体一致,需要注意的有2个点:
定义一些初始化参数,后面会在_MapViewState里面传递给native侧做初始化
定义了一个create回调,参数是一个controller,这个controller其实就是和native侧做交互的对象,后面详细介绍
下面看一下State的代码
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
下面是dart侧最后一个类:controller。前面我们说过,组件使用者在创建MapView这个widget的时候,就能在onCreate回调拿到这个controller,然后后续就能够通过controller来与native做一些交互,比如说开启地图定位,搜索附近poi的列表等等。
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侧的:
backToCurLocation 这个是调用native的能力,将当前地图视图移动到当前定位点的位置
getRecentPoiList 这个是请求native去搜索附近的poi列表,注意这里会获取native的返回值
第三个注意点,就是接收native侧的事件回调,主要是通过channel的回调函数 _handleMethodCall 来统一处理的
regionDidChange native侧如果发现地图的视窗有变化(比如拖拽地图),flutter侧就能收到这个回调
下面看下native侧对应的代码
@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对象。
@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对象
最后就是controller对象了,controller继承自FlutterPlatformView协议,工厂调用Controller对象来创建真正的view实例。
代码部分稍多一点,我们分两部分来说,下面是第一部分
@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对象。
- (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端。
在react-native上是怎么写样式的吧,和传统的web不一样的是,在react-native上面是没有css代码,不过得益于Yoga,我们可以在客户端上像写css一样的去书写我们的样式。
初始化 wkwebview,设置 message handler,native 端注册了 testecho 的messageHandler,实现 WKScriptMessageHandler协议,执行JS 代码 ,所以 JS 可以通过 window.webkit.messageHandlers.testecho.postMessage 来回调客户端,和文档中说的一样
因为js语言的特性,使用node开发程序的时候经常会遇到异步处理的问题。对于之前专长App开发的我来说,会纠结node中实现客户端API请求的“最佳实践”。下面以OAuth2.0为场景,需要处理的流程:获取access token、使用获取到的token,发起API请求、处理API数据
GraphQL是一个API查询语言,他可以将使用PostgreSQL写的server代码自动生成Query或者Mutation,非常的方便。而Apollo Client就是一个强大的JavaScript GraphQL客户端。对于cache,在Apollo Client中有着强大的管理策略。
React Native 是 Facebook 2015年开源的 Javascript 框架,旨在使用 Javascript 高效开发手机端 App。根据大众的需求,我们列出了一个有用的React-Native UI库列表,可以帮助你更好地入门React Native。
在React Native 中由于业务的需要, 我们往往要在诸多的页面间,组件之间做一些参数的传递与管理, 在这里我总结了几大经过验证,稳定好用的方式给大家
随着 H5 标准的发布以及推广,使得移动应用的开发也受到了很大影响,出于效率、成本等原因,移动应用的开发不再完全依赖于 “原生”。近日越发火热的混合应用(Hybrid App)介于 Web 应用和原生应用之间
react-native打包安卓apk的时候,报错Cannot get property packageName on null object,完全没有头绪,研究了半天才发现竟然是因为package.json里面scripts自定了命令导致的,无法理解为何会影响安卓打包并且报错packageName null
之前写的项目都是人家编写好的脚手架,里面包含项目所需的环境文件,但由于有些东西用不到打包增加软件体积,所以自己从头搭建个环境。是基于 Native Base + react-navigation + Redux 的 React Native 脚手架,现在项目环境如下
近些年,移动端普及化越来越高,开发过程中选用 Native 还是 H5 一直是热门话题。Native 和 H5 都有着各自的优缺点,为了满足业务的需要,公司实际项目的开发过程中往往会融合两者进行 Hybrid 开发。Native 和 H5 分处两地,看起来无法联系,那么如何才能让双方协同实现功能呢?
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!