bridge_native

pub package platform

A Flutter plugin that provides type-safe communication between Flutter and native platforms (Android/iOS) using Pigeon. This plugin contains autogenerated Pigeon code and Flutter-side utilities for seamless Flutter-to-native integration.


What is bridge_native?

bridge_native is a foundation plugin that provides:

  • Autogenerated Pigeon code for type-safe communication
  • Request-Response models (Req/Res classes)
  • Flutter utilities (sendToNative() function and Native widget)
  • Platform interfaces that your main plugin implements

This plugin is typically used as a dependency by other plugins that need Flutter-to-native communication.


Features

  • Type-Safe Models: Autogenerated Req and Res classes using Pigeon
  • ReqApi Interface: Type-safe API for native implementations
  • Flutter Utilities:
    • sendToNative(): Send requests from Flutter to native
    • Native widget: Wrapper to receive native events
  • Cross-Platform: Works with both Android (Java) and iOS (Objective-C)
  • Zero Manual Serialization: Pigeon handles all serialization automatically

Installation

Add this to your plugin's pubspec.yaml:

dependencies:
  bridge_native: ^1.0.0

Then run:

flutter pub get

Architecture

┌─────────────────────────────────────┐
│         Your Flutter Plugin         │
│                                     │
│  Uses: sendToNative()               │
│        Native widget                │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│         bridge_native               │
│                                     │
│  Provides:                          │
│  • Req/Res models                   │
│  • ReqApi interface                 │
│  • Pigeon autogenerated code        │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│      Your Native Implementation     │
│                                     │
│  Implements: ReqApi                 │
│  Sets up: EventChannel (optional)   │
└─────────────────────────────────────┘

Usage

1. Flutter Side - Use Provided Utilities

Send Request to Native

import 'package:bridge_native/bridge/native.dart';

// Send request to native and receive response
Future<void> getDeviceInfo() async {
  final response = await sendToNative(
    key: 'getDeviceInfo',
    data: {'includeModel': true},
  );

  print('Response: ${response.data}');
}

Listen to Native Events

import 'package:bridge_native/bridge/native.dart';
import 'package:flutter/material.dart';

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Native(
      child: YourContent(),
      doNativeDataBack: (data) {
        // Handle data from native
        print('Native event: $data');

        if (data is Map) {
          final key = data['key']?.toString() ?? '';
          final payload = data['data'];
          // Process event...
        }
      },
    );
  }
}

2. Native Side - Implement ReqApi

The autogenerated code provides interfaces/protocols that you must implement in your native plugin.

Android (Java)

package com.example.yourplugin;

import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.EventChannel;
import vn.son.bridge.Pigeon;
import java.util.HashMap;
import java.util.Map;

public class YourPlugin implements FlutterPlugin {

    private EventChannel eventChannel;
    private EventChannel.EventSink eventSink;

    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
        // 1. Setup Pigeon API
        Pigeon.ReqApi.setup(binding.getBinaryMessenger(), new AndroidAPI());

        // 2. Setup EventChannel for sending events to Flutter
        eventChannel = new EventChannel(binding.getBinaryMessenger(), "bridgeStream");
        eventChannel.setStreamHandler(new EventChannel.StreamHandler() {
            @Override
            public void onListen(Object arguments, EventChannel.EventSink events) {
                eventSink = events;
            }

            @Override
            public void onCancel(Object arguments) {
                eventSink = null;
            }
        });
    }

    @Override
    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
        eventChannel.setStreamHandler(null);
    }

    // Implement ReqApi interface
    class AndroidAPI implements Pigeon.ReqApi {
        @NonNull
        @Override
        public Pigeon.Res request(@NonNull Pigeon.Req request) {
            // Handle request from Flutter
            String key = request.getKey();
            Map<Object, Object> data = request.getData();

            // Process based on key
            switch (key) {
                case "getDeviceInfo":
                    return handleDeviceInfo(data);
                case "openCamera":
                    return handleCamera(data);
                default:
                    return createResponse("unknown", null);
            }
        }

        private Pigeon.Res handleDeviceInfo(Map<Object, Object> data) {
            Map<String, Object> result = new HashMap<>();
            result.put("model", android.os.Build.MODEL);
            result.put("version", android.os.Build.VERSION.RELEASE);
            return createResponse("deviceInfo", result);
        }

        private Pigeon.Res createResponse(String key, Map<String, Object> data) {
            Pigeon.Res response = new Pigeon.Res();
            response.setKey(key);
            response.setData((Map) data);
            return response;
        }
    }

    // Send event to Flutter
    public void sendEventToFlutter(String key, Map<String, Object> data) {
        if (eventSink != null) {
            Map<String, Object> event = new HashMap<>();
            event.put("key", key);
            event.put("data", data);
            eventSink.success(event);
        }
    }
}

iOS (Swift)

import Flutter
import bridge_native

public class YourPlugin: NSObject, FlutterPlugin, ReqApi {

    private var eventChannel: FlutterEventChannel?
    private var eventSink: FlutterEventSink?

    public static func register(with registrar: FlutterPluginRegistrar) {
        let instance = YourPlugin()

        // 1. Setup EventChannel for sending events to Flutter
        let channel = FlutterEventChannel(
            name: "bridgeStream",
            binaryMessenger: registrar.messenger()
        )
        channel.setStreamHandler(instance)
        instance.eventChannel = channel

        // 2. Setup Pigeon API
        ReqApiSetup(registrar.messenger(), instance)
    }

    // Implement ReqApi protocol
    public func requestRequest(
        _ request: Req,
        error: AutoreleasingUnsafeMutablePointer<FlutterError?>
    ) -> Res? {
        let res = Res()

        switch request.key {
        case "getDeviceInfo":
            res.key = "deviceInfo"
            res.data = [
                "model": UIDevice.current.model,
                "version": UIDevice.current.systemVersion
            ]

        case "openCamera":
            // Handle camera
            res.key = "cameraOpened"
            res.data = ["success": true]

        default:
            res.key = "unknown"
            res.data = nil
        }

        return res
    }

    // Send event to Flutter
    func sendEventToFlutter(key: String, data: [String: Any]) {
        let event: [String: Any] = [
            "key": key,
            "data": data
        ]
        eventSink?(event)
    }
}

// MARK: - FlutterStreamHandler
extension YourPlugin: FlutterStreamHandler {
    public func onListen(
        withArguments arguments: Any?,
        eventSink events: @escaping FlutterEventSink
    ) -> FlutterError? {
        self.eventSink = events
        return nil
    }

    public func onCancel(withArguments arguments: Any?) -> FlutterError? {
        self.eventSink = nil
        return nil
    }
}

API Reference

Flutter API

sendToNative({key, data})

Sends a request to native code and awaits response.

Parameters:

  • key (dynamic): Request identifier
  • data (dynamic): Request payload (typically Map)

Returns:

  • Future<Res>: Response from native

Example:

final res = await sendToNative(
  key: 'action',
  data: {'param': 'value'},
);

Native Widget

A widget wrapper that receives events from native code via EventChannel.

Properties:

  • child (Widget, required): Your widget content
  • doNativeDataBack (Function(dynamic), required): Callback for native events

Example:

Native(
  child: MyContent(),
  doNativeDataBack: (data) {
    // Handle event
  },
)

Native API

Android - Pigeon.ReqApi Interface

public interface ReqApi {
    @NonNull
    Res request(@NonNull Req request);
}

Setup:

Pigeon.ReqApi.setup(binaryMessenger, new YourImplementation());

iOS - ReqApi Protocol

@protocol ReqApi
- (nullable Res *)requestRequest:(Req *)request
                           error:(FlutterError **)error;
@end

Setup:

ReqApiSetup(binaryMessenger, yourImplementation);

Data Models

Req - Request Model

Field Type Description
key String? Request identifier
data Map? Request payload

Res - Response Model

Field Type Description
key String? Response identifier
data Map? Response payload

Complete Example

See the example directory for a complete working example of using bridge_native.

Key implementation points:

  • Flutter: Uses sendToNative() and Native widget
  • Android: Implements Pigeon.ReqApi in EkycPlugin.kt
  • iOS: Implements ReqApi protocol in EkycPlugin.swift
  • Both platforms setup EventChannel named "bridgeStream"

Important Notes

EventChannel Setup

bridge_native does NOT setup EventChannel for you. You must:

  1. Create EventChannel in your plugin's native code
  2. Use the name "bridgeStream" (required by Native widget)
  3. Send events as Map with key and data fields

Pigeon Versions

This plugin uses Pigeon 10.1.4 for code generation. The generated code is compatible with:

  • Android: Java/Kotlin
  • iOS: Objective-C/Swift

Communication Pattern

Request → Response (Synchronous-like):

  • Use sendToNative() from Flutter
  • Native implements ReqApi and returns Res

Event Streaming (Asynchronous):

  • Native sends to EventChannel
  • Flutter listens via Native widget

Troubleshooting

"No implementation found for method request"

Cause: ReqApi not properly setup in native code.

Solution:

  • Android: Call Pigeon.ReqApi.setup(messenger, yourImpl) in onAttachedToEngine
  • iOS: Call ReqApiSetup(messenger, yourImpl) in register(with:)

Events not received in Flutter

Cause: EventChannel not setup or wrong channel name.

Solution:

  • Verify channel name is exactly "bridgeStream"
  • Ensure Native widget is mounted in widget tree
  • Check that native is calling eventSink?.success(data)

Type casting errors

Solution: Always type-check dynamic data:

if (data is Map) {
  final key = data['key']?.toString() ?? '';
  // Safe to use
}

How bridge_native Works

What It Provides

  1. Autogenerated Pigeon Code:

    • Req and Res classes (Android + iOS)
    • ReqApi interface/protocol
    • Codec for serialization
  2. Flutter Utilities:

    • sendToNative() function (wraps Pigeon API)
    • Native widget (wraps EventChannel listener)
    • Convenience functions for common patterns

What You Must Implement

  1. In Your Native Plugin:

    • Implement ReqApi interface/protocol
    • Setup EventChannel if you need native → Flutter events
    • Handle requests in request() method
  2. In Your Flutter Plugin:

    • Use sendToNative() to send requests
    • Use Native widget to receive events
    • Process responses and events

Development

Regenerating Pigeon Code

If you need to modify the API:

  1. Edit pigeons/br_api.dart
  2. Run Pigeon code generation:
    flutter pub run pigeon --input pigeons/br_api.dart
    

Testing

Test your implementation:

test('sendToNative returns response', () async {
  final res = await sendToNative(key: 'test', data: {});
  expect(res, isNotNull);
  expect(res.key, isNotNull);
});

Best Practices

  1. Define Key Constants:

    class BridgeKeys {
      static const getDeviceInfo = 'getDeviceInfo';
      static const openCamera = 'openCamera';
    }
    
  2. Type-Safe Data Handling:

    if (data is Map<String, dynamic>) {
      final value = data['key'] as String?;
    }
    
  3. Error Handling:

    try {
      final res = await sendToNative(key: 'action', data: {});
    } catch (e) {
      print('Bridge error: $e');
    }
    
  4. Document Your Protocol:

    • List all supported keys
    • Define data structures
    • Document response formats

Additional Resources


License

This project is licensed under the MIT License. See the LICENSE file for details.


Contributing

Contributions are welcome! Please submit issues or pull requests.

Repository: https://github.com/sonuos1501/bridge_native Maintainer: SON (Lucas)

Libraries

bridge/native
pigeon