How to Integrate Native Code in Flutter: A Step-by-Step Guide

Flutter’s ability to deliver a seamless cross-platform experience is incredible. But what happens when you need to leverage native features that aren’t readily available in Flutter’s library? That’s where platform channels come into play. By bridging the gap between Flutter and native code, you can call platform-specific APIs in Android and iOS.

In this guide, I’ll show you how to integrate native code into your Flutter project using platform channels. Let’s dive in!


What Are Platform Channels?

Platform channels are a mechanism in Flutter that allows communication between the Dart code and native code. This enables you to invoke native APIs and pass data between Dart and the platform layers.

The process involves three main components:

  1. Dart Side: Sending messages using MethodChannel.
  2. Native Side (Android/iOS): Handling messages and responding to Dart.
  3. Channel Name: A string that uniquely identifies the communication channel.

Step 1: Create a New Flutter Project

If you don’t already have a Flutter project, create one:

flutter create native_integration_demo
cd native_integration_demo

Open the project in your favorite IDE (I recommend VS Code).


Step 2: Define the Platform Channel in Dart

In your Dart code, define a MethodChannel and specify a channel name. This channel name will be used to communicate with the native side.

Open lib/main.dart and add the following code:

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: NativeIntegrationDemo(),
    );
  }
}

class NativeIntegrationDemo extends StatefulWidget {
  @override
  _NativeIntegrationDemoState createState() => _NativeIntegrationDemoState();
}

class _NativeIntegrationDemoState extends State<NativeIntegrationDemo> {
  static const platform = MethodChannel('com.example.native_integration_demo/native');

  String _nativeMessage = 'Waiting for native response...';

  Future<void> _getNativeMessage() async {
    String message;
    try {
      message = await platform.invokeMethod('getNativeMessage');
    } on PlatformException catch (e) {
      message = "Failed to get native message: '${e.message}'";
    }

    setState(() {
      _nativeMessage = message;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Native Integration Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_nativeMessage),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _getNativeMessage,
              child: Text('Get Native Message'),
            ),
          ],
        ),
      ),
    );
  }
}

Step 3: Add Native Code for Android

Navigate to android/app/src/main/java/com/example/native_integration_demo/MainActivity.kt Update it to handle the platform channel:

package com.example.native_integration_demo

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

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.example.native_integration_demo/native"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "getNativeMessage") {
                val message = getNativeMessage()
                result.success(message)
            } else {
                result.notImplemented()
            }
        }
    }

    private fun getNativeMessage(): String {
        return "Hello from Android!"
    }
}

Step 4: Add Native Code for iOS

For iOS, navigate to ios/Runner/AppDelegate.swift and modify it:

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
    let nativeChannel = FlutterMethodChannel(name: "com.example.native_integration_demo/native",
                                              binaryMessenger: controller.binaryMessenger)
    nativeChannel.setMethodCallHandler(
      { (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
        if call.method == "getNativeMessage" {
          result(self.getNativeMessage())
        } else {
          result(FlutterMethodNotImplemented)
        }
      }
    )

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

  private func getNativeMessage() -> String {
    return "Hello from iOS!"
  }
}

Step 5: Run the App

  • Ensure you have an Android emulator or iOS simulator running, or connect a physical device.
  • Run the app using: flutter run
  • Press the “Get Native Message” button, and you’ll see the message from the native side displayed on the screen.

Conclusion

By leveraging platform channels, you can unlock the full potential of native APIs in your Flutter apps. This approach allows you to combine Flutter’s flexibility with platform-specific capabilities, giving you the best of both worlds.

Start experimenting with other native features like camera access, file storage, or custom integrations, and let your apps shine! 🚀

Source code