Este guia explora a integração de aplicações nativas iOS com Flutter, focando no uso do plugin flutter_boost da Alibaba. O objetivo é demonstrar como permitir a comunicação e a transição de páginas entre o ambiente nativo iOS e o Flutter de forma fluida.
Configuração Inicial
Para eniciar, adicione a dependência do flutter_boost ao seu arquivo pubspec.yaml:
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: '1.12.13'
Execute flutter pub get para baixar o pacote.
Estrutura do Projeto Flutter
A estrutura principal da aplicação Flutter é definida no arquivo lib/main.dart. É necessário registrar os construtores de página do Flutter e configurar os observadores de navegação:
import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'simple_page_widgets.dart'; // Importa widgets de exemplo
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<myapp> {
@override
void initState() {
super.initState();
// Registro dos construtores de páginas do Flutter
FlutterBoost.singleton.registerPageBuilders({
'embeded': (pageName, params, _) => EmbededFirstRouteWidget(),
'first': (pageName, params, _) => FirstRouteWidget(),
'second': (pageName, params, _) => SecondRouteWidget(),
'tab': (pageName, params, _) => TabRouteWidget(),
'platformView': (pageName, params, _) => PlatformRouteWidget(),
'flutterFragment': (pageName, params, _) => FragmentRouteWidget(params),
'flutterPage': (pageName, params, _) {
print("Parâmetros da página Flutter: $params");
return FlutterRouteWidget(params: params);
},
});
// Adiciona um observador para eventos de navegação
FlutterBoost.singleton.addBoostNavigatorObserver(TestBoostNavigatorObserver());
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Exemplo Flutter Boost',
// Inicializa o FlutterBoost como o builder principal
builder: FlutterBoost.init(postPush: _onRoutePushed),
home: Container(color: Colors.white), // Container inicial
);
}
// Callback após o push de uma rota
void _onRoutePushed(String pageName, String uniqueId, Map params, Route route, Future _) {
// Lógica adicional após o push, se necessário
}
}
// Observador de navegação para eventos do FlutterBoost
class TestBoostNavigatorObserver extends NavigatorObserver {
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
print("flutterboost#didPush");
}
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
print("flutterboost#didPop");
}
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
print("flutterboost#didRemove");
}
void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
print("flutterboost#didReplace");
}
}
</dynamic></dynamic></dynamic></dynamic></dynamic></dynamic></dynamic></dynamic></myapp>
Exemplos de Widgets e Páginas
O arquivo simple_page_widgets.dart contém exemplos de diferentes widgets e páginas que podem ser exibidos no Flutter:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'platform_view.dart'; // Importa o widget de visualização de plataforma
// Widget para a primeira rota
class FirstRouteWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Primeira Rota')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Botão para abrir página nativa
ElevatedButton(
child: Text('Abrir página nativa'),
onPressed: () {
print("Abrindo página nativa!");
// Abre uma rota nomeada 'native'
FlutterBoost.singleton.open("native").then((value) {
print("Página nativa finalizada com resultado: $value");
});
},
),
// Botão para abrir a segunda rota Flutter
ElevatedButton(
child: Text('Abrir segunda rota'),
onPressed: () {
print("Abrindo segunda página!");
// Abre a rota nomeada 'second'
FlutterBoost.singleton.open("second").then((value) {
print("Segunda rota finalizada com resultado: $value");
});
},
),
// Botão para apresentar a segunda rota Flutter
ElevatedButton(
child: Text('Apresentar segunda rota'),
onPressed: () {
print("Apresentando segunda página!");
// Apresenta a rota 'second' com um parâmetro
FlutterBoost.singleton.open("second", urlParams: {"present": true}).then((value) {
print("Segunda rota apresentada com resultado: $value");
});
},
),
],
),
),
);
}
}
// Widget para uma rota embarcada (exemplo)
class EmbededFirstRouteWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
child: Text('Abrir segunda rota'),
onPressed: () {
FlutterBoost.singleton.open("second");
},
),
),
);
}
}
// Widget para a segunda rota
class SecondRouteWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Segunda Rota")),
body: Center(
child: ElevatedButton(
onPressed: () {
// Obtém as configurações do container atual
BoostContainerSettings settings = BoostContainer.of(context).settings;
// Fecha a rota atual com um resultado
FlutterBoost.singleton.close(settings.uniqueId, result: {"result": "dados da segunda rota"});
},
child: Text('Retornar com resultado!'),
),
),
);
}
}
// Widget para uma rota de aba (exemplo)
class TabRouteWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Rota de Aba")),
body: Center(
child: ElevatedButton(
onPressed: () {
FlutterBoost.singleton.open("second");
},
child: Text('Abrir segunda rota'),
),
),
);
}
}
// Widget para uma rota de visualização de plataforma
class PlatformRouteWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Rota de Plataforma")),
body: Center(
child: ElevatedButton(
child: TextView(), // Exibe um widget de visualização de plataforma
onPressed: () {
FlutterBoost.singleton.open("second").then((value) {
print("Segunda rota finalizada com resultado: $value");
});
},
),
),
);
}
}
// Widget para uma rota Flutter genérica com parâmetros
class FlutterRouteWidget extends StatefulWidget {
FlutterRouteWidget({this.params, this.message});
final Map params;
final String message;
@override
_FlutterRouteWidgetState createState() => _FlutterRouteWidgetState();
}
class _FlutterRouteWidgetState extends State<flutterroutewidget> {
@override
Widget build(BuildContext context) {
final String message = widget.message;
return Scaffold(
appBar: AppBar(
brightness: Brightness.light,
backgroundColor: Colors.white,
textTheme: TextTheme(title: TextStyle(color: Colors.black)),
title: Text('Exemplo Flutter Boost'),
),
body: SingleChildScrollView(
child: Container(
margin: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <widget>[
Container(
margin: const EdgeInsets.only(top: 10.0, bottom: 20.0),
child: Text(
message ?? "Esta é uma activity Flutter\nParâmetros: ${widget.params}",
style: TextStyle(fontSize: 28.0, color: Colors.blue),
),
alignment: AlignmentDirectional.center,
),
// Exemplo de campo de texto Cupertino
const CupertinoTextField(
prefix: Icon(CupertinoIcons.person_solid, color: CupertinoColors.lightBackgroundGray, size: 28.0),
padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
placeholder: 'Nome',
),
// Botão para abrir página nativa
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text('Abrir página nativa', style: TextStyle(fontSize: 22.0, color: Colors.black)),
),
// Abre a rota nativa com parâmetros de URL
onTap: () => FlutterBoost.singleton.open("sample://nativePage", urlParams: {"query": {"aaa": "bbb"}}),
),
// Botão para abrir a primeira rota Flutter
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text('Abrir primeira rota', style: TextStyle(fontSize: 22.0, color: Colors.black)),
),
onTap: () => FlutterBoost.singleton.open("first", urlParams: {"query": {"aaa": "bbb"}}),
),
// Botão para abrir a segunda rota Flutter
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text('Abrir segunda rota', style: TextStyle(fontSize: 22.0, color: Colors.black)),
),
onTap: () => FlutterBoost.singleton.open("second", urlParams: {"query": {"aaa": "bbb"}}),
),
// Botão para abrir rota de aba
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text('Abrir aba', style: TextStyle(fontSize: 22.0, color: Colors.black)),
),
onTap: () => FlutterBoost.singleton.open("tab", urlParams: {"query": {"aaa": "bbb"}}),
),
// Botão para abrir página Flutter
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text('Abrir página Flutter', style: TextStyle(fontSize: 22.0, color: Colors.black)),
),
onTap: () => FlutterBoost.singleton.open("sample://flutterPage", urlParams: {"query": {"aaa": "bbb"}}),
),
// Botão para fazer push de um widget Flutter
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text('Push widget Flutter', style: TextStyle(fontSize: 22.0, color: Colors.black)),
),
onTap: () {
Navigator.push<dynamic>(context, MaterialPageRoute<dynamic>(builder: (_) => PushWidget()));
},
),
// Botão para push de demo de plataforma
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text('Push demo de plataforma', style: TextStyle(fontSize: 22.0, color: Colors.black)),
),
onTap: () {
Navigator.push<dynamic>(context, MaterialPageRoute<dynamic>(builder: (_) => PlatformRouteWidget()));
},
),
// Botão para abrir página fragmento Flutter
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text('Abrir página fragmento Flutter', style: TextStyle(fontSize: 22.0, color: Colors.black)),
),
onTap: () => FlutterBoost.singleton.open("sample://flutterFragmentPage"),
),
],
),
),
),
);
}
}
// Widget para um fragmento Flutter
class FragmentRouteWidget extends StatelessWidget {
final Map params;
FragmentRouteWidget(this.params);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Exemplo Flutter Boost')),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <widget>[
Container(
margin: const EdgeInsets.only(top: 80.0),
child: Text("Este é um fragmento Flutter", style: TextStyle(fontSize: 28.0, color: Colors.blue)),
alignment: AlignmentDirectional.center,
),
Container(
margin: const EdgeInsets.only(top: 32.0),
child: Text(params['tag'] ?? '', style: TextStyle(fontSize: 28.0, color: Colors.red)),
alignment: AlignmentDirectional.center,
),
Expanded(child: Container()),
// Botões de navegação
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text('Abrir página nativa', style: TextStyle(fontSize: 22.0, color: Colors.black)),
),
onTap: () => FlutterBoost.singleton.open("sample://nativePage"),
),
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text('Abrir página Flutter', style: TextStyle(fontSize: 22.0, color: Colors.black)),
),
onTap: () => FlutterBoost.singleton.open("sample://flutterPage"),
),
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 80.0),
color: Colors.yellow,
child: Text('Abrir página fragmento Flutter', style: TextStyle(fontSize: 22.0, color: Colors.black)),
),
onTap: () => FlutterBoost.singleton.open("sample://flutterFragmentPage"),
)
],
),
);
}
}
// Widget de exemplo para push
class PushWidget extends StatefulWidget {
@override
_PushWidgetState createState() => _PushWidgetState();
}
class _PushWidgetState extends State<pushwidget> {
@override
Widget build(BuildContext context) {
return FlutterRouteWidget(message: "Widget Pushado");
}
}
// Widget de visualização de plataforma (exemplo)
typedef void TextViewCreatedCallback(TextViewController controller);
class TextView extends StatefulWidget {
const TextView({Key key, this.onTextViewCreated}) : super(key: key);
final TextViewCreatedCallback onTextViewCreated;
@override
State<statefulwidget> createState() => _TextViewState();
}
class _TextViewState extends State<textview> {
@override
Widget build(BuildContext context) {
// Utiliza AndroidView para Android, Text para outras plataformas
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'plugins.test/view', // Tipo de visualização definido na configuração nativa
onPlatformViewCreated: _onPlatformViewCreated,
);
}
return Text('$defaultTargetPlatform não é suportado pelo plugin text_view');
}
void _onPlatformViewCreated(int id) {
widget.onTextViewCreated?.call(TextViewController._(id));
}
}
class TextViewController {
TextViewController._(int id) : _channel = MethodChannel('plugins.felix.angelov/textview_$id');
final MethodChannel _channel;
// Método para definir o texto na visualização nativa
Future<void> setText(String text) async {
assert(text != null);
return _channel.invokeMethod('setText', text);
}
}
</void></textview></statefulwidget></pushwidget></widget></dynamic></dynamic></dynamic></dynamic></widget></flutterroutewidget>
Configuração Nativa iOS
Podfile
Atualize o ios/Podfile para incluir o FlutterBoost e gerenciar as dependências:
# Descomente para definir uma plataforma global para seu projeto
# platform :ios, '9.0'
# CocoaPods analytics envia estatísticas de rede de forma síncrona, afetando a latência de build do Flutter.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
# Função auxiliar para parsear arquivos de configuração
def parse_KV_file(file, separator='=')
file_abs_path = File.expand_path(file)
if !File.exists? file_abs_path
return [];
end
generated_key_values = {}
skip_line_start_symbols = ["#", "/"]
File.foreach(file_abs_path) do |line|
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
plugin = line.split(pattern: separator)
if plugin.length == 2
podname = plugin[0].strip()
path = plugin[1].strip()
podpath = File.expand_path("#{path}", file_abs_path)
generated_key_values[podname] = podpath
else
puts "Especificação de plugin inválida: #{line}"
end
end
generated_key_values
end
target 'Runner' do
use_frameworks!
use_modular_headers!
# Pod do Flutter
copied_flutter_dir = File.join(__dir__, 'Flutter')
copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
# Copia Flutter.framework e Flutter.podspec se não existirem
unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
unless File.exist?(generated_xcode_build_settings_path)
raise "Generated.xcconfig deve existir. Execute 'flutter pub get' primeiro."
end
generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']
FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) unless File.exist?(copied_framework_path)
FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) unless File.exist?(copied_podspec_path)
end
pod 'Flutter', :path => 'Flutter'
# Pods de Plugins
system('rm -rf .symlinks')
system('mkdir -p .symlinks/plugins')
plugin_pods = parse_KV_file('../.flutter-plugins')
plugin_pods.each do |name, path|
symlink = File.join('.symlinks', 'plugins', name)
File.symlink(path, symlink)
pod name, :path => File.join(symlink, 'ios')
end
end
# Previne que o Cocoapods incorpore um segundo framework Flutter
install! 'cocoapods', :disable_input_output_paths => true
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end
AppDelegate.swift
Configure o AppDelegate.swift para inicializar o FlutterBoost e definir a UI principal:
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Inicializa o roteador de plataforma
let router = PlatformRouterImp();
FlutterBoostPlugin.sharedInstance().startFlutter(with: router, onStart: { (engine) in
// Callback opcional após o início do engine Flutter
});
// Configura a janela principal e o Navigation Controller
self.window = UIWindow(frame: UIScreen.main.bounds)
let viewController = ViewController() // Seu ViewController nativo
let navi = UINavigationController(rootViewController: viewController)
self.window.rootViewController = navi
self.window.makeKeyAndVisible()
return true // Retorna true para indicar que o lançamento foi bem-sucedido
}
}
PlatformRouterImp.swift
Implemente a interface FLBPlatform para gerenciar a navegação entre nativo e Flutter:
import Foundation
import flutter_boost
class PlatformRouterImp: NSObject, FLBPlatform {
// Abre uma URL/rota
func open(_ url: String, urlParams: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
var animated = false;
if exts["animated"] != nil {
animated = exts["animated"] as! Bool;
}
// Cria um container FlutterViewContainer para a rota Flutter
let vc = FLBFlutterViewContainer.init();
vc.setName(url, params: urlParams);
// Navega para a nova página
navigationController().pushViewController(vc, animated: animated);
completion(true);
}
// Apresenta uma URL/rota
func present(_ url: String, urlParams: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
var animated = false;
if exts["animated"] != nil {
animated = exts["animated"] as! Bool;
}
let vc = FLBFlutterViewContainer.init();
vc.setName(url, params: urlParams);
// Apresenta o container modally
navigationController().present(vc, animated: animated) {
completion(true);
};
}
// Fecha uma rota
func close(_ uid: String, result: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
var animated = false;
if exts["animated"] != nil {
animated = exts["animated"] as! Bool;
}
// Verifica se a rota a ser fechada é a apresentada modally
let presentedVC = self.navigationController().presentedViewController;
if let vc = presentedVC as? FLBFlutterViewContainer, vc.uniqueIDString() == uid {
vc.dismiss(animated: animated, completion: {
completion(true);
});
} else {
// Caso contrário, faz o pop da pilha de navegação
self.navigationController().popViewController(animated: animated);
completion(true); // Assume sucesso para pop
}
}
// Retorna o UINavigationController ativo
func navigationController() -> UINavigationController {
let delegate = UIApplication.shared.delegate as! AppDelegate
let navigationController = delegate.window?.rootViewController as! UINavigationController
return navigationController;
}
}
ViewController.swift (Exemplo Nativo)
Um exemplo de ViewController nativo que pode iniciar páginas Flutter:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
// Ação para botão que abre uma página Flutter (push)
@IBAction func onClickPushFlutterPage(_ sender: UIButton, forEvent event: UIEvent){
// Abre a rota 'first' com um ID de callback e animação
FlutterBoostPlugin.open("first", urlParams:[kPageCallBackId:"MycallbackId#1"], exts: ["animated":true], onPageFinished: { (_ result:Any?) in
print(String(format:"Página finalizada com resultado: %@", result as! CVarArg));
}) { (f:Bool) in
print(String(format:"Página aberta: %d", f));
}
}
// Ação para botão que apresenta uma página Flutter (modal)
@IBAction func onClickPresentFlutterPage(_ sender: UIButton, forEvent event: UIEvent){
// Apresenta a rota 'second' com um ID de callback e animação
FlutterBoostPlugin.present("second", urlParams:[kPageCallBackId:"MycallbackId#2"], exts: ["animated":true], onPageFinished: { (_ result:Any?) in
print(String(format:"Página finalizada com resultado: %@", result as! CVarArg));
}) { (f:Bool) in
print(String(format:"Página apresentada: %d", f));
}
}
}
Execução
Após configurar ambos os lados (Flutter e nativo iOS), você pode compilar e executar o projeto. A interação entre as plataformas será gerenciada pelo flutter_boost.