Este guia explora a implementação de desenvolvimento híbrido em projetos iOS nativos (Swift) utilizando o Flutter como um módulo embarcado.
I. Criação do Módulo Flutter
Para iniciar, crie um novo módulo Flutter seguindo as diretrizes da documentação oficial. Navegue até o diretório desejado e execute o comando:
flutter create --template module flutter_module
Após a criação, abra o projeto flutter\_module e edite o arquivo pubspec.yaml para adicionar as dependências necessárias. Execute o comando para atualziar as dependências.
Para gerar os artefatos de pod, execute o comando flutter run.
Crie um novo projeto Swift. No Podfile do projeto Swift, adicione as seguintes configurações:
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
flutter_module_path = './flutter_module' # Caminho para o diretório do seu módulo Flutter
require File.join(flutter_module_path, '.ios', 'Flutter', 'podhelper.rb')
target 'SeuProjetoIOS' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Flutter
install_all_flutter_pods(flutter_module_path)
# Pods for SeuProjetoIOS
end
post_install do |installer|
flutter_post_install(installer) if defined?(flutter_post_install)
end
Execute o comando pod install. É crucial reexecutar este comando sempre que o arquivo pubspec.yaml do módulo Flutter for modificado.
Inicialize um FlutterEngine na classe AppDelegate da sua aplicação. Este motor será exposto como uma propriedade para acesso externo.
No SceneDelegate.swift:
// Exemplo de inicialização do FlutterEngine no SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var flutterEngine = FlutterEngine(name: "MyFlutterEngine")
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Inicia o Flutter Engine
flutterEngine.run()
// ... resto da configuração da cena
}
// ... outros métodos do SceneDelegate
}
Ou na AppDelegate.swift:
// Exemplo de inicialização do FlutterEngine na AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Inicia o Flutter Engine
self.flutterEngine = (UIApplication.shared.delegate as? AppDelegate)!.flutterEngine
self.flutterEngine.run()
// ... resto da configuração doFinishLaunchingWithOptions
return true
}
// Adicione esta propriedade à sua AppDelegate
lazy var flutterEngine = FlutterEngine(name: "MyFlutterEngine")
Para utilizar o Flutter em um ViewController:
import UIKit
import Flutter
class FlutterViewControllerWrapper: UIViewController {
var flutterEngine: FlutterEngine?
override func viewDidLoad() {
super.viewDidLoad()
guard let flutterEngine = flutterEngine else { return }
let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
// Configuração para apresentar o FlutterViewController
addChild(flutterViewController)
view.addSubview(flutterViewController.view)
flutterViewController.view.frame = view.bounds
flutterViewController.didMove(toParent: self)
}
}
II. Comunicação entre Flutter e Nativo
A comunicação entre Flutter e o código nativo (iOS) é viabilizada por meio de canais de mensagens flexíveis.
- O código Flutter envia mensagens para a aplicação nativa hospedeira (iOS) através de Platform Channels.
- A aplicação nativa escuta esses canais, recebe as mensagens, invoca APIs específicas da plataforma e retorna respostas para o código Flutter.
Existem três tipos principais de canais para interação:
- MethodChannel: Ideal para invocações de métodos. Permite que o Flutter chame métodos nativos e receba resultados.
- BasicMessageChannel: Usado para troca de mensagens mais simples, como strings e dados semi-estruturados. Menos comum que o MethodChannel.
- EventChannel: Projetado para fluxos de dados contínuos (event streams). Útil para receber notificações nativas em tempo real, como mudanças no nível da bateria ou status da rede.
Todos os canais compartilham uma estrutura semelhante:
- name: Um identificador único (String) para o canal.
- messager: Uma instância de
BinaryMessengerresponsável pela transmissão e recepção de mensagens. - codec: Um
MessageCodecouMethodCodecque define como as mensagens são codificadas e decodificadas.
1. Uso do MethodChannel
Código Flutter:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class MethodChannelDemo extends StatefulWidget {
const MethodChannelDemo({Key? key}) : super(key: key);
@override
_MethodChannelDemoState createState() => _MethodChannelDemoState();
}
class _MethodChannelDemoState extends State<MethodChannelDemo> {
// Inicializa o MethodChannel com um nome único
static const platform = MethodChannel('com.example.myapp/native_methods');
String _nativeResponse = 'Aguardando resposta nativa...';
String _flutterDataSent = '';
Future<void> _invokeNativeMethod() async {
try {
// Envia dados para o método nativo 'processData'
final dynamic result = await platform.invokeMethod('processData', {
'inputString': 'Olá do Flutter!',
'inputNumber': 42,
});
setState(() {
_nativeResponse = 'Resposta Nativa: $result';
});
_flutterDataSent = 'Dados enviados: Olá do Flutter!, 42';
} on PlatformException catch (e) {
setState(() {
_nativeResponse = "Erro ao chamar método nativo: ${e.message}";
});
}
}
@override
void initState() {
super.initState();
// Configura um handler para receber chamadas do nativo
platform.setMethodCallHandler(_handleNativeCalls);
}
Future<dynamic> _handleNativeCalls(MethodCall call) async {
if (call.method == 'notifyFromNative') {
final Map<String, dynamic> args = call.arguments;
final String message = args['message'] ?? 'Mensagem não especificada';
setState(() {
_nativeResponse = 'Notificação Nativa: $message';
});
return 'Mensagem recebida com sucesso no Flutter'; // Retorno opcional para o nativo
}
// Retorna um erro se o método não for reconhecido
throw MissingPluginException();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Demonstração MethodChannel'),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_flutterDataSent.isNotEmpty ? _flutterDataSent : 'Nenhum dado enviado ainda.'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _invokeNativeMethod,
child: const Text('Enviar Dados para Nativo e Receber Resposta'),
),
const SizedBox(height: 20),
Text(_nativeResponse),
],
),
),
);
}
}
2. Uso do BasicMessageChannel
O BasicMessageChannel permite comunicação bidirecional assíncrona.
Código Flutter:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class BasicMessageChannelDemo extends StatefulWidget {
const BasicMessageChannelDemo({Key? key}) : super(key: key);
@override
_BasicMessageChannelDemoState createState() => _BasicMessageChannelDemoState();
}
class _BasicMessageChannelDemoState extends State<BasicMessageChannelDemo> {
// Cria um BasicMessageChannel com um nome único e codec padrão
static const channel = BasicMessageChannel(
'com.example.myapp/basic_channel', StandardMessageCodec());
String _sentDataStatus = 'Nenhum dado enviado.';
String _receivedFromNative = 'Aguardando mensagem nativa...';
@override
void initState() {
super.initState();
// Define um handler para receber mensagens do nativo
channel.setMessageHandler(_handleIncomingMessage);
}
Future<void> _sendMessageToNative() async {
final Map<String, dynamic> messageData = {
'sender': 'Flutter App',
'payload': 'Mensagem de teste simples',
'timestamp': DateTime.now().toIso8601String(),
};
try {
// Envia a mensagem e aguarda uma resposta (opcional)
final String? reply = await channel.send(messageData) as String?;
setState(() {
_sentDataStatus = 'Mensagem enviada. Resposta nativa: ${reply ?? 'sem resposta'}';
});
} catch (e) {
setState(() {
_sentDataStatus = 'Falha ao enviar mensagem: $e';
});
}
}
// Handler para mensagens recebidas do nativo
Future<String?> _handleIncomingMessage(dynamic message) async {
setState(() {
_receivedFromNative = 'Mensagem Nativa Recebida: $message';
});
// Pode retornar uma resposta para o nativo, se necessário
return 'Mensagem processada no Flutter';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Demonstração BasicMessageChannel'),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_sentDataStatus),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _sendMessageToNative,
child: const Text('Enviar Mensagem para Nativo'),
),
const SizedBox(height: 40),
Text(_receivedFromNative),
],
),
),
);
}
}
3. Uso do EventChannel
O EventChannel é unidirecional, permitindo que o código nativo envie fluxos de eventos para o Flutter (por exemplo, atualizações de localização, status de sensores).
Código Flutter:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class EventChannelDemo extends StatefulWidget {
const EventChannelDemo({Key? key}) : super(key: key);
@override
_EventChannelDemoState createState() => _EventChannelDemoState();
}
class _EventChannelDemoState extends State<EventChannelDemo> {
// Cria um EventChannel com um nome único
static const eventChannel = EventChannel('com.example.myapp/native_events');
String _latestEvent = 'Aguardando eventos nativos...';
Stream<dynamic>? _eventStream;
@override
void initState() {
super.initState();
// Obtém o stream de eventos do canal nativo
_eventStream = eventChannel.receiveBroadcastStream();
// Assina o stream para receber atualizações
_eventStream?.listen(
_onData,
onError: _onError,
onDone: _onDone,
);
}
void _onData(dynamic event) {
setState(() {
_latestEvent = 'Último Evento: $event';
});
}
void _onError(Object error) {
setState(() {
_latestEvent = 'Erro no stream de eventos: $error';
});
}
void _onDone() {
// Opcional: Lógica quando o stream é fechado
print('Stream de eventos nativos fechado.');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Demonstração EventChannel"),
centerTitle: true,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
_latestEvent,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
),
),
);
}
}
Para mais detalhes sobre como adicionar uma tela Flutter a um projeto iOS existente, consulte a documentação oficial: Adicionar Tela Flutter em Projetos iOS.