If you've ever needed to call native Android or iOS code from Flutter, you know the pain. You write Kotlin code, then Swift code, then Dart code, then wire them all together with MethodChannels. It's tedious, error-prone, and frankly, boring.
What if you could just annotate your native methods and call them directly from Dart?
Introducing Flutter Native Bridge — a plugin that eliminates the boilerplate and lets you focus on what matters: your app's functionality.
The Problem
Here's what calling native code traditionally looks like:
Android (Kotlin)
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "my_channel")
.setMethodCallHandler { call, result ->
when (call.method) {
"getDeviceModel" -> result.success(Build.MODEL)
"getBatteryLevel" -> {
// More boilerplate...
}
else -> result.notImplemented()
}
}
}
}
Dart
final channel = MethodChannel('my_channel');
final model = await channel.invokeMethod<String>('getDeviceModel');
Now multiply this by every native feature you need. And don't forget to do it all again for iOS!
The Solution
With Flutter Native Bridge, here's the same functionality:
Android (Kotlin)
@NativeBridge
class DeviceService {
fun getDeviceModel(): String = Build.MODEL
fun getBatteryLevel(): Int = getBattery()
}
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
FlutterNativeBridge.register(this) // One line. That's it.
}
}
iOS (Swift)
class DeviceService: NSObject {
@objc func getDeviceModel() -> String {
return UIDevice.current.model
}
}
// In AppDelegate
FlutterNativeBridge.register(DeviceService())
Dart
// Generated code with full IDE autocomplete!
final model = await DeviceService.getDeviceModel();
final battery = await DeviceService.getBatteryLevel();
No MethodChannel setup. No switch statements. No boilerplate.
Features
- Cross-Platform — Works on both Android and iOS
- Minimal Setup — One annotation, one line of registration
- Auto-Discovery — Android automatically finds annotated classes
- Type-Safe — Generated Dart code with full type support
- IDE Autocomplete — Works seamlessly with your IDE
- Flexible — Use code generation or runtime calls
Installation
Add to your pubspec.yaml:
dependencies:
flutter_native_bridge: ^1.0.0
Quick Start
Step 1: Annotate Your Native Code
Android (Kotlin)
@NativeBridge
class DeviceService {
fun getModel(): String = Build.MODEL
fun getVersion(): Int = Build.VERSION.SDK_INT
@NativeIgnore // Exclude specific methods
fun internalMethod() { }
}
iOS (Swift)
class DeviceService: NSObject {
@objc func getModel() -> String {
return UIDevice.current.model
}
@objc func getVersion() -> String {
return UIDevice.current.systemVersion
}
}
Step 2: Register
Android
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
FlutterNativeBridge.register(this)
}
}
iOS
// In AppDelegate
FlutterNativeBridge.register(DeviceService())
Step 3: Generate Dart Code
dart run flutter_native_bridge:generate
Step 4: Use in Dart
import 'native_bridge.g.dart';
final model = await DeviceService.getModel();
final version = await DeviceService.getVersion();
Runtime API (Without Code Generation)
Don't want to generate code? Use the runtime API:
import 'package:flutter_native_bridge/flutter_native_bridge.dart';
// Direct call
final model = await FlutterNativeBridge.call<String>(
'DeviceService',
'getModel'
);
// Discover all registered classes
final classes = await FlutterNativeBridge.discover();
// {'DeviceService': ['getModel', 'getVersion']}
Android Annotations
| Annotation | Target | Description |
|---|---|---|
@NativeBridge |
Class | Exposes all public methods |
@NativeFunction |
Method | Exposes a single method |
@NativeIgnore |
Method | Excludes a method |
Supported Types
| Kotlin | Swift | Dart |
|---|---|---|
| String | String | String |
| Int | Int | int |
| Double | Double | double |
| Boolean | Bool | bool |
| List<T> | [T] | List<T> |
| Map<K,V> | [K:V] | Map<K,V> |
Real-World Example
Here's a complete example with a battery service:
Android
@NativeBridge
class BatteryService(private val context: Context) {
fun getBatteryLevel(): Int {
val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
fun isCharging(): Boolean {
val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return bm.isCharging
}
}
iOS
class BatteryService: NSObject {
override init() {
super.init()
UIDevice.current.isBatteryMonitoringEnabled = true
}
@objc func getBatteryLevel() -> Int {
return Int(UIDevice.current.batteryLevel * 100)
}
@objc func isCharging() -> Bool {
return UIDevice.current.batteryState == .charging
}
}
Dart
final battery = await BatteryService.getBatteryLevel();
final charging = await BatteryService.isCharging();
print('Battery: $battery% ${charging ? "(Charging)" : ""}');
Why Flutter Native Bridge?
| Traditional Approach | Flutter Native Bridge |
|---|---|
| Manual MethodChannel setup | Auto-configured |
| Switch statements for routing | Direct method calls |
| No type safety | Full type safety |
| Manual iOS + Android sync | Cross-platform generation |
| Repetitive boilerplate | Zero boilerplate |
Get Started
Resources
Conclusion
Flutter Native Bridge removes the friction between Flutter and native code. Whether you're accessing device APIs, integrating SDKs, or building platform-specific features, you can now do it with minimal setup.
Just annotate. Register. Call.
Give it a try and let me know what you think!
If you found this useful, give the package a like on pub.dev and star the repo on GitHub!