DEV Community

kanta13jp1
kanta13jp1

Posted on

Flutter Plugin Development — Calling Native Features from Dart

Flutter Plugin Development — Calling Native Features from Dart

Sometimes Flutter's rich package ecosystem doesn't cover the native feature you need. This guide covers the basics of Flutter plugin development using Platform Channels.

When You Need a Plugin

  • No existing pub.dev package covers the feature
  • Existing packages have insufficient performance
  • Integration with internal libraries or proprietary SDKs
  • Deep OS-specific features (background processing, hardware access, etc.)

Plugin Structure

my_plugin/
  lib/
    my_plugin.dart          # Dart API
    my_plugin_platform_interface.dart
    my_plugin_method_channel.dart
  android/
    src/main/kotlin/
      com/example/my_plugin/
        MyPlugin.kt         # Android implementation
  ios/
    Classes/
      MyPlugin.swift        # iOS implementation
  example/
    lib/main.dart           # Test app
  pubspec.yaml
Enter fullscreen mode Exit fullscreen mode

Generate the Plugin Scaffold

flutter create --template=plugin --platforms=android,ios my_plugin
cd my_plugin
Enter fullscreen mode Exit fullscreen mode

Dart Implementation

// lib/my_plugin.dart
import 'my_plugin_platform_interface.dart';

class MyPlugin {
  static Future<int?> getBatteryLevel() {
    return MyPluginPlatform.instance.getBatteryLevel();
  }
}
Enter fullscreen mode Exit fullscreen mode
// lib/my_plugin_method_channel.dart
import 'package:flutter/services.dart';
import 'my_plugin_platform_interface.dart';

class MethodChannelMyPlugin extends MyPluginPlatform {
  static const _channel = MethodChannel('my_plugin');

  @override
  Future<int?> getBatteryLevel() async {
    try {
      return await _channel.invokeMethod<int>('getBatteryLevel');
    } on PlatformException catch (e) {
      throw Exception('Failed to get battery level: ${e.message}');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Android Implementation (Kotlin)

package com.example.my_plugin

import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

class MyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
    private lateinit var channel: MethodChannel
    private lateinit var context: Context

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(binding.binaryMessenger, "my_plugin")
        channel.setMethodCallHandler(this)
        context = binding.applicationContext
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "getBatteryLevel" -> {
                val batteryLevel = getBatteryLevel()
                if (batteryLevel != -1) result.success(batteryLevel)
                else result.error("UNAVAILABLE", "Battery level not available.", null)
            }
            else -> result.notImplemented()
        }
    }

    private fun getBatteryLevel(): Int {
        val intent = context.registerReceiver(
            null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)
        )
        val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
        val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
        return if (level != -1 && scale != -1) (level * 100 / scale) else -1
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
    }
}
Enter fullscreen mode Exit fullscreen mode

iOS Implementation (Swift)

import Flutter
import UIKit

public class MyPlugin: NSObject, FlutterPlugin {
    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(
            name: "my_plugin",
            binaryMessenger: registrar.messenger()
        )
        registrar.addMethodCallDelegate(MyPlugin(), channel: channel)
    }

    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "getBatteryLevel":
            UIDevice.current.isBatteryMonitoringEnabled = true
            let level = Int(UIDevice.current.batteryLevel * 100)
            if level >= 0 { result(level) }
            else { result(FlutterError(code: "UNAVAILABLE", message: "Not available", details: nil)) }
        default:
            result(FlutterMethodNotImplemented)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  final platform = MethodChannelMyPlugin();

  test('getBatteryLevel', () async {
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(
      const MethodChannel('my_plugin'),
      (call) async => call.method == 'getBatteryLevel' ? 42 : null,
    );

    expect(await platform.getBatteryLevel(), 42);
  });
}
Enter fullscreen mode Exit fullscreen mode

Publishing to pub.dev

# Pre-publish check
flutter pub publish --dry-run

# Publish
flutter pub publish
Enter fullscreen mode Exit fullscreen mode

Summary

Platform Channels give Flutter access to any native feature. By structuring your plugin across Dart / Kotlin / Swift layers, you get type-safe, maintainable plugins ready to share with the community.


Building an AI Life Management app with Flutter × Supabase at 自分株式会社. Sharing indie dev insights every week.

Top comments (0)