DEV Community

vmodal_ai
vmodal_ai

Posted on

Flutter Platform Channels: Calling Kotlin from Dart

Flutter Platform Channels: Calling Kotlin from Dart

Flutter gives you a single codebase for Android and iOS, but sometimes you need functionality that is specific to the native platform. This is where Platform Channels become useful.

In this tutorial, we will build a simple Flutter-to-Kotlin bridge using MethodChannel.

What are Platform Channels?

A platform channel allows Dart code to communicate with native Android or iOS code.

Flutter / Dart
      |
      | MethodChannel
      v
Android / Kotlin
      |
      v
Native API
Enter fullscreen mode Exit fullscreen mode

Flutter provides:

  • MethodChannel — request/response calls
  • EventChannel — continuous streams of events
  • BasicMessageChannel — asynchronous messages

For calling a Kotlin function and receiving a result, MethodChannel is usually the best choice.

Step 1: Create the Flutter project

flutter create platform_channel_demo
cd platform_channel_demo
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Dart platform channel

Create lib/native_service.dart:

import 'package:flutter/services.dart';

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

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

    return result ?? 'No response';
  }
}
Enter fullscreen mode Exit fullscreen mode

The channel name must match the channel name used by Kotlin.

Step 3: Call Kotlin from MainActivity

Open:

android/app/src/main/kotlin/.../MainActivity.kt
Enter fullscreen mode Exit fullscreen mode

Use:

package com.example.platform_channel_demo

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.platform/native"

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

        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            CHANNEL
        ).setMethodCallHandler { call, result ->

            when (call.method) {
                "getPlatformMessage" -> {
                    result.success("Hello from Kotlin!")
                }

                else -> {
                    result.notImplemented()
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Use it from Flutter

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

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String message = 'Waiting...';

  Future<void> loadMessage() async {
    try {
      final value = await NativeService.getPlatformMessage();

      if (!mounted) return;

      setState(() {
        message = value;
      });
    } on PlatformException catch (e) {
      if (!mounted) return;

      setState(() {
        message = 'Native error: ${e.message}';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Platform Channel')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(message),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: loadMessage,
              child: const Text('Call Kotlin'),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Passing arguments

Dart:

final result = await _channel.invokeMethod(
  'greet',
  {'name': 'Nauman'},
);
Enter fullscreen mode Exit fullscreen mode

Kotlin:

"greet" -> {
    val name = call.argument<String>("name") ?: "User"
    result.success("Hello, $name!")
}
Enter fullscreen mode Exit fullscreen mode

Returning structured data

Kotlin can return maps:

result.success(
    mapOf(
        "platform" to "Android",
        "version" to android.os.Build.VERSION.RELEASE
    )
)
Enter fullscreen mode Exit fullscreen mode

Dart:

final Map<dynamic, dynamic> data =
    await _channel.invokeMethod('deviceInfo');

print(data['platform']);
print(data['version']);
Enter fullscreen mode Exit fullscreen mode

Error handling

Native code should return meaningful errors:

result.error(
    "DEVICE_ERROR",
    "Unable to access device information",
    null
)
Enter fullscreen mode Exit fullscreen mode

Dart:

try {
  await NativeService.getPlatformMessage();
} on PlatformException catch (e) {
  debugPrint(e.code);
  debugPrint(e.message);
}
Enter fullscreen mode Exit fullscreen mode

Production recommendations

Avoid scattering MethodChannel calls throughout widgets. A cleaner architecture is:

UI
 |
BLoC / ViewModel
 |
Repository
 |
NativeService
 |
MethodChannel
 |
Kotlin
Enter fullscreen mode Exit fullscreen mode

This keeps platform-specific code isolated and testable.

Common mistakes

Channel names do not match

These must be identical:

MethodChannel('com.example.platform/native')
Enter fullscreen mode Exit fullscreen mode

and:

private val CHANNEL = "com.example.platform/native"
Enter fullscreen mode Exit fullscreen mode

Calling native code directly from many widgets

Create a service or repository layer instead.

Blocking the UI thread

Native code should not perform long-running work on the Android main thread.

Conclusion

Platform Channels are one of Flutter's most useful escape hatches when you need native Android functionality. A clean MethodChannel abstraction lets you combine Flutter's UI productivity with Kotlin's native APIs.

Useful Links

SDK Flutter: https://github.com/v-modal/vmodal_sdk_flutter

SDK Android: https://github.com/v-modal/vmodal_sdk_android

Discord: https://discord.gg/K72z28KUx

Top comments (0)