前言
熟悉flutter开发的人都知道,Flutter和iOS端原生混合开发的方式有两种:
- Flutter调用原生的部分功能(Flutter为主项目)
- Flutter作为一个模块嵌入到原生的项目中(iOS为主项目)
今天我们简单介绍下两种情况的简单使用。
Flutter调用原生的部分功能
就拿之前敲的微信的项目的我的界面来说,切换头像功能,在MinePageState中添加以下代码:
late File _avatarFile = File('');
final MethodChannel _methodChannel = MethodChannel('mine_page/method');
@override
void initState() {
// TODO: implement initState
super.initState();
_methodChannel.setMethodCallHandler((call) {
if (call.method == 'imagePath') {
print(call.arguments.toString());
String imagePath = call.arguments.toString();
setState(() {
_avatarFile = File(imagePath);
});
}
return Future(() {});
});
}
给头像添加一个点击事件:
GestureDetector(
onTap: () {
print('点击了头像');
_methodChannel.invokeMapMethod('picture');
},
child: Container(
width: 70,
height: 70,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
image: DecorationImage(
image: _avatarFile.path == ''
? const AssetImage('images/tabbar_mine_hl.png')
as ImageProvider
: FileImage(_avatarFile),
fit: BoxFit.cover),
),
),
),
找到项目中的ios文件,打开Runner.xcworkspace,添加以下代码:
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate,UIImagePickerControllerDelegate,UINavigationControllerDelegate {
var methodChannel:FlutterMethodChannel?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let vc = self.window.rootViewController as! FlutterViewController
self.methodChannel = FlutterMethodChannel.init(name: "mine_page/method", binaryMessenger: vc.binaryMessenger)
let imageVc = UIImagePickerController.init()
imageVc.delegate = self
self.methodChannel!.setMethodCallHandler { (call , result) in
if(call.method == "picture"){
vc.present(imageVc, animated: true, completion: nil)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
picker.dismiss(animated: true, completion: nil)
if #available(iOS 11.0, *) {
let url = info[UIImagePickerController.InfoKey.imageURL] as! NSURL
let imagePath = url.path;
self.methodChannel!.invokeMethod("imagePath", arguments: imagePath)
} else {
// Fallback on earlier versions
}
}
}
这样就实现了头像切换的功能了,通过MethodChannel
来实现。
就像h5和原生交互一样,MethodChannel
所对应的name
(标识mine_page/method
)要一一对应,不然就找不到要处理的事件,以及相互传参时候的标识也要对应,picture
(Flutter需要原生去干的标识), imagePath
(iOS需要传给Flutter的标识)。
Flutter作为一个模块嵌入到原生的项目中
此时创建Flutter就为module
类型的,取名dededemo
,项目创建完成,继续在dededemo
项目同层级下创建原生项目NativeDemo
:
接下来利用Cocoapods
来将flutter_module
作为一个插件集成到原生项目中。创建PodFile
文件:
platform :ios, '10.0'
# flutter module 文件路径
flutter_application_path = '../dededemo'
load File.join(flutter_application_path, '.IOS', 'Flutter', 'podhelper.rb')
target 'NativeDemo' do
install_all_flutter_pods(flutter_application_path)
use_frameworks!
end
pod install
,集成结束之后,打开原生项目,查看Pods文件
编译原生项目,发现并不报错。
接着在Main
中添加两个按钮:
分别在
ViewController
中添加相应的点击事件,代码如下:
#import "ViewController.h"
#import
@interface ViewController ()
@property (nonatomic, strong) FlutterEngine *flutterEngine;
@property (nonatomic, strong) FlutterViewController *flutterVc;
@property (nonatomic, strong) FlutterBasicMessageChannel *msgChannel;
@end
@implementation ViewController
-(FlutterEngine *)flutterEngine{
if (!_flutterEngine) {
FlutterEngine *engine = [[FlutterEngine alloc]initWithName:@"lcr"];
if (engine.run) {
_flutterEngine = engine;
}
}
return _flutterEngine;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.flutterVc = [[FlutterViewController alloc]initWithEngine:self.flutterEngine nibName:nil bundle:nil];
self.msgChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messageChannel" binaryMessenger:self.flutterVc.binaryMessenger];
[self.msgChannel setMessageHandler:^(id _Nullable message, FlutterReply _Nonnull callback) {
NSLog(@"收到Flutter的:%@",message);
}];
}
- (IBAction)pushFlutter:(id)sender {
self.flutterVc.modalPresentationStyle = UIModalPresentationFullScreen;
FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:@"one_page" binaryMessenger:self.flutterVc.binaryMessenger];
[methodChannel invokeMethod:@"one" arguments:nil];
[self presentViewController:self.flutterVc animated:YES completion:nil];
[methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
if ([call.method isEqualToString:@"exit"]) {
[self.flutterVc dismissViewControllerAnimated:YES completion:nil];
}
}];
}
- (IBAction)pushFlutter2:(id)sender {
self.flutterVc.modalPresentationStyle = UIModalPresentationFullScreen;
FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:@"two_page" binaryMessenger:self.flutterVc.binaryMessenger];
[methodChannel invokeMethod:@"two" arguments:nil];
[self presentViewController:self.flutterVc animated:YES completion:nil];
[methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
if ([call.method isEqualToString:@"exit"]) {
[self.flutterVc dismissViewControllerAnimated:YES completion:nil];
}
}];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
static int a = 0;
[self.msgChannel sendMessage:[NSString stringWithFormat:@"%d",a++]];
}
@end
原生项目中添加完代码,Flutter
中也需要添加相应的响应代码。
在main.dart
文件中输入如下代码:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
final MethodChannel _oneChannel = const MethodChannel('one_page');
final MethodChannel _twoChannel = const MethodChannel('two_page');
final BasicMessageChannel _messageChannel =
const BasicMessageChannel('messgaeChannel', StandardMessageCodec());
String pageIndex = 'one';
@override
void initState() {
// TODO: implement initState
super.initState();
_oneChannel.setMethodCallHandler((call) {
pageIndex = 'one';
print(call.method);
setState(() {});
return Future(() {});
});
_twoChannel.setMethodCallHandler((call) {
pageIndex = 'two';
print(call.method);
setState(() {});
return Future(() {});
});
_messageChannel.setMessageHandler((message) {
print('收到来自iOS的$message');
return Future(() {});
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primaryColor: Colors.blue,
),
home: _rootPage(pageIndex),
);
}
Widget _rootPage(String pageIndex) {
switch (pageIndex) {
case 'one':
return Scaffold(
appBar: AppBar(
title: Text(pageIndex),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_oneChannel.invokeListMethod('exit');
},
child: Text(pageIndex)),
TextField(
onChanged: (String str) {
print(str);
_messageChannel.send(str);
},
)
],
),
);
case 'two':
return Scaffold(
appBar: AppBar(
title: Text(pageIndex),
),
body: Center(
child: ElevatedButton(
onPressed: () {
_twoChannel.invokeListMethod('exit');
},
child: Text(pageIndex)),
));
default:
return Scaffold(
appBar: AppBar(
title: Text(pageIndex),
),
body: Center(
child: ElevatedButton(
onPressed: () {
MethodChannel('default_page').invokeListMethod('exit');
},
child: Text(pageIndex),
),
),
);
}
}
}
如上代码,利用两个MethodChannel
实例,分别对应一个原生需要Flutter处理的事件(one_page
和 two_page
),_MyAppState
中所做对应的处理就是,当_oneChannel
唤起时改变pageIndex,生成界面one,当_twoChannel
被唤起时对应的重新渲染,生成界面two,两个界面中分别添加一个按钮,可以在原生点击按钮时候可以退出界面。BasicMessageChannel 可以实现原生和Flutter之间的数据信息传输。
注意
按以上方式运行,会以下错误:
Failed to find assets path for "Frameworks/App.framework/flutter_assets"
Engine run configuration was invalid
Could not launch engine with configuration.
解决方法就是在项目中添加下面脚本:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed ${SOURCE_ROOT}/Flutter/App.framework
其中还需确认项目中Build Settings
中的User-Defined
中是否有FLUTTER_ROOT
,如果没有的话,需要添加,这个也就是你电脑里flutter安装的位置。我的是装在家目录下:
但是,这个报错并不是一定会发生的,如果发生了,也可以多次pod install ,多尝试一下,说不定下次运行就不报错了。怪怪~~
这样就能正常交互了,也就不报错了。