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.
Related Topics
Take a look at these posts to learn about common communication patterns between Dart and native code if you already know what you are looking for.
- Flutter 101
- Flutter and Native Communication
- Flutter Plugin Playground
- Native Communication with a Callback using MethodChannel
- Listeners with EventChannel
Dart Side
EventChannel
s 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!