Flutter: Platform Channels e Interop Nativo
Flutter não é uma ilha — Platform Channels são a ponte que o conecta ao continente nativo.
📋 Pré-requisitos: Você já deve estar familiarizado com
- Aula 5: StatefulWidget e Interação
1. O Que Você Vai Aprender
- Três tipos de Platform Channel: MethodChannel, EventChannel, BasicMessageChannel
- Implementação de Handler em Kotlin/Java (Android) e Swift (iOS)
- Geração de código Pigeon: comunicação bidirecional type-safe
- Fluxo de desenvolvimento de Plugin e scaffolding flutter-plugin
- ShopApp: chamando SDKs de pagamento nativos (integração Apple Pay / Google Pay)
2. Uma História Real de Integração de Pagamento
(1) O Problema: Flutter Não Pode Chamar SDKs de Pagamento Nativos Diretamente
O ShopApp do Bob precisa integrar Apple Pay e Google Pay, mas ambos os SDKs só têm APIs nativas (Swift/Kotlin) — Flutter não pode chamá-los diretamente. Sem pagamentos nativos, as taxas de conversão são 40% menores — os usuários não querem digitar números de cartão de crédito manualmente no celular.
(2) A Solução com MethodChannel
MethodChannel é o canal de mensagens Flutter ↔ nativo — o lado Flutter invoca métodos, o lado nativo os trata e retorna resultados.
import 'package:flutter/services.dart';
// Flutter side: invoke native payment
final channel = MethodChannel('com.shopapp/payment');
final result = await channel.invokeMethod<bool>('startPayment', {
'amount': 99.99,
'currency': 'USD',
'merchantId': 'shopapp_inc',
});
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(3) Benefício: Maior Conversão com Pagamento Nativo
Após integrar pagamentos nativos via MethodChannel, a taxa de conversão de pagamento do Bob subiu de 55% para 92% — Apple Pay/Google Pay completa com um toque, sem digitação.
3. Mecanismo de Comunicação do Platform Channel
sequenceDiagram
participant Flutter
participant Channel as MethodChannel
participant Android as Android(Kotlin)
participant iOS as iOS(Swift)
Flutter->>Channel: invokeMethod('pay', amount: 99.99)
Channel->>Android: onMethodCall()
Channel->>iOS: onMethodCall()
Android-->>Channel: PaymentResult
iOS-->>Channel: PaymentResult
Channel-->>Flutter: result
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(1) Comparação de Tipos de Channel
| Tipo | Padrão de Comunicação | Tipos de Dados | Caso de Uso |
|---|---|---|---|
MethodChannel |
Requisição-Resposta | Tipos padrão | Chamadas únicas (pagamento/câmera) |
EventChannel |
Push em streaming | Tipos padrão | Eventos contínuos (sensores/GPS) |
BasicMessageChannel |
Mensagens bidirecionais | Codec personalizado | Comunicação bidirecional de alta frequência |
(2) Mapeamento de Tipos de Dados
| Dart | Android (Kotlin) | iOS (Swift) |
|---|---|---|
bool |
Boolean |
NSNumber(value: Bool) |
int |
Int |
NSNumber(value: Int) |
double |
Double |
NSNumber(value: Double) |
String |
String |
String |
List |
ArrayList |
NSArray |
Map |
HashMap |
NSDictionary |
4. Implementação do MethodChannel
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Lado Flutter do MethodChannel
import 'package:flutter/services.dart';
// Custom exception class definitions (add to your project):
/*
class PaymentCancelledException implements Exception {}
class PaymentFailedException implements Exception {
final String message;
PaymentFailedException(this.message);
}
class PaymentNotSupportedException implements Exception {}
class PaymentException implements Exception {
final String message;
PaymentException(this.message);
}
*/
class NativePaymentService {
static const _channel = MethodChannel('com.shopapp/payment');
static Future<bool> startPayment({
required double amount,
required String currency,
required String merchantId,
}) async {
try {
final result = await _channel.invokeMethod<bool>('startPayment', {
'amount': amount,
'currency': currency,
'merchant_id': merchantId,
});
return result ?? false;
} on PlatformException catch (e) {
switch (e.code) {
case 'PAYMENT_CANCELLED':
throw PaymentCancelledException();
case 'PAYMENT_FAILED':
throw PaymentFailedException(e.message ?? 'Payment failed');
case 'NOT_SUPPORTED':
throw PaymentNotSupportedException();
default:
throw PaymentException(e.message ?? 'Unknown error');
}
}
}
static Future<bool> isPaymentAvailable() async {
return await _channel.invokeMethod<bool>('isPaymentAvailable') ?? false;
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Lado Android Handler Kotlin
// android/app/src/main/kotlin/com/shopapp/MainActivity.kt
class MainActivity : FlutterActivity() {
private lateinit var channel: MethodChannel
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.shopapp/payment")
channel.setMethodCallHandler { call, result ->
when (call.method) {
"startPayment" -> {
val amount = call.argument<Double>("amount") ?: 0.0
val currency = call.argument<String>("currency") ?: "USD"
val merchantId = call.argument<String>("merchant_id") ?: ""
startGooglePay(amount, currency, merchantId, result)
}
"isPaymentAvailable" -> {
val available = isGooglePayAvailable()
result.success(available)
}
else -> result.notImplemented()
}
}
}
private fun startGooglePay(amount: Double, currency: String, merchantId: String, result: MethodChannel.Result) {
// Google Pay API integration
val request = PaymentDataRequest.newBuilder()
.setTransactionInfo(TransactionInfo.newBuilder()
.setTotalPriceStatus(WalletConstants.TOTAL_PRICE_STATUS_FINAL)
.setTotalPrice(amount.toString())
.setCurrencyCode(currency)
.build())
.build()
// Launch payment activity and return result
}
private fun isGooglePayAvailable(): Boolean {
val isReadyToPayRequest = IsReadyToPayRequest.newBuilder().build()
// Check Google Pay availability
return true
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Lado iOS Handler Swift
// ios/Runner/AppDelegate.swift
import Flutter
import PassKit
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: "com.shopapp/payment",
binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { (call, result) in
switch call.method {
case "startPayment":
guard let args = call.arguments as? [String: Any],
let amount = args["amount"] as? Double,
let currency = args["currency"] as? String,
let merchantId = args["merchant_id"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil))
return
}
self.startApplePay(amount: amount, currency: currency,
merchantId: merchantId, result: result)
case "isPaymentAvailable":
result(PKPaymentAuthorizationViewController.canMakePayments())
default:
result(FlutterMethodNotImplemented)
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func startApplePay(amount: Double, currency: String,
merchantId: String, result: @escaping FlutterResult) {
let paymentRequest = PKPaymentRequest()
paymentRequest.merchantIdentifier = merchantId
paymentRequest.supportedNetworks = [.visa, .masterCard, .amex]
paymentRequest.merchantCapabilities = .capability3DS
paymentRequest.countryCode = "US"
paymentRequest.currencyCode = currency
paymentRequest.paymentSummaryItems = [
PKPaymentSummaryItem(label: "ShopApp", amount: NSDecimalNumber(value: amount))
]
guard let vc = PKPaymentAuthorizationViewController(paymentRequest: paymentRequest) else {
result(FlutterError(code: "NOT_SUPPORTED", message: "Apple Pay not available", details: nil))
return
}
vc.delegate = self
// Present payment sheet
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
5. Comunicação em Streaming com EventChannel
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Monitoramento de status da bateria
import 'package:flutter/services.dart';
// Flutter side
class BatteryService {
static const _channel = EventChannel('com.shopapp/battery');
static Stream<double> get batteryLevel {
return _channel.receiveBroadcastStream().map((event) => event as double);
}
}
// Usage
StreamBuilder<double>(
stream: BatteryService.batteryLevel,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Battery: ${snapshot.data!.toStringAsFixed(0)}%');
}
return const CircularProgressIndicator();
},
)
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
6. Comunicação Type-Safe com Pigeon
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Definição e geração com Pigeon
// pigeons/payment_api.dart
import 'package:pigeon/pigeon.dart';
// ⚙️ Dev dependency: flutter pub add --dev pigeon
class PaymentRequest {
double? amount;
String? currency;
String? merchantId;
}
class PaymentResult {
bool? success;
String? transactionId;
String? errorMessage;
}
@HostApi()
abstract class PaymentApi {
@async
PaymentResult startPayment(PaymentRequest request);
bool isPaymentAvailable();
}
// Run: dart run pigeon --input pigeons/payment_api.dart
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
| Recurso do Pigeon | Descrição |
|---|---|
| Segurança de tipos | Verificação de tipos de parâmetros em tempo de compilação |
| Geração dupla-plataforma | Gera código Dart + Kotlin + Swift simultaneamente |
| Null safety | Código gerado suporta null safety |
| Suporte async | Anotação @async trata callbacks assíncronos automaticamente |
7. Desenvolvimento de Plugin
(1) Estrutura do Projeto de Plugin
shopapp_payment/
├── lib/
│ └── shopapp_payment.dart # Dart API
├── android/
│ └── src/main/kotlin/ # Android implementation
├── ios/
│ └── Classes/ # iOS implementation
├── pubspec.yaml
└── example/ # Example app
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Criando um Plugin
# Create plugin project
flutter create --template=plugin --platforms=android,ios shopapp_payment
# Plugin pubspec.yaml
name: shopapp_payment
description: Native payment integration for ShopApp
version: 1.0.0
flutter:
plugin:
platforms:
android:
package: com.shopapp.payment
pluginClass: ShopAppPaymentPlugin
ios:
pluginClass: ShopAppPaymentPlugin
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
8. Exemplo Completo: UI de Integração de Pagamento do ShopApp
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'dart:io';
// ⚙️ Install dependencies: flutter pub add flutter_riverpod go_router
// Custom class definition source:
// - NativePaymentService: see Section 4 Flutter side MethodChannel in this lesson
// - PaymentCancelledException/PaymentFailedException: see Section 4 in this lesson
// - cartProvider: see Lesson 12 CartNotifier
class PaymentPage extends ConsumerWidget {
final double total;
const PaymentPage({super.key, required this.total});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text('Payment')),
body: FutureBuilder<bool>(
future: NativePaymentService.isPaymentAvailable(),
builder: (context, snapshot) {
final nativeAvailable = snapshot.data ?? false;
return ListView(children: [
// Native payment options
if (nativeAvailable) ...[
ListTile(
leading: const Icon(Icons.phone_iphone),
title: Text(Platform.isIOS ? 'Apple Pay' : 'Google Pay'),
subtitle: const Text('Fast & secure'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _payNative(context, ref),
),
const Divider(),
],
// Credit card option
ListTile(
leading: const Icon(Icons.credit_card),
title: const Text('Credit Card'),
subtitle: const Text('Visa, Mastercard, Amex'),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push('/checkout/card'),
),
// PayPal option
ListTile(
leading: const Icon(Icons.account_balance_wallet),
title: const Text('PayPal'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
const SizedBox(height: 32),
Padding(padding: const EdgeInsets.all(16),
child: Text('Total: \$${total.toStringAsFixed(2)}',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold))),
]);
},
),
);
}
Future<void> _payNative(BuildContext context, WidgetRef ref) async {
try {
final success = await NativePaymentService.startPayment(
amount: total, currency: 'USD', merchantId: 'shopapp_inc',
);
if (success && context.mounted) {
ref.read(cartProvider.notifier).clear();
context.go('/order/confirmed');
}
} on PaymentCancelledException {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Payment cancelled')));
} on PaymentFailedException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message)));
}
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
❓ Perguntas Frequentes
P: Existe uma convenção de nomenclatura para nomes de canal do MethodChannel? R: Use notação de domínio reverso:
com.shopapp/payment. O nome do canal deve ser consistente nos lados Flutter e nativo.
P: As chamadas do MethodChannel são assíncronas? R: Sim,
invokeMethodretorna um Future. O processamento no lado nativo roda na thread principal; operações longas devem usar uma thread em segundo plano.
P: Como lidar com PlatformException? R: Use try-catch para capturar PlatformException, distinguindo tipos de erro pelo campo code, e exiba mensagens amigáveis ao usuário.
P: Pigeon ou MethodChannel escrito manualmente? R: Pigeon é recomendado para novos projetos (type-safe, verificação em tempo de compilação); comunicação simples e pontual pode usar MethodChannel diretamente.
P: Qual a diferença entre um Plugin e escrever Platform Channels diretamente? R: Um Plugin é um pacote reutilizável publicável no pub.dev; escrever Platform Channels diretamente é específico do projeto.
P: A plataforma web suporta Platform Channels? R: Não. Na web, use JS interop (dart:js_interop) para chamar APIs do navegador.
📖 Resumo
- MethodChannel usa padrão requisição-resposta, adequado para chamadas únicas (pagamento/câmera)
- EventChannel fornece push em streaming, adequado para eventos contínuos (sensores/localização)
- O lado nativo Kotlin/Swift responde a chamadas Flutter via MethodCallHandler
- Geração de código Pigeon habilita comunicação bidirecional type-safe
- Plugins são empacotados como pacotes reutilizáveis, publicados no pub.dev
📝 Exercícios
- Básico (dificuldade ⭐): Crie um MethodChannel onde o lado Flutter chama código nativo para obter o modelo do dispositivo, e o lado nativo retorna uma String.
- Intermediário (dificuldade ⭐⭐): Implemente um EventChannel para monitorar o status nativo da bateria, exibindo-o em tempo real no lado Flutter com StreamBuilder.
- Desafio (dificuldade ⭐⭐⭐): Use Pigeon para gerar uma API de pagamento type-safe, chame pagamento nativo (simulado) a partir do Flutter, e trate resultados de sucesso/cancelamento/falha.