Integração Nativa iOS com Flutter Utilizando FlutterBoost

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.

Tags: Flutter ios integração nativa flutterboost navegacao

Publicado em 7-31 15:55