Flutter is a powerful cross-platform framework for building natively compiled applications from a single codebase. However, sometimes you need to integrate Flutter apps with platform-specific (native) code. One common requirement is to receive events and data streams from native code into your Flutter application. This post delves into how to achieve this using MethodChannels and EventChannels in Flutter.
Understanding Native Integration in Flutter
Flutter’s architecture allows developers to write platform-agnostic code, but it also provides mechanisms to interact with the underlying native platforms (Android and iOS) when needed. This is accomplished primarily through:
- Method Channels: Used for one-off communications where Flutter calls native code and receives a single response.
- Event Channels: Used for continuous, asynchronous communication from native code to Flutter. This is ideal for streaming data or receiving event updates.
Why Receive Events and Data Streams?
Several scenarios warrant receiving events and data streams from native code:
- Hardware Sensors: Accessing and processing data from device sensors like GPS, accelerometer, gyroscope, etc.
- Real-Time Data: Receiving real-time data feeds from native APIs (e.g., financial market data, sensor data from external devices).
- Native Libraries: Integrating with native libraries that provide continuous data updates (e.g., camera preview feeds, audio streams).
- Platform Events: Handling platform-specific events that require continuous monitoring and processing (e.g., network status changes, battery level updates).
Setting up the Flutter Project
First, ensure you have a Flutter project set up. If not, create a new one using:
flutter create native_stream_app
cd native_stream_app
Part 1: Method Channels – Establishing Initial Communication
Although our primary goal is event streams, it’s good practice to use Method Channels to initially configure the native components.
1. Defining the Method Channel in Flutter
Open lib/main.dart
and add the following:
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(
title: 'Native Stream App',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
static const platform = MethodChannel('com.example.native_stream_app/method_channel');
String _message = 'Waiting for native message...';
Future _getMessageFromNative() async {
String message;
try {
final String result = await platform.invokeMethod('getMessage');
message = 'Native message: $result';
} on PlatformException catch (e) {
message = "Failed to get message: '${e.message}'.";
}
setState(() {
_message = message;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Native Stream Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_message),
ElevatedButton(
onPressed: _getMessageFromNative,
child: Text('Get Native Message'),
),
],
),
),
);
}
}
Here, we define a MethodChannel
with the name 'com.example.native_stream_app/method_channel'
. The _getMessageFromNative
function calls the native method getMessage
.
2. Implementing the Method Channel on Android (Kotlin)
Open android/app/src/main/kotlin/com/example/native_stream_app/MainActivity.kt
and modify it:
package com.example.native_stream_app
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_stream_app/method_channel"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "getMessage") {
result.success("Hello from Android Native!")
} else {
result.notImplemented()
}
}
}
}
This sets up the method handler for the getMessage
method.
3. Implementing the Method Channel on iOS (Swift)
Open 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 methodChannel = FlutterMethodChannel(name: "com.example.native_stream_app/method_channel",
binaryMessenger: controller.binaryMessenger)
methodChannel.setMethodCallHandler { (call, result) in
if call.method == "getMessage" {
result("Hello from iOS Native!")
} else {
result(FlutterMethodNotImplemented)
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Similar to Android, this provides the implementation for getMessage
in the native iOS environment.
Now, run the app to verify that you can receive a message from the native side when you press the button.
Part 2: Event Channels – Streaming Data from Native Code
Now, let’s set up the event channel for continuous data streaming from native code to Flutter.
1. Defining the Event Channel in Flutter
Modify lib/main.dart
to include the event channel setup and stream handling:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Native Stream App',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
static const methodChannel = MethodChannel('com.example.native_stream_app/method_channel');
static const eventChannel = EventChannel('com.example.native_stream_app/event_channel');
String _message = 'Waiting for native message...';
String _streamMessage = 'Waiting for stream data...';
StreamSubscription? _streamSubscription;
@override
void initState() {
super.initState();
_startStream();
}
Future _getMessageFromNative() async {
String message;
try {
final String result = await methodChannel.invokeMethod('getMessage');
message = 'Native message: $result';
} on PlatformException catch (e) {
message = "Failed to get message: '${e.message}'.";
}
setState(() {
_message = message;
});
}
void _startStream() {
_streamSubscription = eventChannel.receiveBroadcastStream().listen((data) {
setState(() {
_streamMessage = 'Stream data: $data';
});
}, onError: (error) {
setState(() {
_streamMessage = 'Error: ${error.toString()}';
});
}, onDone: () {
setState(() {
_streamMessage = 'Stream closed';
});
});
}
void _stopStream() {
_streamSubscription?.cancel();
setState(() {
_streamMessage = 'Stream stopped';
});
}
@override
void dispose() {
_stopStream();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Native Stream Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_message),
ElevatedButton(
onPressed: _getMessageFromNative,
child: Text('Get Native Message'),
),
SizedBox(height: 20),
Text(_streamMessage),
],
),
),
);
}
}
In this code:
- We define an
EventChannel
named'com.example.native_stream_app/event_channel'
. - The
_startStream
function listens to the stream and updates the UI with the received data. - The
_stopStream
function cancels the stream subscription when the widget is disposed of to avoid memory leaks.
2. Implementing the Event Channel on Android (Kotlin)
Modify android/app/src/main/kotlin/com/example/native_stream_app/MainActivity.kt
to stream data:
package com.example.native_stream_app
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.*
class MainActivity: FlutterActivity() {
private val METHOD_CHANNEL = "com.example.native_stream_app/method_channel"
private val EVENT_CHANNEL = "com.example.native_stream_app/event_channel"
private var eventSink: EventChannel.EventSink? = null
private var job: Job? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "getMessage") {
result.success("Hello from Android Native!")
} else {
result.notImplemented()
}
}
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
eventSink = events
startStreaming()
}
override fun onCancel(arguments: Any?) {
eventSink = null
job?.cancel()
}
}
)
}
private fun startStreaming() {
job = CoroutineScope(Dispatchers.Default).launch {
var counter = 0
while (isActive) {
delay(1000)
val data = "Data from Android: ${counter++}"
eventSink?.success(data)
}
}
}
}
Explanation:
- A new
EventChannel
is set up with the specified name. setStreamHandler
is used to handle the lifecycle of the stream.onListen
starts a coroutine that emits data every second.onCancel
cancels the coroutine and clears theeventSink
.
3. Implementing the Event Channel on iOS (Swift)
Modify ios/Runner/AppDelegate.swift
to stream data from iOS:
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
private var timer: Timer?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let methodChannel = FlutterMethodChannel(name: "com.example.native_stream_app/method_channel",
binaryMessenger: controller.binaryMessenger)
methodChannel.setMethodCallHandler { (call, result) in
if call.method == "getMessage" {
result("Hello from iOS Native!")
} else {
result(FlutterMethodNotImplemented)
}
}
let eventChannel = FlutterEventChannel(name: "com.example.native_stream_app/event_channel",
binaryMessenger: controller.binaryMessenger)
eventChannel.setStreamHandler(self)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
startStreaming()
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
timer?.invalidate()
timer = nil
eventSink = nil
return nil
}
private func startStreaming() {
var counter = 0
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
let data = "Data from iOS: (counter)"
self.eventSink?(data)
counter += 1
}
}
}
Explanation:
- A new
FlutterEventChannel
is set up. - The
AppDelegate
implementsFlutterStreamHandler
to manage the stream lifecycle. onListen
starts a timer that emits data every second.onCancel
stops the timer and clears theeventSink
.
Run your Flutter app, and you should now see the stream data being received and displayed in real-time in your Flutter UI.
Conclusion
Receiving events and data streams from native code is a crucial part of building rich, integrated Flutter applications. By using MethodChannels for initial communication and EventChannels for continuous data streaming, you can seamlessly bridge the gap between Flutter’s cross-platform environment and platform-specific functionalities. Remember to properly manage resources, especially when dealing with streams, to ensure your app is performant and stable.