为Flutter提供Native组件(iOS)

1,289 阅读4分钟

预览的效果


创建Plugin

通过Android Studio 创建一个新的Flutter Plugin。

我们选择Flutter Plugin,这个是创建Plugin的

同样Flutter platform view也属于一个Plugin范畴;


分析一下我们创建的Plugin

如下图所示:我们的Plugin包含以下目录和文件,我们只分析对我们有用的问题;


  • android
  • ios
  • example
  • lib
  • pubspec.yaml

我们分析仅已iOS开发为例,所以我们需要设计的文件夹不包括Android(Android的实现跟iOS的实现基本一致)

ios:

  • assets
  • Classes

同学们是不是很熟悉,这个和我们自定义的Cocoapods组件库。

如果不涉及图片或者其它资源,

我们只需要在Classes文件夹下提供我们iOS侧的实现即可。

如何实现请看下面的【对应平台实现】部分

lib:

  • xxx.dart

这个文件夹下存放的是根据我们创建的文件夹名称命名的dart文件

对于简单的视图组件来说,我们在这一个文件内定义就可以了。

在这个组件内我们做了视图的定义、传递的参数、通讯的方法

具体的开发请看【自定义Widget】部分

pubspec.yaml

这个跟创建一个plugin是一个道理,

里面包含了我们创建的platformview plugin的一些基本信息

名称、开发者、版本号等。

一般我们只有在发布组件或者内部修改组件的时候

才会更改此文件。

自定义Widget

import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';

// 定义callback
typedef void JWCustomViewCreateCallback(JWCustomViewController controller);

// 创建通讯类
class JWCustomViewController {
  // 构建通道
  JWCustomViewController._(int id)
      : _channel = MethodChannel('wanmei.custom.plugins/custom_$id');
  final MethodChannel _channel;

  // 定义方法
  Future<void> sayHello() async {
    return _channel.invokeMethod('sayHello');
  }
}

// 定义有状态组件
class JWCustomView extends StatefulWidget {

  // 定义传递进来的参数
  const JWCustomView({Key key, this.onCreateCallback, this.showText}) : super (key: key);
  final JWCustomViewCreateCallback onCreateCallback;
  final String showText;

  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _JWCustomViewState();
  }
}

// 包裹组件返回
class _JWCustomViewState extends State<JWCustomView> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build

    // 进行平台判断
    if (defaultTargetPlatform == TargetPlatform.iOS) {
      // ios返回代码
      return UiKitView(
        viewType: "wanmei.custom.plugins/custom",
        onPlatformViewCreated: _onPlatformViewCreated, // native端代码的初始化
        creationParams: <String, dynamic> { // 传递的参数
          "showText": widget.showText,
          "otherColor": 'green',
        },
        creationParamsCodec: StandardMessageCodec(), // 传递参数的编码格式,如果需要传参。则是必须的;
      );
    } else if (defaultTargetPlatform == TargetPlatform.android) {
      // android返回视图
      // 我们这里暂时不考虑安卓平台的实现
      return Text('暂时不考虑安卓平台的实现');
    } else {
      return Text('插件尚未支持对应平台');
    }

    return Container();
  }

  // 创建对应平台视图
  void _onPlatformViewCreated(int id) {
    // 如果创建方法的回调是空
    if (widget.onCreateCallback == null) {
      return;
    }
    // 不为空则调用平台方创建我们需要的视图
    widget.onCreateCallback(JWCustomViewController._(id));
  }
}

对应平台实现(iOS)

这部分大概有2个文件:

  • xxxxPlugin.h
  • xxxxCustom.h

xxxPlugin.h

#import <Flutter/Flutter.h>

@interface FlutterpluginlabelPlugin : NSObject<FlutterPlugin>
@end

xxxPlugin.m

#import "FlutterpluginlabelPlugin.h"
#import "FlutterCustomView.h"

@implementation FlutterpluginlabelPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
    // 首先删除plugin模板生成的注册代码。因为我们修改了通UN的字符,而且我们不需要在这个文件内修改插件对应的action
    
    // 通过自定义组件名进行通信开启注册ID
    [registrar registerViewFactory:[[FlutterCustomViewFactory alloc] initWithMessenger:registrar.messenger] withId:@"wanmei.custom.plugins/custom"];
}

// 这是插件开发时候生成的默认代码,我们不需要,删除它getPlatformVersion


@end

xxxCustom.h

//
//  FlutterCustomView.h
//  flutterpluginlabel
//
//  Created by wangjiawei on 2020/4/29.
//

#import <Foundation/Foundation.h>
// 引入Flutter插件
#import <Flutter/Flutter.h>


NS_ASSUME_NONNULL_BEGIN

@interface FlutterCustomView : NSObject

@end

// 实现自定时视图
@interface FlutterCustomViewController : NSObject<FlutterPlatformView>

- (instancetype)initWithWithFrame:(CGRect)frame
                   viewIdentifier:(int64_t)viewId
                        arguments:(id _Nullable)args
                  binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;

@end

// 实现消息通道
@interface FlutterCustomViewFactory : NSObject<FlutterPlatformViewFactory>

- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messager;

@end

NS_ASSUME_NONNULL_END

xxxCustom.m

//
//  FlutterCustomView.m
//  flutterpluginlabel
//
//  Created by wangjiawei on 2020/4/29.
//

#import "FlutterCustomView.h"

@implementation FlutterCustomView

@end

#pragma mark - 视图界面

@implementation FlutterCustomViewController {
    // 持有视图属性
    int64_t _viewId;
    FlutterMethodChannel *_channel;
    UILabel *_nativeLabel;
}

// 构建视图
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger{
    if ([super init]) {
        
        // 获取参数
        NSDictionary *dic = args;
        NSString *otherColor = dic[@"otherColor"];
        NSString *showText = dic[@"showText"];
        
        _nativeLabel = [UILabel new];
        _nativeLabel.text = showText;
        _nativeLabel.textColor = [otherColor isEqualToString:@"green"] ? [UIColor greenColor]: [UIColor blackColor];
        _nativeLabel.font = [UIFont boldSystemFontOfSize:24];
        
        _viewId = viewId;
        NSString* channelName = [NSString stringWithFormat:@"wanmei.custom.plugins/custom_%lld", viewId];
        _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger];
        __weak __typeof__(self) weakSelf = self;
        [_channel setMethodCallHandler:^(FlutterMethodCall *  call, FlutterResult  result) {
            [weakSelf onMethodCall:call result:result];
        }];
        
    }
    
    return self;
}

// 返回创建好的组件
- (UIView *)view{
    return _nativeLabel;
}

// 处理方法调用
- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result{
    if ([[call method] isEqualToString:@"sayHello"]){
        [_nativeLabel setText:@"你好呀,赛利亚!"];
    } else {
        result(FlutterMethodNotImplemented);
    }
}


@end

#pragma mark - 消息通道

@implementation FlutterCustomViewFactory {
    // 持有消息
    NSObject<FlutterBinaryMessenger>*_messenger;
}

- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messager{
    self = [super init];
    if (self) {
        _messenger = messager;
    }
    return self;
}

- (NSObject<FlutterMessageCodec> *)createArgsCodec{
    return [FlutterStandardMessageCodec sharedInstance];
}

- (NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args{
   FlutterCustomViewController *imp = [[FlutterCustomViewController alloc] initWithWithFrame:frame viewIdentifier:viewId arguments:args binaryMessenger:_messenger];
    return imp;
}

@end

在项目中使用

注意!!!

很关键的一个点,iOS这边如果需要为Flutter提供Native视图,需要修改Info.plist

在Flutter你打包的ios/Ruuner项目中的Info.plist添加如下代码

<key>io.flutter.embedded_views_preview</key>   
<true/>

在example中使用:

我们观察到example的pubspec.yaml已经引用了我们自定义组件

这是创建plugin的时候自动添加的

我们只需要在Flutter项目中的pubspec.yaml同理引用即可

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutterpluginlabel/flutterpluginlabel.dart';

void main() => runApp(MaterialApp(
  home: ActivityIndicatorExample(),
));

class ActivityIndicatorExample extends StatelessWidget{

  // 引用与初始化
  JWCustomViewController controller;
  void _onActivityIndicatorControllerCreated(JWCustomViewController _controller){
    controller = _controller;
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(title: const Text("加载测试"),),
      body: Stack(
        alignment: Alignment.bottomCenter,
        children: <Widget>[
          new Container(
            child: new Stack(
              children: <Widget>[
                // 是一个背景透明的视图
                JWCustomView(
                  showText: '我这边创建一个原生视图!!!',
                  onCreateCallback: _onActivityIndicatorControllerCreated,
                ),
                new Container(
                  alignment: Alignment.center,
                  child: new Text("我是flutter控件,没有被遮挡~"),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.only(left: 45.0,right: 45.0,top: 0.0,bottom: 50.0),
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                FloatingActionButton(
                  onPressed: (){
                    controller.sayHello();
                  },
                  child: new Text("Start"),
                ),
              ],
            ),
          )
        ],
      ),
    );
  }

}