Integração Flutter em Projetos iOS Nativos com Swift

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 BinaryMessenger responsável pela transmissão e recepção de mensagens.
  • codec: Um MessageCodec ou MethodCodec que 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.

Tags: Flutter ios swift Desenvolvimento Híbrido Platform Channels

Publicado em 9-16 13:15