混合開發(三)iOS原生調用Flutter
Flutter 項目 調用一些原生的功能!用的比較多的就是第三方插件,因爲比較簡單
官方 《Flutter實戰》
原生項目中部分頁面使用Flutter,這種也是比較常見的。
FLutter本身定位的是開發一個完整的App應用。所以要是隻讓其做成一個頁面的話有些功能是不支持的。Flutter本身有自己的渲染引擎,如果是小項目用Flutter就不划算,只有非常大型的項目將其部分或者全部頁面用Flutter來實現。
詳細代碼參見Demo
1、FLutter Module
模塊創建
創建出來的工程看一下
打開文件路徑可以去看一下,是隱藏文件
隱藏的目的是,官方不希望我們對這些文件進行操作。我們開發的是一個Flutter頁面是能運行的,這裏的ios 和 Android 文件只是爲了讓我們做測試用的。當我們把這個Flutter寫好之後,是要集成到原生項目中使用的,並不需要那些隱藏的內容。
擴展:
打開終端輸入命令:(由於系統不一樣,有可能無效,可以自行上網查閱)
1、顯示隱藏文件/文件夾
$ defaults write com.apple.finder AppleShowAllFiles -boolean true ; killall Finder
2、隱藏隱藏文件/文件夾
$ defaults write com.apple.finder AppleShowAllFiles -boolean false ; killall Finder
2、新建iOS項目
要讓Flutter和我們的iOS項目產生管理,使用pod進行管理
先生成Podfiile 文件,直接打開終端,cd 打開iOS項目路徑。pod init ,pod install。之後就可以再Xcode 裏面查看了。
liujilou@liujiloudeMacBook-Pro ~ % cd /Users/liujilou/Desktop/code/AiOSFlutterModule/NativeDemo
liujilou@liujiloudeMacBook-Pro NativeDemo % pod init
liujilou@liujiloudeMacBook-Pro NativeDemo % pod install
然後用Xcode 打開工程,編輯Podfile 文件。這裏的引用格式的Flutter官網提供的,可以去查閱
flutter_application_path = '../flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')platform :ios, '9.0'
target 'NativeDemo' do
install_all_flutter_pods(flutter_application_path)
use_frameworks!
end
注意:這個Flutter 的路徑是相對路徑,如果修改改了路徑,這裏記得要改變
重新 pod install
這樣就關聯成功了,一些 Flutter 的內容就安裝進去了
這個時候我們也可以到ViewController 裏面試一下是否成功了。
#import <Flutter/Flutter.h> 頭文件能導入就說明成功了
3、調用 Flutter 頁面(一)不推薦
iOS裏面先創建2個按鈕,然後去打開Flutter頁面
3.1、iOS頁面
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
CGSize viewSize = self.view.frame.size;
UIButton * button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button1.frame = CGRectMake((viewSize.width-100)/2, 100, 100, 40);
button1.backgroundColor = [UIColor orangeColor];
button1.tag = 1001;
[button1 setTitle:@"按鈕一" forState:(UIControlStateNormal)];
[button1 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)];
[self.view addSubview:button1];
UIButton * button2 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button2.frame = CGRectMake((viewSize.width-100)/2, 200, 100, 40);
button2.backgroundColor = [UIColor greenColor];
button2.tag = 1002;
[button2 setTitle:@"按鈕二" forState:(UIControlStateNormal)];
[button2 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)];
[self.view addSubview:button2];
}
-(void)pushFlutter:(UIButton *)btn
{
NSString * pageIndex = @"one";
NSString * page = @"one_page";
if (btn.tag == 1002) {
pageIndex = @"two";
page = @"two_page";
}
// 不要每次都去alloc init 一個新的FLutter,這樣非常消耗性能
FlutterViewController * vc = [[FlutterViewController alloc] init];
[vc setInitialRoute:pageIndex];//初始化傳給Flutter的值
[self presentViewController:vc animated:YES completion:nil];
}
我們這裏用的是跟iOS一樣的調用方式,每次點擊按鈕的時候去重新創建Flutter頁面。
如果全屏顯示Flutter頁面,在Flutter中要做回退。我們先不做全屏顯示,先演示後面做
3.2、Flutter的頁面
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp(
//window 需要導入import 'dart:ui';
//window.defaultRouteName 拿到的就是 iOS中寫的 [vc setInitialRoute:@"one"];帶過來的值 one
pageIndex: window.defaultRouteName));
// ----------------------------------------------------
class MyApp extends StatelessWidget {
final String pageIndex;
const MyApp({Key key, this.pageIndex}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: rootPage(pageIndex),
);
}
rootPage(String pageIndex) {
switch (pageIndex) {
case 'one':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: RaisedButton(
onPressed: () {
// 直接退出頁面了,一般不會這麼做。
MethodChannel('one_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
)));
case 'two':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: RaisedButton(
onPressed: () {
MethodChannel('two_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
)),
);
}
}
}
- iOS通過初始化的時候傳值給Flutter [vc setInitialRoute:pageIndex];
- Flutter通過 window.defaultRouteName 拿到iOS傳過來的值,然後去判斷 顯示AppBar 等。然後 可以通過 MethodChannel('one_page').invokeMapMethod('exit'); 退出Flutter頁面,並給iOS發送消息傳值。
但是Flutter 創建之後是一直存在在內存中的,而且非常大
通過上圖可以看到,一個空的Flutter都非常大。而且每次創建的話Flutter頁面的數據不能保存。因爲Flutter要有自己的渲染引擎,不能像iOS的頁面一樣這樣創建,所以建議全局建一個 Flutter 引擎。
4、Flutter 頁面調用(二)推薦
@property (nonatomic, strong) FlutterEngine * flutterEngine;
@property (nonatomic, strong) FlutterViewController * flutterVC;
Flutter 引擎
- (FlutterEngine *)flutterEngine
{
if (!_flutterEngine) {
//這裏不要直接用 _flutterEngine ,然後 _flutterEngine.run 因爲在頁面將要顯示的時候纔去執行運行,那麼Flutter的頁面顯示的會非常慢
//_flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"];
//定義一個局部變量,判斷一下如果這個flutterEngine已經運行起來了,那麼我們的全局_flutterEngine就等於這個 flutterEngine 。失敗的話就返回nil
FlutterEngine * flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"];
if (flutterEngine.run) {//Flutter 運行運行起來了
_flutterEngine = flutterEngine;
}
}
return _flutterEngine;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.flutterVC = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
self.flutterVC.modalPresentationStyle = UIModalPresentationFullScreen;//模態展示風格(全屏顯示)
-(void)pushFlutter:(UIButton *)btn
{
NSString * pageIndex = @"one";
NSString * page = @"one_page";
if (btn.tag == 1002) {
pageIndex = @"two";
page = @"two_page";
}
[self presentViewController:self.flutterVC animated:YES completion:nil];
}
4.1、問題
// 直接這樣寫就會出錯,崩潰.
window.defaultRouteName 是空的
爲什麼是空的呢?因爲在run的時候就去運行了Flutter,但是這一個時候並沒有去傳值。
可以通過單獨運行Flutter 代碼看一下值,和報錯信息
調試
因爲 [vc setInitialRoute:pageIndex]; 傳值是在初始化的時候傳的,我們現在不每次都去創建Flutter了所以需要改一下。Flutter也不能通過 window.defaultRouteName 來取值了。
修改Flutter代碼
4.2、Flutter
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(_MyApp());
// ----------------------------------------
class _MyApp extends StatefulWidget {
@override
__MyAppState createState() => __MyAppState();
}
class __MyAppState extends State<_MyApp> {
String pageIndex = 'one';
// 這裏是解碼器,互相調用
final MethodChannel _oneChannel = MethodChannel('one_page');
final MethodChannel _twoChannel = MethodChannel('two_page');
@override
void initState() {
// 調用方法一次接收信息
_oneChannel.setMethodCallHandler((call) {
setState(() {
pageIndex = call.method;
});
return null;
});
_twoChannel.setMethodCallHandler((call) {
setState(() {
pageIndex = call.method;
});
return null;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: rootPage(pageIndex),
);
}
rootPage(String pageIndex) {
switch (pageIndex) {
case 'one':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () {
// 直接退出頁面了,一般不會這麼做。
MethodChannel('one_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
),
],
));
case 'two':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: RaisedButton(
onPressed: () {
MethodChannel('two_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
)),
);
}
}
}
5、通訊
1、FlutterMethodChannel 調用方法(method invocation)一次通訊
//下面的這裏兩種都是持續通訊的
2、 FlutterBasicMessageChannel :傳遞字符串&半結構化的信息
3、FlutterEventChannel :用於數據流(stream)的通訊
完整的代碼
5.1、iOS
// ViewController.m
// NativeDemo
//
// Created by liujilou on 2020/6/28.
// Copyright © 2020 liujilou. All rights reserved.
//
// FlutterMethodChannel 調用方法(method invocation)一次通訊
//下面的這裏兩種都是持續通訊的
// FlutterBasicMessageChannel :傳遞字符串&半結構化的信息
// FlutterEventChannel :用於數據流(stream)的通訊
#import "ViewController.h"
#import <Flutter/Flutter.h>
@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 ,然後 _flutterEngine.run 因爲在頁面將要顯示的時候纔去執行運行,那麼Flutter的頁面顯示的會非常慢
//_flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"];
//定義一個局部變量,判斷一下如果這個flutterEngine已經運行起來了,那麼我們的全局_flutterEngine就等於這個 flutterEngine 。失敗的話就返回nil
FlutterEngine * flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"];
if (flutterEngine.run) {//Flutter 運行運行起來了
_flutterEngine = flutterEngine;
}
}
return _flutterEngine;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.flutterVC = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
self.flutterVC.modalPresentationStyle = UIModalPresentationFullScreen;//模態展示風格(全屏顯示)
// 接收Flutter 的數據
// 這裏因爲messenger 需要 FlutterBinaryMessenger 類型所以報警告
self.msgChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messageChannel" binaryMessenger:self.flutterVC];
[self.msgChannel setMessageHandler:^(id _Nullable message, FlutterReply _Nonnull callback) {
NSLog(@"收到Flutter 的%@",message);
}];
CGSize viewSize = self.view.frame.size;
UIButton * button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button1.frame = CGRectMake((viewSize.width-100)/2, 100, 100, 40);
button1.backgroundColor = [UIColor orangeColor];
button1.tag = 1001;
[button1 setTitle:@"按鈕一" forState:(UIControlStateNormal)];
[button1 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)];
[self.view addSubview:button1];
UIButton * button2 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button2.frame = CGRectMake((viewSize.width-100)/2, 200, 100, 40);
button2.backgroundColor = [UIColor greenColor];
button2.tag = 1002;
[button2 setTitle:@"按鈕二" forState:(UIControlStateNormal)];
[button2 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)];
[self.view addSubview:button2];
}
-(void)pushFlutter:(UIButton *)btn
{
NSString * pageIndex = @"one";
NSString * page = @"one_page";
if (btn.tag == 1002) {
pageIndex = @"two";
page = @"two_page";
}
// 不要每次都去alloc init 一個新的FLutter,這樣非常消耗性能
// FlutterViewController * vc = [[FlutterViewController alloc] init];
// 直接這樣寫就會出錯,奔潰.
// window.defaultRouteName 是空的
// FlutterViewController * vc = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
//
// vc.modalPresentationStyle = UIModalPresentationFullScreen;//模態跳轉頁面全屏顯示
// 問題就出來 Route,Route在run之後,所以Flutter收不到初始化的數據,這裏就不用Route 了。
// 因爲Route本身就是在初始化的時候傳值用的,我們既然不想讓每次都去重新創建,那就不會每次都初始化所以這裏用Route 就不合適了
// [vc setInitialRoute:pageIndex];//初始化的時候帶過去的值
// [self presentViewController:vc animated:YES completion:nil];
// 使用Channel 通道傳值
FlutterMethodChannel * methodChannel = [FlutterMethodChannel methodChannelWithName:page binaryMessenger:self.flutterVC];
// 發送消息
[methodChannel invokeMethod:pageIndex arguments:nil];
[self presentViewController:self.flutterVC animated:YES completion:nil];
// 監聽 Flutter 回調回來的參數
[methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
if ([call.method isEqualToString:@"exit"]) {
[self.flutterVC dismissViewControllerAnimated:YES completion:nil];
}
}];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int a = 0;
[self.msgChannel sendMessage:[NSString stringWithFormat:@"%d",a++]];
}
@end
做持續通訊
iOS點擊屏幕,就向Flutter發送消息。將a 值傳過去
5.2、Flutter
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(_MyApp()
// MyApp(
// window 需要導入import 'dart:ui';
// window.defaultRouteName 拿到的就是 iOS中寫的 [vc setInitialRoute:@"one"];帶過來的值 one
// pageIndex: window.defaultRouteName
// )
);
// ----------------------------------------------------
class MyApp extends StatelessWidget {
final String pageIndex;
const MyApp({Key key, this.pageIndex}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: rootPage(pageIndex),
);
}
rootPage(String pageIndex) {
switch (pageIndex) {
case 'one':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: RaisedButton(
onPressed: () {
// 直接退出頁面了,一般不會這麼做。
MethodChannel('one_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
)));
case 'two':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: RaisedButton(
onPressed: () {
MethodChannel('two_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
)),
);
}
}
}
// ----------------------------------------
class _MyApp extends StatefulWidget {
@override
__MyAppState createState() => __MyAppState();
}
class __MyAppState extends State<_MyApp> {
String pageIndex = 'one';
// 這裏是解碼器,互相調用
final MethodChannel _oneChannel = MethodChannel('one_page');
final MethodChannel _twoChannel = MethodChannel('two_page');
// 這個是通訊,也需要一個解碼器
final BasicMessageChannel _messageChannel =
BasicMessageChannel('messageChannel', StandardMessageCodec());
@override
void initState() {
// 可以持續接收信息
_messageChannel.setMessageHandler((message) {
print('收到了來自iOS的:$message');
return null;
});
// 調用方法一次接收信息
_oneChannel.setMethodCallHandler((call) {
setState(() {
pageIndex = call.method;
});
return null;
});
_twoChannel.setMethodCallHandler((call) {
setState(() {
pageIndex = call.method;
});
return null;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: rootPage(pageIndex),
);
}
rootPage(String pageIndex) {
switch (pageIndex) {
case 'one':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () {
// 直接退出頁面了,一般不會這麼做。
MethodChannel('one_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
),
TextField(
// 輸入框寫數據,向iOS發送數據
onChanged: (String str) {
_messageChannel.send(str);
},
)
],
));
case 'two':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: RaisedButton(
onPressed: () {
MethodChannel('two_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
)),
);
}
}
}
Flutter 和原生頁面不要頻繁來回切換,內存消耗會非常大。
同時Flutter 銷燬是不會完全銷燬的,所以就不要去銷燬了,就整體保存一份引擎避免重複創建。
混合開發
可以利用FLutter 作爲項目的主要開發框架
也可以如上,將FLutter作爲一個業務,iOS做框架。但是還是那句話不要頻繁的來回切換。