混合开发分两种情况
回到之前的仿微信的我的界面来做一个点击头像通过相册更换图片的功能。
在之前的现实头像的地方添加一个GestureDetector,并且添加onTap方法。
Flutter里面和原生通讯用的是Flutter专门提供的MethodChannel。
这里声明一个_methodChannel
MethodChannel _methodChannel = MethodChannel('mine_page/method');
然后在onTap使用_methodChannel里面的invokeMapMethod方法,这个时候就通知原生了。
onTap: () {
_methodChannel.invokeMapMethod('picture');
},
然后这个时候来到iOS,在appDelegate里面处理methodChannel。
let vc = self.window.rootViewController
let channel = FlutterMethodChannel.init(name: "mine_page/method", binaryMessenger: vc as! FlutterBinaryMessenger)
channel.setMethodCallHandler { call, result in
if (!call.method.isEmpty && call.method == "picture" ) {
let imageVC = UIImagePickerController()
vc?.present(imageVC, animated: true, completion: nil)
}
}
这时候Flutter通讯到iOS端已经没问题了,接下来还要接受iOS返回的数据,这里也就是照片。
这时候把channel抽取出来做一个属性,然后AppDelegate遵守UIImagePickerControllerDelegate和UINavigationControllerDelegate,在UIImagePickerControllerDelegate的didFinishPickingMediaWithInfo里面拿到选中图片的url并且使用channel调用invokeMethod返回给flutter。
@objc class AppDelegate: FlutterAppDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
var channel: FlutterMethodChannel?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let vc: FlutterViewController = self.window.rootViewController as! FlutterViewController
channel = FlutterMethodChannel.init(name: "mine_page/method", binaryMessenger: vc as! FlutterBinaryMessenger)
let imageVC = UIImagePickerController()
imageVC.delegate = self
channel!.setMethodCallHandler { call, result in
if (!call.method.isEmpty && 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) {
let urlString:String = (info[UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerImageURL")] as! NSURL).absoluteString ?? ""
self.channel?.invokeMethod("imagePath", arguments: urlString)
}
}
}
Flutter 中,在initState中添加调用setMethodCallHandler添加对imagePath的处理
这样点击后就可以获得图片的url 了。
获取图片的url之后,那么就可以根据这个url来更换头像了。
声明一个可选File属性。
File? _avatarFile;
在setMethodCallHandler里面赋值
_methodChannel.setMethodCallHandler((call) async {
if (call.method == "imagePath") {
String imagePath = call.arguments.toString().substring(7);
setState(() {
_avatarFile = File(imagePath);
});
}
});
在头像里面的image判断_avatarFile是否为空,为空则显示默认图片,否则就显示选择的图片。
GestureDetector(
child: Container(
width:70,
height:70,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
image: DecorationImage(image: _avatarFile == null ? AssetImage('images/Hank.png') : FileImage(_avatarFile!) as ImageProvider,fit: BoxFit.cover)
),
),
onTap: () {
_methodChannel.invokeMapMethod('picture');
},
),
imagePicke可以同时实现ios和安卓的图片选择。
将头像的点击抽取出来成一个方法
onTap: _pickImage,
imagePicke不仅可以打开相册,还能直接返回选择结果,这样就大大减少了代码量。注意,这里iOS需要到info.plist里面配置相关权限,否则就会奔溃。
void _pickImage() async{
XFile? file = await ImagePicker().pickImage(source: ImageSource.gallery);
setState(() {
_avatarFile = File( file!.path);
});
}
如果原生项目要嵌入Flutter页面,那么就不能创建Flutter App,而是需要创建Flutter Module。这个时候,创建的ios和android 的文件夹就是隐藏文件了,只是用来调试用的。这里的原生代码不会被打包进去,所以不能在Flutter Module 里面写原生代码。
首先创建一个原生项目,而原生项目要使用Flutter Module,那么就需要用到cocoapods。在新建的xcode项目文件夹里面调用 pod init。接着打开podfile添加Flutter。
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
flutter_application_path = "../flutter_module"
load File.join(flutter_application_path,'.iOS','Flutter','podhelper.rb')
target 'FlutterTestDemo' do
# Comment the next line if you don't want to use dynamic frameworks
install_all_flutter_pods(flutter_application_path)
use_frameworks!
# Pods for FlutterTestDemo
end
这里的flutter_application_path是创建的flutter_module里面podhelper.rb所在的位置。
然后就 pod install 就好了。这时候可以打开iOS项目看看是否能够import Flutter,如果可以就安装成功了。
创建一个按钮,添加点击事件,点击之后就调用Flutter界面
- (IBAction)pushFlutter:(id)sender {
FlutterViewController *vc = [[FlutterViewController alloc] init];
[self presentViewController:vc animated:true completion:nil];
}
点击之后成功调用Flutter界面
将Flutter 里面的 You have pushed the button this many times: 修改成 times:,重新运行后发现文字没有改变。这是因为这里Flutter 的Framework并没有加载,这里需要xcode清理缓存之后重新运行,这样就会重新加载Flutter 的Framework,然后界面修改就展示出来了。
这里注意到内存暴涨到了80多M,这是因为程序中添加了Flutter的渲染引擎,而即使后面关掉了Flutter页面,渲染引擎依然还存在,还是要占用内存。
这里在创建一个按钮,然后点击的时候也跳转一个Flutter页面
- (IBAction)pushFlutterTwo:(id)sender {
FlutterViewController *vc = [[FlutterViewController alloc] init];
[self presentViewController:vc animated:true completion:nil];
}
那么这里如何标记显示不同的页面呢?这里看到setInitialRoute方法,可以看到注释也是写到不建议使用。如果要用的话在initWithEngine的时候进行路由的设置。也就是说我们并不是创建一个VC,而是创建一个引擎,并且这个引擎应该只有一个。
这里先用setInitialRoute,给第一个按钮传值字符串one,给第二个按钮传值字符串two。
- (IBAction)pushFlutter:(id)sender {
FlutterViewController *vc = [[FlutterViewController alloc] init];
[vc setInitialRoute:@"one"];
[self presentViewController:vc animated:true completion:nil];
}
- (IBAction)pushFlutterTwo:(id)sender {
FlutterViewController *vc = [[FlutterViewController alloc] init];
[vc setInitialRoute:@"two"];
[self presentViewController:vc animated:true completion:nil];
}
在Flutter 里面修改页面,这里接受iOS传进来的Route值,然后根据这个值返回不同的界面。
import 'dart:ui';
import 'package:flutter/material.dart';
void main() => runApp( MyApp(
pageIndex: window.defaultRouteName,
));
class MyApp extends StatelessWidget {
final String pageIndex;
const MyApp({Key? key, required this.pageIndex}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
),
home: _rootPage(pageIndex),
);
}
// 根据pageIndex返回页面
Widget _rootPage(String pageIndex) {
switch(pageIndex) {
case 'one':
return Scaffold(
appBar: AppBar(title: Text(pageIndex),),
body: Center(child: Text(pageIndex),),
);
case 'two':
return Scaffold(
appBar: AppBar(title: Text(pageIndex),),
body: Center(child: Text(pageIndex),),
);
default:
return Scaffold(
appBar: AppBar(title: Text(pageIndex),),
body: Center(child: Text(pageIndex),),
);
}
}
}
这里还需要在Flutter里面添加dismiss功能。
这里先到Flutter里面将MyApp修改成StatefulWidget,并且这里不需要传pageIndex了,这里在_MyAppState里面创建属性pageIndex,_oneChannel,_twoChannel,然后监听从原生传过来的值,然后设置pageIndex的值,再将返回的widget里面添加button,并且各自的channel调用invokeMapMethod传值。
void main() => runApp( MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
highlightColor: const Color.fromRGBO(1, 0, 0, 0.0),
splashColor: const Color.fromRGBO(1, 0, 0, 0.0),
primarySwatch: Colors.grey,
),
home: const MyApp(),
));
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
String pageIndex = 'one';
final _oneChannel = MethodChannel('one_page');
final _twoChannel = MethodChannel('two_page');
@override
void initState() {
// TODO: implement initState
super.initState();
_oneChannel.setMethodCallHandler((call) {
setState(() {
pageIndex = call.method;
});
return Future(() {});
});
_twoChannel.setMethodCallHandler((call) {
setState(() {
pageIndex = call.method;
});
return Future(() {});
});
}
@override
Widget build(BuildContext context) {
return _rootPage(pageIndex!);
}
Widget _rootPage(String pageIndex) {
switch (pageIndex) {
case 'one':
return Scaffold(
appBar: AppBar(
title: Text(pageIndex),
),
body: ElevatedButton(
onPressed: () {
_oneChannel.invokeMapMethod('exit');
},
child: Text(pageIndex),
),
);
case 'two':
return Scaffold(
appBar: AppBar(
title: Text(pageIndex),
),
body: ElevatedButton(
onPressed: () {
_twoChannel.invokeMapMethod('exit');
},
child: Text(pageIndex),
),
);
default:
return Scaffold(
appBar: AppBar(
title: Text(pageIndex),
),
body: ElevatedButton(
onPressed: () {
MethodChannel('default_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
),
);
}
}
}
接着修改iOS代码,这里先创建一个引擎,并且调用run方法。
@property (nonatomic,strong) FlutterEngine *flutterEngine;
// 懒加载
- (FlutterEngine *) flutterEngine{
if (!_flutterEngine) {
FlutterEngine *engine = [[FlutterEngine alloc] initWithName:@"FlutterEngine"];
if (engine.run) {
_flutterEngine = engine;
}
}
return _flutterEngine;
}
声明一个flutterVc
@property (nonatomic,strong) FlutterViewController *flutterVc;
然后在viewDidLoad初始化flutterVc,这里还可以避免后面调用会卡顿。
self.flutterVc = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
这样就可以使用present同一个flutterVc,然后创建methodChannel,调用methodChannel的invokeMethod方法传不同的值 ,然后使用methodChannel来监听flutter的信息,如果传过来’exit’,那么就调用dismissViewController方法。
- (IBAction)pushFlutter:(id)sender {
//创建MethodChannel
FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:@"one_page" binaryMessenger:self.flutterVc.binaryMessenger];
//告诉Flutter对应的页面
[methodChannel invokeMethod:@"one" arguments:nil];
//弹出VC
[self presentViewController:self.flutterVc animated:true completion:nil];
//监听退出
[methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
if ([call.method isEqualToString:@"exit"]) {
[self dismissViewControllerAnimated:true completion:nil];
}
}];
}
- (IBAction)pushFlutterTwo:(id)sender {
//创建MethodChannel
FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:@"two_page" binaryMessenger:self.flutterVc.binaryMessenger];
//告诉Flutter对应的页面
[methodChannel invokeMethod:@"two" arguments:nil];
//弹出VC
[self presentViewController:self.flutterVc animated:true completion:nil];
//监听退出
[methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
if ([call.method isEqualToString:@"exit"]) {
[self dismissViewControllerAnimated:true completion:nil];
}
}];
}
还有一个常用的channel : BasicMessageChannel 是用来做持续通讯的,收到消息之后还可以回复消息。
在Flutter里面创建一个BasicMessageChannel,这里需要传两个参数,一个是名字一个是解码器。
final BasicMessageChannel _basicMessageChannel = BasicMessageChannel('messageChannel',StandardMessageCodec());
然后到initState使用_basicMessageChannel来监听来自ios的信息。
_basicMessageChannel.setMessageHandler((message) {
print('收到来自ios的$message');
return Future(() {});
});
然后在_rootPage将body修改成Column,添加一个TextField然后在输入改变的时候传输里面的值给原生。
Widget _rootPage(String pageIndex) {
switch (pageIndex) {
case 'one':
return Scaffold(
appBar: AppBar(
title: Text(pageIndex),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_oneChannel.invokeMapMethod('exit');
},
child: Text(pageIndex),
),
TextField(onChanged: (String str){
_basicMessageChannel.send(str);
},)
],
),
);
case 'two':
return Scaffold(
appBar: AppBar(
title: Text(pageIndex),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_oneChannel.invokeMapMethod('exit');
},
child: Text(pageIndex),
),
TextField(onChanged: (String str){
_basicMessageChannel.send(str);
},)
],
),
);
default:
return Scaffold(
appBar: AppBar(
title: Text(pageIndex),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_oneChannel.invokeMapMethod('exit');
},
child: Text(pageIndex),
),
TextField(onChanged: (String str){
_basicMessageChannel.send(str);
},)
],
),
);
}
}
接下来在iOS里面创建FlutterBasicMessageChannel并添加监听
@property (nonatomic,strong) FlutterBasicMessageChannel *flutterBasicMessageChannel;
self.flutterBasicMessageChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messageChannel" binaryMessenger:self.flutterVc.binaryMessenger];
[self.flutterBasicMessageChannel setMessageHandler:^(id _Nullable message, FlutterReply _Nonnull callback) {
NSLog(@"收到Flutter的消息:%@",message);
}];
这样就可以监听到Flutter里面TextFiled输入的变化。
在iOS里面添加touchesBegan,并给Flutter传消息。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
static int a = 0;
[self.flutterBasicMessageChannel sendMessage:[NSString stringWithFormat:@"%d",a++]];
}