Skip to main content

Platform Channels

FLUTTER • NATIVE INTEGRATION

How to Connect Flutter to Native Code Using Platform Channels

A step-by-step tutorial with Dart, Kotlin, and Swift examples, plus error handling and production best practices.

Reading time: 12–15 minutes

Flutter allows developers to build Android and iOS applications from a shared codebase. Sometimes an application needs a platform API or native SDK that is not available through an existing Flutter package. Platform channels provide a way for Dart code to communicate with native Android and iOS code.

What we will build: a Flutter button that calls native code. Android returns a message from Kotlin, while iOS returns a message from Swift.

1. Why Use Platform Channels?

Platform channels are useful when you need to integrate an existing native SDK, access a platform API that lacks a suitable Flutter package, reuse legacy native code, or implement a platform-specific feature.

For example, a payment provider may offer Android and iOS SDKs but no Flutter plugin. A platform channel can connect your Dart code to those native SDKs.

2. How Do Platform Channels Work?

A platform channel is a named communication path between Flutter and the host platform. Dart sends a method name and optional arguments. Native code handles the request and returns a result or an error.

Flutter UI
Dart
↔
MethodChannel
Method and arguments
↔
Native handler
Kotlin or Swift

Figure 1. Request and response flow between Flutter and native code.

The three common channel types are:

  • MethodChannel: request-and-response calls.
  • EventChannel: continuous streams of native events.
  • BasicMessageChannel: message exchange using a selected codec.

This tutorial uses MethodChannel.

3. Step One — Create the Dart Service

Use a dedicated class to keep native communication separate from your widgets. The channel name must match exactly in Dart, Kotlin, and Swift.

File: lib/native_bridge.dart

import 'package:flutter/services.dart';

class NativeBridge {
  static const MethodChannel _channel =
      MethodChannel('com.example.native_bridge/platform');

  static Future<String> getPlatformMessage() async {
    try {
      final String? result =
          await _channel.invokeMethod<String>('getPlatformMessage');

      return result ?? 'Native code returned no message.';
    } on PlatformException catch (e) {
      throw Exception('Native call failed: ${e.message ?? e.code}');
    } on MissingPluginException {
      throw Exception('No native handler is registered.');
    }
  }
}

Create a Flutter Screen

The widget calls the service, shows a progress indicator while waiting, and displays either the result or an error.

File: lib/main.dart

import 'package:flutter/material.dart';
import 'native_bridge.dart';

void main() => runApp(const NativeBridgeApp());

class NativeBridgeApp extends StatelessWidget {
  const NativeBridgeApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Native Bridge Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF5B4BDB),
        ),
        useMaterial3: true,
      ),
      home: const NativeBridgePage(),
    );
  }
}

class NativeBridgePage extends StatefulWidget {
  const NativeBridgePage({super.key});

  @override
  State<NativeBridgePage> createState() => _NativeBridgePageState();
}

class _NativeBridgePageState extends State<NativeBridgePage> {
  String _message = 'Tap the button to call native code.';
  bool _loading = false;

  Future<void> _callNative() async {
    setState(() {
      _loading = true;
      _message = '';
    });

    try {
      final result = await NativeBridge.getPlatformMessage();
      if (!mounted) return;
      setState(() => _message = result);
    } catch (error) {
      if (!mounted) return;
      setState(() => _message = 'Error: $error');
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Native Bridge')),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Card(
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Icon(Icons.swap_horiz, size: 48),
                  const SizedBox(height: 16),
                  Text('Flutter ↔ Native',
                    style: Theme.of(context).textTheme.headlineSmall),
                  const SizedBox(height: 12),
                  const Text(
                    'Call a native method and display its response.',
                    textAlign: TextAlign.center,
                  ),
                  const SizedBox(height: 20),
                  FilledButton(
                    onPressed: _loading ? null : _callNative,
                    child: Text(_loading ? 'Calling...' : 'Call native code'),
                  ),
                  const SizedBox(height: 18),
                  if (_loading)
                    const CircularProgressIndicator()
                  else
                    SelectableText(_message, textAlign: TextAlign.center),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}
Why check mounted? A native call is asynchronous. The user could leave the screen before it finishes. Checking mounted prevents updating a widget that is no longer in the widget tree.

4. Step Two — Implement Android with Kotlin

In a typical Flutter Android project, open the main activity under android/app/src/main/kotlin/your/package/MainActivity.kt. Register the method handler in configureFlutterEngine.

File: MainActivity.kt

package com.example.native_bridge

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private val channelName =
        "com.example.native_bridge/platform"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            channelName
        ).setMethodCallHandler { call, result ->
            when (call.method) {
                "getPlatformMessage" -> {
                    result.success("Hello from Android (Kotlin)!")
                }
                else -> result.notImplemented()
            }
        }
    }
}

Key points:

  • binaryMessenger transports messages between Flutter and Android.
  • setMethodCallHandler registers the native handler.
  • call.method identifies which method Dart requested.
  • result.success(...) returns a value to Flutter.
  • result.notImplemented() reports an unsupported method.

If your project already has a MainActivity, merge the handler into the existing class rather than replacing unrelated setup. Make sure the Kotlin package declaration matches your project.

5. Step Three — Implement iOS with Swift

Open ios/Runner.xcworkspace in Xcode. In the standard Flutter template, you can register the channel in AppDelegate.swift.

File: AppDelegate.swift

import UIKit
import Flutter

@main
@objc class AppDelegate: FlutterAppDelegate {
  private let channelName =
    "com.example.native_bridge/platform"

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions:
      [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    if let controller = window?.rootViewController
        as? FlutterViewController {
      let channel = FlutterMethodChannel(
        name: channelName,
        binaryMessenger: controller.binaryMessenger
      )

      channel.setMethodCallHandler { call, result in
        switch call.method {
        case "getPlatformMessage":
          result("Hello from iOS (Swift)!")
        default:
          result(FlutterMethodNotImplemented)
        }
      }
    }

    return super.application(
      application,
      didFinishLaunchingWithOptions: launchOptions
    )
  }
}

Some Flutter projects use customized engine or scene setup. If the root view controller is not a FlutterViewController, register the channel through the Flutter controller or engine used by your app. Avoid registering duplicate handlers for the same method.

6. Error Handling and Best Practices

For methods that accept arguments, Dart can pass a supported map:

Dart — pass an argument

final result = await _channel.invokeMethod<String>(
  'greet',
  <String, dynamic>{'name': 'Alex'},
);

Kotlin — validate input

"greet" -> {
    val name = call.argument<String>("name")
    if (name.isNullOrBlank()) {
        result.error(
            "INVALID_ARGUMENT",
            "A non-empty name is required.",
            null
        )
    } else {
        result.success("Hello, $name!")
    }
}

Swift — validate input

case "greet":
  guard
    let args = call.arguments as? [String: Any],
    let name = args["name"] as? String,
    !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
  else {
    result(FlutterError(
      code: "INVALID_ARGUMENT",
      message: "A non-empty name is required.",
      details: nil
    ))
    return
  }
  result("Hello, \(name)!")

Understand Native Responses

Native responsePurposeDart result
success(value)Operation succeededFuture returns the value
error(code, message, details)Operation failedThrows PlatformException
notImplemented()Method is unsupportedNo normal result is returned

Production Checklist

  • Use exactly the same channel and method names on every platform.
  • Validate arguments in native code, not only in Dart.
  • Keep platform calls in a service or repository instead of scattering them through widgets.
  • Do not block the main thread with long-running native work.
  • Handle permissions, lifecycle events, and platform-specific failures.
  • Do not log secrets or sensitive user data.
  • After changing Kotlin or Swift, perform a full rebuild if hot reload does not apply the native changes.
  • Consider Pigeon for larger APIs that benefit from generated, typed communication.

7. MethodChannel vs Plugin vs Pigeon

  • MethodChannel: best for a small number of request-and-response calls.
  • Flutter plugin: useful for reusable native integrations shared across multiple apps.
  • Pigeon: useful for larger APIs where generated, type-safe interfaces are helpful.
  • Existing Flutter package: often the simplest choice for common functionality already supported by a reliable package.

Conclusion

Platform channels let Flutter's Dart code communicate with native Android and iOS implementations. Create a channel with a shared name, invoke a method from Dart, register matching handlers in Kotlin and Swift, and return either a value or a structured error.

Final checklist: matching channel names, matching method names, native handlers registered correctly, asynchronous UI states handled, and native inputs validated.

Technical note: Native project structure can vary by Flutter version and app configuration. Adapt the registration code to your app's existing Android and iOS lifecycle setup.

Comments

Popular posts from this blog

Flutter Bloc Pattern 9.1.1 Call API

Flutter Tutorial How to Fetch API Data & Display in ListView using Flutter BLoC Pattern (v9.1.1+) Learn how to decouple your business logic from the UI using the latest flutter_bloc package. We will cover state management, robust error handling, repository pattern, and clean architecture standards. Managing state cleanly is crucial when building production-ready Flutter applications. The BLoC (Business Logic Component) pattern allows us to separate presentation from business logic cleanly. In this detailed tutorial, we will fetch data from a REST API and render it inside an attractive, scrollable ListView complete with error handling and loading indicators. What You Will Learn: Creating Data Models for API JSON responses Building a Repository Layer for network calls Declaring modern Dart 3 Sealed Classes for BLoC States and Events Consuming BLoC using BlocProvider and BlocConsumer ...

Designing and Building Dual-Screen Apps for Concept "iPhone Duo" in Flutter

FLUTTER • ADAPTIVE UI • CONCEPT DESIGN Designing and Building Dual-Screen Apps for the Concept “iPhone Duo” in Flutter Explore adaptive layouts, two-pane navigation, simulated dual-screen experiences, and the engineering decisions behind apps designed for two displays. Reading time: 10–12 minutes Concept disclaimer: “iPhone Duo” is used here as a hypothetical dual-screen device concept, not as a claim that Apple has announced or released such a product. Flutter examples in this article demonstrate adaptive and simulated two-pane layouts that can be tested on existing devices. What if an iPhone could provide two displays that work together? Imagine reading an article on one screen while keeping notes on the other, browsing a product list beside its details, or viewing a video while controlling playback on a separate panel. This kind of experience introduces an interesting app-design challenge: the interface must understand how much space is avail...

Dynamic and responsive ListView in flutter

How to create responsive and dynamic Listview? Step 1: Create Listview builder ListView.builder( physics: ScrollPhysics(parent: AlwaysScrollableScrollPhysics()), itemCount: allBillList[ 0 ].data!.length, scrollDirection: Axis.vertical, itemBuilder: (c, index1) { String data = mainList![index1]; return ListItem(data); }) Step 2: Create separate ListItem.dart file import 'package:flutter/cupertino.dart' ; import 'package:flutter/material.dart' ; class ListItem extends StatefulWidget { ListItem({ this .data}); final String? data; @override _ListItemState createState() => _ListItemState(); } class _ListItemState extends State<ListItem> { @override Widget build(BuildContext context) { // TODO: implement build return Column( children: [ Container(margin: const EdgeInsets.all( 16.0 ), child:Text(data)), BasicWidget.getDivider(), ], ); } } So if you want to delete or add items in main...