Receiving Events from Native Code in Flutter

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:

  1. Define an Event Channel in Flutter.
  2. Implement the Native Code for Sending Events.
  3. Listen to the Event Channel in Flutter.
  4. 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:async and package:flutter/services.dart.
  • Create an EventChannel with a unique name: 'com.example.my_app/native_events'. This name should match the one used in your native code.
  • Define a getter nativeEventStream that returns a Stream of dynamic events. This stream is created by calling receiveBroadcastStream() 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 NativeEventHandler that conforms to FlutterStreamHandler.
  • Implement onListen to start sending events. It receives a FlutterEventSink which is used to send data to Flutter.
  • Implement onCancel to stop sending events and clean up resources.
  • Register the event channel in the register method.
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 NativeEventHandler that implements EventChannel.StreamHandler.
  • Implement onListen to start sending events. It receives an EventChannel.EventSink which is used to send data to Flutter.
  • Implement onCancel to stop sending events and clean up resources.
  • Register the event channel in the register method.
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 StatefulWidget to display the received event messages.
  • In initState, start listening to the nativeEventStream.
  • Update the _eventMessage state 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 eventSink is 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.