Flutter is a popular cross-platform framework that enables developers to build high-quality apps for iOS, Android, and the web from a single codebase. Often, you might need to integrate Flutter with native code (written in languages like Swift, Objective-C for iOS, and Java or Kotlin for Android) to leverage platform-specific features or optimize performance. A crucial part of such integrations is the ability to receive events or data from native code in your Flutter application. This article will guide you through the process of receiving events from native code in Flutter, providing detailed explanations and code examples.
Why Receive Events from Native Code in Flutter?
Receiving events from native code in Flutter can be essential for various reasons:
- Accessing Native APIs: Some platform-specific APIs may not have direct Flutter equivalents, requiring native implementation.
- Performance Optimization: Offloading computationally intensive tasks to native code can improve performance.
- Hardware Integration: Interacting with device-specific hardware features might necessitate native code.
- Existing Native Libraries: Leveraging existing native libraries without rewriting them in Dart.
Overview of Method Channels
Flutter communicates with native code using Method Channels. A method channel provides a mechanism to invoke methods on the native side from Flutter and vice versa. For receiving events, Flutter utilizes Event Channels, which are specifically designed for continuous data streams from native code to Flutter.
Steps to Receive Events from Native Code in Flutter
To receive events from native code, you need to follow these steps:
- Define an Event Channel in Flutter.
- Implement the Native Code for Sending Events.
- Listen to the Event Channel in Flutter.
- Handle the Received Events in Flutter.
1. Define an Event Channel in Flutter
First, you need to define an event channel in your Flutter code. This channel will be the gateway for receiving events from the native side. The event channel requires a unique name (string identifier) for proper identification.
import 'dart:async';
import 'package:flutter/services.dart';
class NativeEventManager {
static const EventChannel _eventChannel =
EventChannel('com.example.my_app/native_events');
Stream<dynamic> get nativeEventStream {
return _eventChannel.receiveBroadcastStream();
}
}
Explanation:
- Import necessary libraries:
dart:asyncandpackage:flutter/services.dart. - Create an
EventChannelwith a unique name:'com.example.my_app/native_events'. This name should match the one used in your native code. - Define a getter
nativeEventStreamthat returns aStreamof dynamic events. This stream is created by callingreceiveBroadcastStream()on the event channel.
2. Implement the Native Code for Sending Events
Next, implement the native code to send events through the defined event channel. This implementation differs slightly between iOS (Swift or Objective-C) and Android (Java or Kotlin).
iOS (Swift)
Implement the FlutterStreamHandler protocol in Swift to handle the event stream:
import Flutter
@objc class NativeEventHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
public func onListen(
withArguments arguments: Any?,
eventSink events: @escaping FlutterEventSink
) -> FlutterError? {
self.eventSink = events
startSendingEvents() // Custom method to start sending events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
stopSendingEvents() // Custom method to stop sending events
return nil
}
private func startSendingEvents() {
// Example: Simulate sending events every 2 seconds
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { timer in
let eventData = ["message": "Event from iOS: (Date())"]
self.eventSink?(eventData)
}
}
private func stopSendingEvents() {
// Logic to stop sending events, if needed
}
}
@objc class SwiftNativeCodePlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftNativeCodePlugin()
let eventChannelName = "com.example.my_app/native_events"
let eventChannel = FlutterEventChannel(name: eventChannelName,
binaryMessenger: registrar.messenger())
let eventHandler = NativeEventHandler()
eventChannel.setStreamHandler(eventHandler)
registrar.register(instance, forKey: "SwiftNativeCodePlugin")
}
}
Explanation:
- Create a class
NativeEventHandlerthat conforms toFlutterStreamHandler. - Implement
onListento start sending events. It receives aFlutterEventSinkwhich is used to send data to Flutter. - Implement
onCancelto stop sending events and clean up resources. - Register the event channel in the
registermethod.
Android (Kotlin)
Implement the EventChannel.StreamHandler interface in Kotlin:
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.PluginRegistry
class NativeEventHandler : EventChannel.StreamHandler {
private var eventSink: EventChannel.EventSink? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
startSendingEvents() // Custom method to start sending events
}
override fun onCancel(arguments: Any?) {
eventSink = null
stopSendingEvents() // Custom method to stop sending events
}
private fun startSendingEvents() {
// Example: Simulate sending events every 2 seconds
Thread {
while (eventSink != null) {
try {
Thread.sleep(2000) // 2 seconds
val eventData = mapOf("message" to "Event from Android: ${System.currentTimeMillis()}")
eventSink?.success(eventData)
} catch (e: InterruptedException) {
println("Event sending interrupted: ${e.message}")
eventSink?.error("INTERRUPTED", e.message, null)
eventSink = null
}
}
}.start()
}
private fun stopSendingEvents() {
// Logic to stop sending events, if needed
}
}
class NativeCodePlugin(private val registry: PluginRegistry.Registrar) {
fun register() {
val eventChannelName = "com.example.my_app/native_events"
val eventChannel = EventChannel(registry.view(), eventChannelName)
val eventHandler = NativeEventHandler()
eventChannel.setStreamHandler(eventHandler)
}
}
Explanation:
- Create a class
NativeEventHandlerthat implementsEventChannel.StreamHandler. - Implement
onListento start sending events. It receives anEventChannel.EventSinkwhich is used to send data to Flutter. - Implement
onCancelto stop sending events and clean up resources. - Register the event channel in the
registermethod.
Register the Plugin in MainActivity (Kotlin)
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import androidx.annotation.NonNull
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
NativeCodePlugin(this.registrarFor("com.example.my_app/native_events")!!).register()
}
}
3. Listen to the Event Channel in Flutter
In your Flutter code, listen to the event stream and update your UI or application state accordingly.
import 'package:flutter/material.dart';
class EventListeningWidget extends StatefulWidget {
@override
_EventListeningWidgetState createState() => _EventListeningWidgetState();
}
class _EventListeningWidgetState extends State<EventListeningWidget> {
final NativeEventManager _nativeEventManager = NativeEventManager();
String _eventMessage = 'No event yet';
@override
void initState() {
super.initState();
_nativeEventManager.nativeEventStream.listen((event) {
setState(() {
_eventMessage = event['message'] ?? 'Unknown event';
});
}, onError: (error) {
setState(() {
_eventMessage = 'Error: ${error.message}';
});
}, onDone: () {
setState(() {
_eventMessage = 'Stream closed';
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Native Events in Flutter'),
),
body: Center(
child: Text(_eventMessage),
),
);
}
}
Explanation:
- Create a
StatefulWidgetto display the received event messages. - In
initState, start listening to thenativeEventStream. - Update the
_eventMessagestate variable with the received data. - Handle potential errors and stream completion events.
4. Handle the Received Events in Flutter
In the example above, the Flutter code receives event data as a Map and extracts the message from it. You can adapt the handling logic based on the structure of the data sent from the native side.
Best practices for handling events:
- Error Handling: Implement robust error handling to gracefully manage unexpected errors from the native side.
- Data Transformation: Transform native data into Dart-compatible formats (e.g., convert native objects to Dart Maps or Lists).
- Resource Management: Ensure proper cleanup of resources when the event stream is closed or no longer needed to prevent memory leaks.
Complete Example
Flutter (Dart) Code:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class NativeEventManager {
static const EventChannel _eventChannel =
EventChannel('com.example.my_app/native_events');
Stream<dynamic> get nativeEventStream {
return _eventChannel.receiveBroadcastStream();
}
}
class EventListeningWidget extends StatefulWidget {
@override
_EventListeningWidgetState createState() => _EventListeningWidgetState();
}
class _EventListeningWidgetState extends State<EventListeningWidget> {
final NativeEventManager _nativeEventManager = NativeEventManager();
String _eventMessage = 'No event yet';
@override
void initState() {
super.initState();
_nativeEventManager.nativeEventStream.listen((event) {
setState(() {
_eventMessage = event['message'] ?? 'Unknown event';
});
}, onError: (error) {
setState(() {
_eventMessage = 'Error: ${error.message}';
});
}, onDone: () {
setState(() {
_eventMessage = 'Stream closed';
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Native Events in Flutter'),
),
body: Center(
child: Text(_eventMessage),
),
);
}
}
void main() {
runApp(MaterialApp(
home: EventListeningWidget(),
));
}
iOS (Swift) Code:
import Flutter
@objc class NativeEventHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
public func onListen(
withArguments arguments: Any?,
eventSink events: @escaping FlutterEventSink
) -> FlutterError? {
self.eventSink = events
startSendingEvents() // Custom method to start sending events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
stopSendingEvents() // Custom method to stop sending events
return nil
}
private func startSendingEvents() {
// Example: Simulate sending events every 2 seconds
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { timer in
let eventData = ["message": "Event from iOS: (Date())"]
self.eventSink?(eventData)
}
}
private func stopSendingEvents() {
// Logic to stop sending events, if needed
}
}
@objc class SwiftNativeCodePlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftNativeCodePlugin()
let eventChannelName = "com.example.my_app/native_events"
let eventChannel = FlutterEventChannel(name: eventChannelName,
binaryMessenger: registrar.messenger())
let eventHandler = NativeEventHandler()
eventChannel.setStreamHandler(eventHandler)
registrar.register(instance, forKey: "SwiftNativeCodePlugin")
}
}
Android (Kotlin) Code:
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import androidx.annotation.NonNull
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.PluginRegistry
class NativeEventHandler : EventChannel.StreamHandler {
private var eventSink: EventChannel.EventSink? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
startSendingEvents() // Custom method to start sending events
}
override fun onCancel(arguments: Any?) {
eventSink = null
stopSendingEvents() // Custom method to stop sending events
}
private fun startSendingEvents() {
// Example: Simulate sending events every 2 seconds
Thread {
while (eventSink != null) {
try {
Thread.sleep(2000) // 2 seconds
val eventData = mapOf("message" to "Event from Android: ${System.currentTimeMillis()}")
eventSink?.success(eventData)
} catch (e: InterruptedException) {
println("Event sending interrupted: ${e.message}")
eventSink?.error("INTERRUPTED", e.message, null)
eventSink = null
}
}
}.start()
}
private fun stopSendingEvents() {
// Logic to stop sending events, if needed
}
}
class NativeCodePlugin(private val registry: PluginRegistry.Registrar) {
fun register() {
val eventChannelName = "com.example.my_app/native_events"
val eventChannel = EventChannel(registry.view(), eventChannelName)
val eventHandler = NativeEventHandler()
eventChannel.setStreamHandler(eventHandler)
}
}
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
NativeCodePlugin(this.registrarFor("com.example.my_app/native_events")!!).register()
}
}
Troubleshooting Common Issues
- Event Channel Name Mismatch: Ensure the event channel name in Flutter matches exactly with the name in the native code.
- Null EventSink: Check if the
eventSinkis null before sending events in the native code. - Error Handling: Always handle potential errors on both the Flutter and native sides.
- Platform Threading: Ensure that the native code for sending events is running on the appropriate platform thread to avoid UI blocking.
Conclusion
Receiving events from native code in Flutter is a powerful technique for integrating platform-specific functionalities and optimizing performance. By defining event channels, implementing native code to send events, and listening to these events in Flutter, developers can create robust cross-platform applications. Understanding the underlying principles and following best practices ensures seamless integration and efficient event handling between Flutter and native components.