Engineering Core
ISB Vietnam's skilled software engineers deliver high-quality applications, leveraging their extensive experience in developing financial tools, business management systems, medical technology, and mobile/web platforms.

Introduction

Flutter excels at rendering a declarative UI at 60fps, but it remains a guest on the host operating system. When an application requires access to platform-specific APIs—such as low-energy Bluetooth, obscure biometric sensors, or background process management—we must bridge the gap between the Dart runtime and the native host.

While the MethodChannel API is the foundational transport layer, building a scalable, maintainable plugin requires more than just passing strings back and forth. This post details the architecture and engineering standards for building production-grade Flutter plugins, focusing on the Federated Architecture, Type Safety, and Concurrency.

How to implement

The Federated Plugin Architecture

For production systems, monolithic plugins (where Android, iOS, and Dart code reside in one package) are discouraged. The industry standard is the Federated Plugin Architecture. This pattern decouples the API definition from the platform implementations, enabling independent scalability and testing.

The Structure

A federated plugin is split into multiple packages, typically organized in a monorepo:

  • plugin_name (App-Facing): The entry point for consumers. It forwards calls to the default platform instance.
  • plugin_name_platform_interface: Contains abstract base classes and data models. This ensures all platform implementations adhere to the same contract.
  • plugin_name_android / plugin_name_ios: The concrete implementations for specific platforms.

Benefits:

  • Isolating Dependencies: Android-specific Gradle dependencies do not leak into the Web or iOS implementations.
  • Testability: The platform_interface allows you to inject mock implementations during Dart unit tests without needing a simulator.

Enforcing Type Safety with Pigeon

The raw MethodChannel relies on Map<String, dynamic> and untyped standard message codecs. This is brittle; a typo in a map key or a mismatched data type causes runtime crashes (ClassCastException) rather than compile-time errors.

The Solution: Pigeon.

Pigeon is a code generation tool that creates type-safe bridges between Dart and Native code. It generates the serialization logic, ensuring that data contracts are respected across boundaries.

Step A: Define the Schema (Dart)

Create a standalone Dart file (e.g., pigeons/messages.dart) to define the API.

Dart:

import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(PigeonOptions(
     dartOut: 'lib/src/messages.g.dart',
     kotlinOut: 'android/src/main/kotlin/com/example/plugin/Messages.g.kt',
     swiftOut: 'ios/Classes/Messages.g.swift',
     kotlinOptions: KotlinOptions(package: 'com.example.plugin'),
))

class CompressionConfig {
     int? quality;
     String? format; // 'jpeg' or 'png'
}

class CompressionResult {
     Uint8List? data;
     String? error;
}

@HostApi()
abstract class ImageCompressorApi {
     @async
     CompressionResult compress(Uint8List rawData, CompressionConfig config);
}

Step B: Generate the Protocol

Running the Pigeon generator produces:

  • Dart: An abstract class used by your plugin logic.
  • Kotlin: An interface (ImageCompressorApi) to implement.
  • Swift: A protocol (ImageCompressorApi) to conform to.

Android Implementation (Kotlin)

Modern Android plugins should be written in Kotlin and must handle lifecycle awareness and threading correctly.

The generated Pigeon interface simplifies the setup. Note the use of Coroutines to move work off the main thread.

Kotlin:

import io.flutter.embedding.engine.plugins.FlutterPlugin
import kotlinx.coroutines.*

class ImageCompressorPlugin : FlutterPlugin, ImageCompressorApi {
     private val scope = CoroutineScope(Dispatchers.Main)

     override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
          // Wire up the generated Pigeon API
          ImageCompressorApi.setUp(binding.binaryMessenger, this)
     }

     override fun compress(
          rawData: ByteArray,
          config: CompressionConfig,
          result: Result<CompressionResult>
     ) {
          // MOVE TO BACKGROUND THREAD
          scope.launch(Dispatchers.Default) {
               try {
                    val compressedData = NativeCompressor.process(rawData, config.quality)
                    val output = CompressionResult(data = compressedData)

                    // Return to Main Thread to send result back to Flutter
                   withContext(Dispatchers.Main) {
                        result.success(output)
                   }

               } catch (e: Exception) {
                    withContext(Dispatchers.Main) {
                    result.error(e)
              }}
          }

     }

     override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
          ImageCompressorApi.setUp(binding.binaryMessenger, null)
          scope.cancel() // Prevent memory leaks
     }
}

Key Engineering Consideration: If your plugin requires Activity references (e.g., for startActivityForResult or Permissions), your plugin class must implement ActivityAware. Do not rely on the deprecated Registrar.

iOS Implementation (Swift)

iOS implementation follows a similar pattern using Swift protocols and Grand Central Dispatch (GCD).

Swift:

import Flutter
import UIKit

public class SwiftCompressorPlugin: NSObject, FlutterPlugin, ImageCompressorApi {

     public static func register(with registrar: FlutterPluginRegistrar) {
          let messenger = registrar.messenger()
          let api = SwiftCompressorPlugin()
           // Wire up the generated Pigeon API
          ImageCompressorApiSetup.setUp(binaryMessenger: messenger, api: api)
     }

     func compress(
          rawData: FlutterStandardTypedData,
          config: CompressionConfig,
          completion: @escaping (Result<CompressionResult, Error>) -> Void
     ) {
          // MOVE TO BACKGROUND QUEUE
          DispatchQueue.global(qos: .userInitiated).async {
          do {
                let data = try NativeCompressor.process(rawData.data, quality: config.quality)
                let result = CompressionResult(data: data, error: nil)

                // Callback is thread-safe in Pigeon generated code,
               // but explicit main queue dispatch is good practice for UI work
               DispatchQueue.main.async {
                      completion(.success(result))
                }
               } catch {
                     completion(.failure(error))
               }
          }
     }
}

Performance and Concurrency

A common bottleneck in plugin development is blocking the Platform Thread.

  • The Issue: Flutter's Platform Channels invoke native methods on the host's Main Thread (UI Thread).
  • The Consequence: If you perform JSON parsing, Bitmap decoding, or File I/O directly in the handler, the entire device UI (not just the Flutter app) will freeze (Jank).
  • The Fix: Always offload operations exceeding 16ms to a background thread (using Dispatchers.IO in Kotlin or DispatchQueue.global in Swift) immediately upon receiving the call.

Testing Strategy

Robust plugins require a layered testing approach.

Unit Tests (Dart)

Mock the platform interface. Because we decoupled the logic, we can test the Dart transformation layers without an emulator.

Dart:

class MockApi implements ImageCompressorApi {
     @override
     Future<CompressionResult> compress(Uint8List rawData, CompressionConfig config) async {
          return CompressionResult(data: rawData); // Echo back for testing
     }
    }

     void main() {
           test('Controller transforms data correctly', () async {
          final api = MockApi();
          // Inject API into controller and assert logic
      });
}

Integration Tests (On-Device)

Use the integration_test package to verify the full round-trip. This ensures the native compilation and linking are correct.

Summary

Building a plugin is not just about making it work; it is about making it safe and scalable.

  • Federate: Split your logic from your platform implementations.
  • Strict Typing: Use Pigeon to eliminate runtime serialization errors.
  • Thread Management: Never block the Main Thread; offload heavy lifting immediately.
  • Lifecycle: Manage Activity attachment and detachment cleanly to avoid leaks.

Ready to get started?

Contact IVC for a free consultation and discover how we can help your business grow online.

Contact IVC for a Free Consultation
Written by
Author Avatar
Engineering Core
ISB Vietnam's skilled software engineers deliver high-quality applications, leveraging their extensive experience in developing financial tools, business management systems, medical technology, and mobile/web platforms.

COMPANY PROFILE

Please check out our Company Profile.

Download

COMPANY PORTFOLIO

Explore my work!

Download

ASK ISB Vietnam ABOUT DEVELOPMENT

Let's talk about your project!

Contact US