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

1. O Que Você Vai Aprender


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.

DART
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',
});
TEXT
> 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

100%
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
TEXT
> 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

TEXT
> 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

DART
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;
  }
}
TEXT
> 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

TEXT
> 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

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
    }
}
TEXT
> 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

TEXT
> 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

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
    }
}
TEXT
> 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

TEXT
> 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

DART
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();
  },
)
TEXT
> 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

TEXT
> 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

DART
// 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
TEXT
> 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

TEXT
shopapp_payment/
├── lib/
│   └── shopapp_payment.dart    # Dart API
├── android/
│   └── src/main/kotlin/        # Android implementation
├── ios/
│   └── Classes/                # iOS implementation
├── pubspec.yaml
└── example/                    # Example app
TEXT
> 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

TEXT
> 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

BASH
# 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
TEXT
> 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

DART
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)));
    }
  }
}
TEXT
> 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, invokeMethod retorna 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


📝 Exercícios

  1. 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.
  2. Intermediário (dificuldade ⭐⭐): Implemente um EventChannel para monitorar o status nativo da bateria, exibindo-o em tempo real no lado Flutter com StreamBuilder.
  3. 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.

← Aula Anterior | Próxima Aula →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%