Call native iOS and Android code from Flutter via platform channels
✓Works with OpenClaudeYou are the #1 Flutter native interop expert from Silicon Valley — the engineer that companies hire when they need to access platform-specific APIs that no Flutter plugin supports. The user wants to call native iOS/Android code from Flutter.
What to check first
- Confirm what native API you need — check pub.dev for existing plugins first
- Decide on channel type: MethodChannel (request/response), EventChannel (streams), BasicMessageChannel
Steps
- Define a unique channel name (e.g., com.yourcompany.app/native)
- On Flutter side: create MethodChannel and call invokeMethod
- On Android (Kotlin): implement MethodChannel.MethodCallHandler
- On iOS (Swift): implement FlutterMethodCallHandler
- Handle errors and unsupported methods explicitly
- Test on both platforms — the API surface is the same but implementations differ
Code
// Flutter (Dart)
import 'package:flutter/services.dart';
class BatteryService {
static const _channel = MethodChannel('com.example.app/battery');
static Future<int> getBatteryLevel() async {
try {
final int level = await _channel.invokeMethod('getBatteryLevel');
return level;
} on PlatformException catch (e) {
print('Failed: ${e.message}');
return -1;
}
}
static Future<void> openSettings() async {
await _channel.invokeMethod('openSettings');
}
}
// Android (Kotlin)
// android/app/src/main/kotlin/com/example/app/MainActivity.kt
package com.example.app
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.content.Context
import android.os.BatteryManager
import android.content.Intent
import android.provider.Settings
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.app/battery"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"getBatteryLevel" -> {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level not available", null)
}
}
"openSettings" -> {
startActivity(Intent(Settings.ACTION_SETTINGS))
result.success(null)
}
else -> result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
}
// iOS (Swift)
// ios/Runner/AppDelegate.swift
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(
name: "com.example.app/battery",
binaryMessenger: controller.binaryMessenger
)
batteryChannel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in
switch call.method {
case "getBatteryLevel":
self?.receiveBatteryLevel(result: result)
case "openSettings":
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
result(nil)
}
default:
result(FlutterMethodNotImplemented)
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func receiveBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
if device.batteryState == UIDevice.BatteryState.unknown {
result(FlutterError(code: "UNAVAILABLE", message: "Battery level unavailable", details: nil))
} else {
result(Int(device.batteryLevel * 100))
}
}
}
// Stream-based: EventChannel example
const _eventChannel = EventChannel('com.example.app/sensor');
Stream<double> getSensorStream() {
return _eventChannel.receiveBroadcastStream().cast<double>();
}
Common Pitfalls
- Forgetting to handle the 'else' case — Flutter side gets MissingPluginException
- Calling platform channels before configureFlutterEngine — channel doesn't exist yet
- Passing complex objects — only basic types are supported (use JSON for complex)
- Not testing on both platforms — they have different APIs and edge cases
When NOT to Use This Skill
- When a pub.dev plugin already exists — use it instead
- For pure Dart logic that doesn't need native APIs
How to Verify It Worked
- Test on real devices, not just simulators
- Test the error case (unsupported method)
- Verify the channel name matches exactly between Flutter and native
Production Considerations
- Wrap platform channel calls in a service class for testability
- Mock platform channels in tests with TestDefaultBinaryMessengerBinding
- Document which platforms each method works on
Related Flutter Skills
Other Claude Code skills in the same category — free to download.
Flutter Widget
Build custom Flutter widgets with state management
Flutter Riverpod
Set up Riverpod for state management in Flutter
Flutter Navigation
Configure GoRouter for declarative navigation
Flutter HTTP
Build HTTP client with Dio and interceptors
Flutter Firebase
Integrate Firebase (Auth, Firestore, Storage) in Flutter
Flutter Testing
Write widget tests and integration tests for Flutter
Flutter State Management with Riverpod
Manage app state in Flutter using Riverpod 2.x for type-safe reactive state
Want a Flutter skill personalized to YOUR project?
This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.