Listeners with EventChannel in Flutter

Back to Blog Home

Listeners with EventChannel in Flutter

Today, we are going to play with a really useful but quite ignored facility in the Flutter SDK, namely the EventChannel. It is a bridge between Dart and native code which is able to transmit recurring events without requiring multiple MethodChannel invokes from the receiving side. They are the best tool there is to implement listeners for app events occurring outside of Dart.

We are going to use our infamous playground project to bootstrap our implementation. Feel free to fork it and modify according to your needs. If you are interested in the details of how the playground works, please refer to this post.

Dart Side

EventChannels are created pretty much the same way as method channels. Let’s create a dedicated file to store our singleton reference and give it a name to easily recognize it.

// events.dart

import 'package:flutter/services.dart';

const _channel = const EventChannel('events');

Our playground will request a subscription to this channel by providing a callback function. It will also eventually cancel its subscription when it is done working with the results. We explicitly alias function types for these behaviors like below.

typedef void Listener(dynamic msg);
typedef void CancelListening();

In case you want to support multiple listeners simultaneously, you may want identify them to be able to recognize them during cancellation.

int nextListenerId = 1;

Finally, we create a helper function to initiate the communication with the native side like this.

CancelListening startListening(Listener listener) {
  var subscription = _channel.receiveBroadcastStream(
    nextListenerId++
  ).listen(listener, cancelOnError: true);

  return () {
    subscription.cancel();
  };
}

Test code for playground

Let’s import our new file to playground and run our newly created function in it.

import 'package:flutter/material.dart';
import 'events.dart';

  // Checkout the github repo ... 

class PlaygroundState extends State<Playground> {

  // ...

  // Main function called when playground is run
  bool running = false;
  void runPlayground() async {
    if (running) return;
    running = true;

    var cancel = startListening((msg) {
      setState(() {
        log(msg);
      });
    });

    await Future.delayed(Duration(seconds: 4));

    cancel();

    running = false;
  }
}

Android Native

On this side, we will have a FlutterActivity which holds this channel throughout its life time.

public class MainActivity extends FlutterActivity {
  private EventChannel channel;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);

    // Prepare channel
    channel = new EventChannel(getFlutterView(), "events");
    channel.setStreamHandler(new EventChannel.StreamHandler() {
      @Override
      public void onListen(Object listener, EventChannel.EventSink eventSink) {
        startListening(listener, eventSink);
      }

      @Override
      public void onCancel(Object listener) {
        cancelListening(listener);
      }
    });
  }

In Java (or Kotlin), a single instance to EventChannel.StreamHandler interface grabs all listener request by itself. Remember that we sent an identifier parameter in Dart to be able to distinguish listeners from each other. Here, Object listener is exactly that, converted to a Java int.

Let’s define the remaining methods to complete our circle.

// Listeners
  private Map<Object, Runnable> listeners = new HashMap<>();

  void startListening(Object listener, EventChannel.EventSink emitter) {
    // Prepare a timer like self calling task
    final Handler handler = new Handler();
    listeners.put(listener, new Runnable() {
      @Override
      public void run() {
        if (listeners.containsKey(listener)) {
          // Send some value to callback
          emitter.success("Hello listener! " + (System.currentTimeMillis() / 1000));
          handler.postDelayed(this, 1000);
        }
      }
    });

    // Run task
    handler.postDelayed(listeners.get(listener), 1000);
  }

  void cancelListening(Object listener) {
    // Remove callback
    listeners.remove(listener);
  }
}

This example simulates a timer that triggers each second. You may replace that behavior with a real listener to a native event source without changing the outline by copying the body of run() into your own block.

iOS Native

Flutter’s consistency in its own native APIs is really remarkable. Here, using Objective-C, we will glue all the pieces pretty much the same as Android.

Objective -C doesn’t support anonymous class instances. Therefore, we are going to define a FlutterStreamHandler in its own source file pair (.h/.m).

//
//  EventHandler.h
#import <Flutter/Flutter.h>
#import <Foundation/Foundation.h>

#ifndef EventHandler_h
#define EventHandler_h

@interface EventHandler : NSObject<FlutterStreamHandler>

- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
                                       eventSink:(FlutterEventSink)events;
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments;

@end

#endif /* EventHandler_h */
//
//  EventHandler.m

#include "EventHandler.h"

@implementation EventHandler
{
    // Listeners
    NSMutableDictionary* listeners;
}

- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
                                       eventSink:(FlutterEventSink)events {
    [self startListening:arguments emitter:events];
    return nil;
}


- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {
    [self cancelListening:arguments];
    return nil;
}

- (void) startListening:(id)listener emitter:(FlutterEventSink)emitter {
    // Prepare callback dictionary
    if (self->listeners == nil) self->listeners = [NSMutableDictionary new];
    
    // Get callback id
    NSString* currentListenerId =
        [[NSNumber numberWithUnsignedInteger:[((NSObject*) listener) hash]] stringValue];
    
    // Prepare a timer like self calling task
    void (^callback)(void) = ^() {
        void (^callback)(void) = [self->listeners valueForKey:currentListenerId];
        if ([self->listeners valueForKey:currentListenerId] != nil) {
            int time = (int) CFAbsoluteTimeGetCurrent();
            
            emitter([NSString stringWithFormat:@"Hello Listener! %d", time]);
            
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), callback);
        }
    };
    
    // Run task
    [self->listeners setObject:callback forKey:currentListenerId];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), callback);
}

- (void) cancelListening:(id)listener {
    // Get callback id
    NSString* currentListenerId =
        [[NSNumber numberWithUnsignedInteger:[((NSObject*) listener) hash]] stringValue];
    
    // Remove callback
    [self->listeners removeObjectForKey:currentListenerId];
}
@end

Native iOS classes for Flutter are always named as the same as their Android counterparts, with an added prefix of “Flutter”. Just like the same as before, we will store the listener id in a NSMutableDictionary to be able to map them back during cancellation. dispatch_after helps us define a timer like cycle to simulate a periodic event source. You may replace that with a real source of your choice.

To finish this up, let’s create our channel in our app delegate.

#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
#include "EventHandler.h"

@implementation AppDelegate
{
    FlutterEventChannel* channel;
}

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [GeneratedPluginRegistrant registerWithRegistry:self];

    // Prepare channel
    FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
    self->channel = [FlutterEventChannel eventChannelWithName:@"events" binaryMessenger:controller];

    [self->channel setStreamHandler:[EventHandler new]];

    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

Test

Boot up your emulator/simulator or plug your device to run your playground. If everything went smoothly, you now have an event source capable of listening events from the native platforms.

Source Code

You may find a complete implementation of this tutorial here. I hope it becomes useful. Until then, have a nice one!

Remember, sharing is caring!