Using EventChannel for Streaming Data from Native Code in Flutter

In Flutter, you might encounter scenarios where you need to communicate with native platform code to leverage platform-specific functionalities or libraries. One powerful mechanism for this communication is the EventChannel. Unlike MethodChannels that provide a request-response style of communication, EventChannels are designed for streaming data from the native side to Flutter. This blog post delves into how to use EventChannel for streaming data from native code in Flutter.

What is an EventChannel?

An EventChannel is a communication channel in Flutter used to establish a continuous stream of data from the native platform (Android or iOS) to the Flutter app. It is particularly useful when you need real-time or continuous updates from the native side, such as sensor data, location updates, or real-time data streams.

Why Use EventChannel?

  • Real-Time Data: Facilitates streaming real-time data from native to Flutter.
  • Asynchronous Updates: Enables asynchronous updates without the need for constant polling.
  • Efficient Communication: Provides an efficient way to handle continuous data flow compared to request-response mechanisms.

How to Implement EventChannel in Flutter

To implement EventChannel, you need to set up communication channels on both the Flutter side and the native side (Android or iOS).

Step 1: Flutter Side Implementation

First, define the EventChannel in your Flutter code and set up a stream to receive data.


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

class NativeStreamScreen extends StatefulWidget {
  @override
  _NativeStreamScreenState createState() => _NativeStreamScreenState();
}

class _NativeStreamScreenState extends State<NativeStreamScreen> {
  static const eventChannel = EventChannel('example.streaming.data');
  StreamSubscription? _streamSubscription;
  List<String> _dataStream = [];

  @override
  void initState() {
    super.initState();
    _startStreaming();
  }

  @override
  void dispose() {
    _stopStreaming();
    super.dispose();
  }

  void _startStreaming() {
    _streamSubscription = eventChannel.receiveBroadcastStream().listen(
      (data) {
        setState(() {
          _dataStream.add(data.toString());
        });
      },
      onError: (error) {
        print('Error receiving data: $error');
      },
      onDone: () {
        print('Stream closed.');
      },
    );
  }

  void _stopStreaming() {
    _streamSubscription?.cancel();
    _streamSubscription = null;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Native Data Stream'),
      ),
      body: ListView.builder(
        itemCount: _dataStream.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(_dataStream[index]),
          );
        },
      ),
    );
  }
}

In this Flutter code:

  • We define an EventChannel with a unique name (example.streaming.data).
  • We use receiveBroadcastStream() to create a stream from the EventChannel.
  • We listen to the stream and update the UI with the received data.
  • Error and done handlers are included to manage stream events.

Step 2: Android Native Code Implementation

Next, implement the native side (Android) to stream data to the Flutter app.


import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import android.os.Handler
import android.os.Looper

class MainActivity: FlutterActivity() {
    private val eventChannelName = "example.streaming.data"
    private var eventChannel: EventChannel? = null
    private val handler = Handler(Looper.getMainLooper())

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

        eventChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, eventChannelName)
        eventChannel?.setStreamHandler(
            object : EventChannel.StreamHandler {
                private var eventSink: EventChannel.EventSink? = null
                private var counter = 0

                override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
                    eventSink = events
                    startStreaming()
                }

                override fun onCancel(arguments: Any?) {
                    eventSink = null
                    stopStreaming()
                }

                private fun startStreaming() {
                    handler.post(object : Runnable {
                        override fun run() {
                            if (eventSink != null) {
                                eventSink?.success("Data from Native: ${counter++}")
                                handler.postDelayed(this, 1000) // Send data every 1 second
                            }
                        }
                    })
                }

                private fun stopStreaming() {
                    handler.removeCallbacksAndMessages(null)
                }
            }
        )
    }

    override fun onDestroy() {
        super.onDestroy()
        eventChannel?.setStreamHandler(null)
    }
}

In this Android code:

  • We initialize the EventChannel with the same name used in Flutter (example.streaming.data).
  • We implement StreamHandler to manage the stream lifecycle.
  • The onListen method starts streaming data when the Flutter side starts listening.
  • The onCancel method stops streaming data when the Flutter side stops listening.
  • We use a Handler to send data every 1 second (you can adjust the interval as needed).

Step 3: iOS Native Code Implementation

Here’s the native implementation for iOS using Swift:


import Flutter
import UIKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  private var eventChannel: FlutterEventChannel?
  private var eventSink: FlutterEventSink?
  private var timer: Timer?
  private var counter = 0

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let binaryMessenger = controller.binaryMessenger

    eventChannel = FlutterEventChannel(name: "example.streaming.data", binaryMessenger: binaryMessenger)
    eventChannel?.setStreamHandler(self)

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

extension AppDelegate: FlutterStreamHandler {
  func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
    eventSink = events
    startStreaming()
    return nil
  }

  func onCancel(withArguments arguments: Any?) -> FlutterError? {
    eventSink = nil
    stopStreaming()
    return nil
  }

  private func startStreaming() {
    timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
      if let sink = self.eventSink {
        sink("Data from Native: (self.counter)")
        self.counter += 1
      }
    }
  }

  private func stopStreaming() {
    timer?.invalidate()
    timer = nil
  }
}

In this iOS code:

  • We initialize the FlutterEventChannel with the same name used in Flutter (example.streaming.data).
  • We implement FlutterStreamHandler to manage the stream lifecycle.
  • The onListen method starts streaming data when the Flutter side starts listening.
  • The onCancel method stops streaming data when the Flutter side stops listening.
  • We use a Timer to send data every 1 second.

Conclusion

Using EventChannel in Flutter allows you to efficiently stream data from native code to your Flutter app, making it ideal for real-time updates and continuous data streams. By implementing the necessary setup on both the Flutter and native sides, you can leverage platform-specific functionalities while keeping your UI reactive and up-to-date.