The Flutter team covers how to extract information from your native platform using Method Channels. Method Channels are great for requesting data from your native platform when you need it, but what if you want to stream in data from your native platform continuously? That's where Event Channels come in. An Event Channel is an object that allows you to constantly send information from the native portion of your app to the Flutter portion of your app via a stream.

In this tutorial, you will create an Event Channel to continuously send random numbers from the native portion of your app (iOS or Android) to the Flutter portion of your app.


Prerequisites

Note: Although Xcode is optional, we will write code for iOS and Android.

Step 1 - Initial Setup

Let start by creating a new Flutter Plugin project and name the project "event_channel_tutorial."  Once you have set up a new project, you should have a Dart file generated for you that contains a single MethodChannel. Here is my file, for example:

event_channel_tutorial.dart

Step 2 - Defining our Event Channel API

Let's start by defining an Event Channel. I have named my channel "random_number_channel." Naming your Channel is required, and your native code must also reference the same channel name; we'll see this very soon.

import 'dart:async';

import 'package:flutter/services.dart';

class EventChannelTutorial {
  static const MethodChannel _channel =
  const MethodChannel('event_channel_tutorial');


  // New Event Channel
  static const EventChannel _randomNumberChannel = const EventChannel('random_number_channel');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}
event_channel_tutorial.dart

Now we need a way to listen for new random numbers from the native portion of our app. Event Channel's come with a Stream object to let you hear for events. You can access the Stream object via the receiveBroadcastStream method.

import 'dart:async';

import 'package:flutter/services.dart';

class EventChannelTutorial {
  static const MethodChannel _channel =
  const MethodChannel('event_channel_tutorial');

  // New
  static const EventChannel _randomNumberChannel = const EventChannel('random_number_channel');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }

  // New
  static Stream<int> get getRandomNumberStream {
    return _randomNumberChannel.receiveBroadcastStream().cast();
  }
}
event_channel_tutorial.dart

Step 3 - Update the Example app.


We are now ready to use our API. Let's head over to the example/main.dart file. Modify your build method with the code down below:

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Column(
          children: [
            Text('Running on: $_platformVersion\n'),
            StreamBuilder<int>(
              stream: EventChannelTutorial.getRandomNumberStream,
              builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
                if(snapshot.hasData) {
                  return Text("Current Random Number: ${snapshot.data}");
                } else {
                  return Text("Waiting for new random number...");
                }
              },
            ),
          ],
        ),
      ),
    );
  }

Since we will be listening for events from a Stream object, I have created a StreamBuilder to react to our Event Channel's events. Suppose data is available from the random number stream. In that case, we display a Text widget that shows the current random number. If there is no data available, we display a Text widget that informs the user that we are waiting for data. When you run your app for the first time, you should see two text widgets, one that shows the current platform version and the other saying that we are waiting for a new random number.

Step 4 - iOS App Setup

Now we need to update the native portion of our app so that we can send random numbers to the Flutter portion of our app. Head over to the swift file named SwiftEventChannelTutorialPlugin.swift located in ios/Classes. You should see something like this:

We need to set up an Event Channel with the same name as the Event Channel we created in our Dart API. After line 8, please add the following:

let randomNumberChannel = FlutterEventChannel(name: "random_number_channel", binaryMessenger: registrar.messenger())

We need to create a class that will generate a random number and sends that number to our app's Flutter portion. This class should trigger code when the Flutter portion of our app listens to the random event channel stream or cancels that stream. Luckily there is a class that we can inherit from that does just that– FlutterStreamHandler. Create a file named "RandomNumberStreamHandler.swift" and add the following code to that file:

import Foundation
import Flutter

class RandomNumberStreamHandler: NSObject, FlutterStreamHandler{
    var sink: FlutterEventSink?
    var timer: Timer?
    
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        sink = events
        timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(sendNewRandomNumber), userInfo: nil, repeats: true)
        return nil
    }
    
    @objc func sendNewRandomNumber() {
        guard let sink = sink else { return }
        
        let randomNumber = Int.random(in: 1..<10)
        sink(randomNumber)
    }
    
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        sink = nil
        timer?.invalidate()
        return nil
    }
}

On line 5, we define a FlutterEventSink. We use a FlutterEventSink to feed data to the stream located in our Dart API. On line 6, we define a Timer object which we will use to fire the sendNewRandomNumber method periodically. The sendNewRandomNumber method checks if our FlutterEventSink exists. If it does, it will generate a random number and pass that number into the sink. Notice that there are two methods inside this class onListen and onCancel.

OnListen is triggered when our app's Flutter portion listens to the stream we obtain from the getRandomNumberStream method we created in step 2. The StreamBuilder we made in example/main.dart is listening to the stream received from the getRandomNumberStream method. Within OnListen we store the FlutterEventSink provided by OnListen into the sink variable to use it inside the sendNewRandomNumber method. We also create a timer that will fire the sendNewRandomNumber every second.

The onCancel method is triggered when you unsubscribe from the stream returned by the getRandomNumberStream method we created in step 2. In the OnCancel method, we nullify our sink and invalidate our timer.

We created a class called RandomNumberStreamHandler that will handle triggering code when we listen or cancel our random number generator stream. Still, we need to register our stream handler with the Flutter system; otherwise, the Flutter portion of our app will not communicate with the native part of our app. Go back to SwiftEventChannelTutorialPlugin.swift. Add the following lines after creating the random number Event Channel:

    let randomNumberStreamHandler = RandomNumberStreamHandler()
    randomNumberChannel.setStreamHandler(randomNumberStreamHandler)

Now we are ready to try our app again. In your Android Studio terminal, run to the following command within example/iOS:

pod install

Go back to Xcode and run the app. You should see a new random number appear every second.


Android Implementation

The process for the Android implementation is pretty similar to the iOS implementation. We start by defining our EventChannel within EventChannelTutorialPlugin.kt:

package com.example.event_channel_tutorial

import androidx.annotation.NonNull

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result

/** EventChannelTutorialPlugin */
class EventChannelTutorialPlugin: FlutterPlugin, MethodCallHandler {
  /// The MethodChannel that will the communication between Flutter and native Android
  ///
  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
  /// when the Flutter Engine is detached from the Activity
  private lateinit var channel : MethodChannel

  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.binaryMessenger, "event_channel_tutorial")
    channel.setMethodCallHandler(this)

	// New
    val randomNumberEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "random_number_channel")
  }

...
EventChannelTutorialPlugin.kt

We create a StreamHandler that sends the Flutter portion of our app a random number every second:

package com.example.event_channel_tutorial
import android.os.Handler
import io.flutter.plugin.common.EventChannel
import java.util.*

class RandomNumberStreamHandler: EventChannel.StreamHandler {
    var sink: EventChannel.EventSink? = null
    var handler: Handler? = null

    private val runnable = Runnable {
        sendNewRandomNumber()
    }

    fun sendNewRandomNumber() {
        val randomNumber = Random().nextInt(9)
        sink?.success(randomNumber)
        handler?.postDelayed(runnable, 1000)
    }

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
        sink = events
        handler = Handler()
        handler?.post(runnable)
    }

    override fun onCancel(arguments: Any?) {
        sink = null
        handler?.removeCallbacks(runnable)
    }
}
RandomNumberStreamHandler.kt

Finally, we set the RandomNumberStreamHandler as the StreamHandler for our Event Channel:

  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.binaryMessenger, "event_channel_tutorial")
    channel.setMethodCallHandler(this)

    val randomNumberEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "random_number_channel")
    // New
   randomNumberEventChannel.setStreamHandler(RandomNumberStreamHandler())
  }
EventChannelTutorialPlugin.kt


I hope you found this tutorial to be helpful. If you found this tutorial to be useful, please share this with others. You can find the source code for this tutorial here.

Are you looking to build a Flutter App for your business? Contact me here.