0%

flutter_plugin开发

一个开发Flutter plugin 和 在Flutter中嵌入原生控件的笔记。完全是照着的官网来实践的。

https://flutter.dev/docs/development/packages-and-plugins/developing-packages

Flutter中的package分为两种,一种是纯dart语言的的package,比如 fluro,称之为Dart packages

还有一种是由原生平台代码(Android:java or kotlin,iOS:OC or swift),比如battery,称之为Plugin packages

下面是对Plugin packages开发的实践

Create the package

创建工程,命令行

flutter create --org com.example --template=plugin name

默认Android是java,iOS是OC,也可以指定默认语言

flutter create --template=plugin -i swift -a kotlin name

也可以同过AS创建,选择Flutter plugin就好。

主要文件如下(以工程根目录为基础目录来说的):

  • lib

    刚创建好的项目中,该文件夹下只有一个以项目名命名的dart文件,创建了一个MethodChannel 和实现了一个platformVersion方法

  • android

    这里需要注意一下,在AndroidStudio中以Project方式预览工程时,该目录下的src.main文件夹下只有一个清单文件,可以切换到Android方式预览。可以看到src.main下有一个实现MethodCallHandler接口的类,并且已经实现了getPlatformVersion的调用,我们开发插件原生代码就是在这里写的

  • iOS

    在AndroidStudio下看到只有两个文件夹Assets和Classes,在Classes下有个.h.m,也是创建了FlutterMethodChannel和实现了platformVersion调用

  • Example

    在这里测试我们自己写的插件,并且给使用这提供示例。该文件夹下包含Android、iOS和Flutter代码

Implement the package

As a plugin package contains code for several platforms written in several programming languages, some specific steps are needed to ensure a smooth experience.

Define the package API

假如我们要开发一个打开某些原生界面的插件,比如打开原生设置页面、拨号页面、浏览器等

lib下的dart文件中,增加一个名字为openOSView的方法调用,并且传递一个String类型的url参数

1
2
3
4
5
6
7
8
9
10
11
12
13
class FlutterPluginOpenNative {
static const MethodChannel _channel =
const MethodChannel('flutter_plugin_open_native');

static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}

static Future<void> openNative(String url) async {
await _channel.invokeMethod('openOSView',url);
}
}

Add Android platform code

在Android下实现了MethodCallHandler接口的类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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("openOSView")){
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(call.arguments.toString()));
activity.startActivity(intent);
}
else {
result.notImplemented();
}
}

Add iOS platform code

在iOS

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
}
else if([@"openOSView" isEqualToString:call.method]){
NSString * phoneUri = call.arguments;
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:phoneUri]];
}

else {
result(FlutterMethodNotImplemented);
}
}

如果是用XCode开发的话,选择打开example–>ios–>Runner.xcworkspace,该文件存在于

Pods-->Development Pods-->projectname

project in xcode

第一个红框是项目中example–>ios中的内容

第二个红框是项目中ios文件夹下内容


这样,一个简单的插件就开发完了,我们可以在example中测试一下,由于插件特别简单,不需要在原生侧做初始化之类的工作,使用创建项目时生成的代码就可以满足我们的需要。

example-->lib-->main.dart中放一个按钮,点击的时候调用FlutterPluginOpenNative.openNative("https://blog.huangyuanlove.com");然后运行到设备,查看一下效果

flutter run -d all 可以运行到所有已连接的设备


Flutter嵌入原生控件

上面是开发的插件,下面是如果在flutter中嵌入原生控件

这里用到了Flutter中的两个类AndroidViewUiKitView,懒得翻译直接看官方版https://api.flutter.dev/flutter/widgets/AndroidView-class.htmlhttps://api.flutter.dev/flutter/widgets/UiKitView-class.html

例如,我们要嵌入原生展示文字的控件,对于Android来讲,一般是TextView,对于iOS来讲,一般是UILabel。

在 flutter侧

一般是一个Controller和一个StatefulWidget,当然这里的Controller只是一个普通类,用来创建channel和原生通信

代码如下

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 TextViewCreatedCallback(TextViewController controller);
class TextViewController {
TextViewController._(int id)
: _channel = new MethodChannel('me.chunyu.textview/textview');

final MethodChannel _channel;

//调用setText方法给控件设置文字,需要原生侧进行实现
Future<void> setText(String text) async {
assert(text != null);
return _channel.invokeMethod('setText', text);
}
}

class AndroidTextView extends StatefulWidget {

final TextViewCreatedCallback onTextViewCreated;
const AndroidTextView({
Key key,
this.onTextViewCreated,
}) : super(key: key);

@override
_AndroidTextViewState createState() => _AndroidTextViewState();
}

//对Android平台返回AndroidView,对iOS平台返回UiKitView,并且制定viewType
class _AndroidTextViewState extends State<AndroidTextView> {
@override
Widget build(BuildContext context) {
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'me.chunyu.textview/textview',
onPlatformViewCreated: _onPlatformViewCreated,
);
}else if (defaultTargetPlatform == TargetPlatform.iOS){
return UiKitView(
viewType: 'me.chunyu.textview/textview',
onPlatformViewCreated: _onPlatformViewCreated,
);
}
return Text(
'$defaultTargetPlatform is not yet supported by the text_view plugin');

}
void _onPlatformViewCreated(int id) {
if (widget.onTextViewCreated == null) {
return;
}
print("id _onPlatformViewCreated :--> $id");


widget.onTextViewCreated(new TextViewController._(id));
}
}

在Android侧

我们需要一个实现了PlatformViewFactory的类 和一个实现了PlatformViewMethodCallHandler接口的类

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
57
58

class TextViewFactory extends PlatformViewFactory{

private final BinaryMessenger messenger;

public TextViewFactory(BinaryMessenger messenger) {
super(StandardMessageCodec.INSTANCE);
this.messenger = messenger;
}

@Override
public PlatformView create(Context context, int id, Object o) {
return new FlutterTextView(context, messenger, id);
}

}

class FlutterTextView implements PlatformView, MethodCallHandler {
private final TextView textView;
private final MethodChannel methodChannel;

FlutterTextView(Context context, BinaryMessenger messenger, int id) {
Log.e("id","id :-->" +id);
textView = new TextView(context);
textView.setTextColor(Color.BLUE);
textView.setBackgroundColor(Color.GREEN);
methodChannel = new MethodChannel(messenger, "me.chunyu.textview/textview");
methodChannel.setMethodCallHandler(this);
}

@Override
public View getView() {
return textView;
}

//实现以下flutter通过channel调用的`setText`方法
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch (methodCall.method) {
case "setText":
setText(methodCall, result);
break;
default:
result.notImplemented();
}

}

//设置文字,并返回成功
private void setText(MethodCall methodCall, Result result) {
String text = (String) methodCall.arguments;
textView.setText(text);
result.success(null);
}

@Override
public void dispose() {}
}

这里需要注意,在插件的registerWith方法中注册一下channel

1
registrar.platformViewRegistry().registerViewFactory("me.chunyu.textview/textview",new TextViewFactory(registrar.messenger()));

在iOS侧

一个.h文件,声明一下实现NSObject<FlutterPlatformView>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>
NS_ASSUME_NONNULL_BEGIN

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

@end
@interface FlutterAndroidTextViewFactory : NSObject<FlutterPlatformViewFactory>

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

@end

NS_ASSUME_NONNULL_END

一个.m文件,实现以上方法

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
57
58
59
60
61
62
63
64
65
66

#import "AndroidTextView.h"

@implementation FlutterAndroidTextViewFactory{
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{
AndroidTextView * androidTextView = [[AndroidTextView alloc] initWithWithFrame:frame viewIdentifier:viewId arguments:args binaryMessenger:_messenger];

return androidTextView;

}

@end


@implementation AndroidTextView{
int64_t _viewId;
FlutterMethodChannel* _channel;
UILabel * _label;

}

- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger{
if ([super init]) {
_label = [[UILabel alloc] init];
_label.textColor = UIColor.redColor;
_label.backgroundColor = UIColor.blueColor;
_label.font = [UIFont fontWithName:@"Arial" size:30];
_viewId = viewId;
NSString* channelName = @"me.chunyu.textview/textview";
_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{
NSLog(@"invoke view()");
return _label;
}
//实现flutter侧通过channel调用的`setText`方法
-(void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result{
if ([[call method] isEqualToString:@"setText"]) {
_label.text = [call arguments];
NSLog(@"%@", call.arguments);
}
}
@end

在plugin中的registerWithRegistrar方法中注册一下channel

1
[registrar registerViewFactory:[[FlutterAndroidTextViewFactory alloc] initWithMessenger:registrar.messenger] withId:@"me.chunyu.textview/textview"];

需要注意的是,如果想要在iOS中显示这种flutter嵌入原生的空间,需要在info.plist文件中加入

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

在flutter中嵌入原生控件并不推荐,性能跟不上,消耗太大了,但是在某些情况下不得不这么搞。。。。。

Adding documentation

这个没啥好说的,跟着官方做就好了

When you publish a package, API documentation is automatically generated and published to dartdocs.org, see for example the device_info docs

Adding licenses to the LICENSE file

添加协议

Publishing packages

发布之前运行一下 flutter pub pub publish --dry-run,根据提示修改不满足需求的地方。

之后执行flutter pub pub publish,具体可以看For details on publishing, see the publishing docs for the Pub site.