An In-Depth Dive Into Streaming Data Across Platform Channels on Flutter
In this tutorial, you’ll learn how to use Platform Channels to stream data into your Flutter app. By Wilberforce Uwadiegwu.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
An In-Depth Dive Into Streaming Data Across Platform Channels on Flutter
25 mins
- Getting Started
- Platform Channels: An Overview
- Setting Up Event Consumption in Dart
- Reacting to Streamed Events
- Event Dispatching on Android
- Setting Up the EventChannel
- Dispatching Events
- Tearing Down the EventChannel
- Event Dispatching on iOS
- Setting Up EventChannel
- Dispatching Events
- Tearing Down the EventChannel
- Streaming Arbitrary Bytes
- Receiving, Concatenating and Consuming Bytes on Flutter
- Streaming Bytes on Android
- Streaming Bytes on iOS
- Where to Go From Here?
Flutter’s Platform Channels are a set of APIs that facilitate calling native platform APIs from Dart. There are three main platform channel APIs that can be used to communicate back and forth with the underlying native technology: MessageChannel, MethodChannel and EventChannel. These APIs provide an interface to call methods and pass data between code written in Java/Kotlin or Objective-C/Swift and Dart.
Platform channels make it easy to integrate native functionality, like camera, geolocation and deep links, into Flutter apps. In this tutorial, you’ll use platform channels to stream predefined events and arbitrary data from iOS and Android to Dart. You’ll learn:
- How to use Event Channels to stream connectivity information to your flutter app.
- Using Event Channels to send images in the form of binary data to your flutter app.
- Setting up both native Android and iOS clients to interact with your Dart code.
Getting Started
To get started, download the starter project by clicking Download Materials at the top or bottom of this tutorial.
Open starter with the latest version of Android Studio or Visual Studio. Then, run flutter packages get
from Terminal. If you’re building for iOS, you’ll also need to install iOS dependencies by navigating to the iOS directory and running pod install
.
Run the project. You’ll see something like this:
In this tutorial, you’ll build an app whose UI reacts in real-time to the platform’s network state events. Then, you’ll add support to stream and display an image’s bytes from the platform on Flutter.
But first, take a closer look at platform channels.
Platform Channels: An Overview
Like previously mentioned, Platform Channels are a way to send information from Dart to the native iOS or Android code. The platform channels API is made up of a few different utilities:
Binary Messaging is the lowest level Platform channel API. It’s a bi-directional and synchronous API that sits between Dart and platform code and facilitates passing byte buffers over a channel. It’s used to send sequences of bytes between Dart and platforms, such as iOS and Android.
The rest of the platform channel APIs are built atop Binary Messaging. They add support for high-level data structures like strings, maps and lists. As stated earlier, platform channel consists of MessageChannel, MethodChannel and EventChannel.
MessageChannel exposes raw data in the form of byte buffers. You can pass a codec to interpret the bytes to a high-level data structure, like an image. While Dart provides some off-the-shelf codecs, you can also write your own custom ones.
MethodChannel invokes named methods, with or without arguments, between Dart and native code. You can use it to send a one-off send-and-reply message. It doesn’t support a continuous stream of data.
EventChannel exposes data from the platform to Dart as streams. When you subscribe to an event channel on the Dart end, you get a continuous stream of data from iOS or Android.
To recap, Platform Channels allow you to send and received data both to and from platforms across a channel. In this case, the channel is a string, an identifier of the message and its destination.
With that out of the way, it’s time to write some code and start consuming some events!
Setting Up Event Consumption in Dart
Before you can use an EventChannel
, you need to declare it.
Open network_stream_widget.dart. Inside NetworkStreamWidget
, declare the eventChannel
above build()
like this:
final _eventChannel = const EventChannel('platform_channel_events/connectivity');
Then import 'package:flutter/services.dart'
in the same file.
Congraulations, you’ve just created your first platform channel! You created an EventChannel
. The eventChannel
‘s name is "platform_channel_events/connectivity"
. The name can be anything you want, but it has to be unique.
Next, you’ll need a set of constant values for the supported network states. Like the event name, these values can be arbitrary, but must be consistent across Android, iOS, and Dart.
In constants.dart, inside Constants
, add:
/// Event for when network state changes to Wifi
static const wifi = 0xFF;
/// Event for when network state changes to cellular/mobile data
static const cellular = 0xEE;
/// Event for when network is disconnected
static const disconnected = 0xDD;
/// Event for when network state is a state you do not
/// support (e.g VPN or Ethernet on Android)
static const unknown = 0xCC;
This code only supports wifi, cellular and disconnected states. You can use unknown for any other network state that is out of the scope of this tutorial.
That set of constants is great, but it’d be great to have an enumerated set of values for better type safety. In utils.dart, add the following declaration right above the commented statements:
/// Connection is an enum of supported network states
enum Connection {
/// When connection state is [Constants.wifi]
wifi,
/// When connection state is [Constants.cellular]
cellular,
/// When connection state is [Constants.disconnected]
disconnected,
/// When connection state is [Constants.unknown]
unknown
}
Now that you’ve got an enum
, you need a function to map the network constants to Connection
. First, import 'constants.dart'
in utils.dart. Then add this statement below the enum
you just declared:
/// converts the network events to the appropriate values of
/// the [Connection] enum
Connection intToConnection(int connectionInt) {
var connection = Connection.unknown;
switch (connectionInt) {
case Constants.wifi:
connection = Connection.wifi;
break;
case Constants.cellular:
connection = Connection.cellular;
break;
case Constants.disconnected:
connection = Connection.disconnected;
break;
case Constants.unknown:
connection = Connection.unknown;
break;
}
return connection;
}
Each connection state is going to be shown with a different color and different text, so you’ll need two more similar functions: getConnectionColor()
and getConnectionMessage()
. getConnectionColor()
maps Connection
to Color objects while getConnectionMessage()
maps Connection
to user-readable strings.
You’ll find these functions in utils.dart. Uncomment them and then import 'package:flutter/material.dart'
.
Since you’re using an EventChannel
, you need to get the actual stream from the eventChannel
object.
Open network_stream_widget.dart. In NetworkStreamWidget
, just above the return statement in build()
, add:
final networkStream = _eventChannel
.receiveBroadcastStream()
.distinct()
.map((dynamic event) => intToConnection(event as int));
Then import 'utils.dart'
into same file.
In the piece of code above, you use the receiveBroadcastStream()
method to get a stream of events from the EventChannel. You used the distinct()
method to filter out duplicate events. Finally, you converted the events to Connection
values with intToConnection()
.
Run the project. You’ll see a UI similar to the previous screenshot.
Reacting to Streamed Events
Now it’s time to update the UI. You’re going to use a StreamBuilder
since you’re operating on a Stream
returned from eventChannel
.
Still in NetworkStreamWidget
, replace the returned widget in build()
with:
@override
Widget build(BuildContext context) {
...
return StreamBuilder<Connection>(
initialData: Connection.disconnected,
stream: networkStream,
builder: (context, snapshot) {
final connection = snapshot.data ?? Connection.unknown;
final message = getConnectionMessage(connection);
final color = getConnectionColor(connection);
return _NetworkStateWidget(message: message, color: color);
},
);
}
With the statements above:
- First, you return a
StreamBuilder
frombuild()
. - You pass
Connection.disconnected
as the initial state for the widget, andnetworkStream
as the actualStream
being used. - In
StreamBuilder
‘sbuilder()
, you get the event from thesnapshot
and convert it to color and user-readable string. - Finally, you pass these values to
_NetworkStateWidget
._NetworkStateWidget
builds the widgets necessary to display the network states.
Run the project. You’ll something like this:
It’s starting to look good, but it’s not reactive yet. Also, a MissingPluginException
error appears in the log, but that’s expected for now.
Now that you have everything all wired up on the Dart side, it’s time to
tackle event dispatching on Android.
Event Dispatching on Android
Since you’re going to be working with native android code, you’ll write this section in Kotlin. You’ll set up an EventChannel with the same name you used earlier. Then you’ll listen for network state changes and notify the event callback of the EventChannel when the network state changes.
Setting Up the EventChannel
The first step is to create an EventChannel
in native Android code to mimic the one you created in Dart.
In the starter project directory, open the android folder with Android Studio. Now open the MainActivity
class and declare the channel name above configureFlutterEngine()
:
private val networkEventChannel = "platform_channel_events/connectivity"
Next, in configureFlutterEngine()
, right below the call to super
add:
EventChannel(flutterEngine.dartExecutor.binaryMessenger, networkEventChannel)
.setStreamHandler(NetworkStreamHandler(this))
And then import io.flutter.plugin.common.EventChannel
.
The above code creates a new EventChannel
with the channel name you specified in the last step. It uses the default BinaryMessenger
provided by the flutter engine and then sets a StreamHandler
to facilitate the actual event stream.
You’re done with MainActivity
for now. Good job! You’re killing it!👏
Now open NetworkStreamHandler
, and declare the event callback above onListen()
like this:
private var eventSink: EventChannel.EventSink? = null
An EventSink
is just a fancy callback.
Inside onListen()
, assign eventSink
and call startListeningNetworkChanges()
:
eventSink = events
startListeningNetworkChanges()
To save you some time, startListeningNetworkChanges()
is already implemented for you. It gets a reference to Android’s connectivity manager and registers networkCallback
. networkCallback
receives changes to the network state.
Next, you’ll work on dispatching connectivity events.
Dispatching Events
Now you’ll declare constants for the supported network states in Android, just like you did in Flutter.
In the Constants.kt file, add these statements inside Constants
:
const val wifi = 0xFF
const val cellular = 0xEE
const val disconnected = 0xDD
const val unknown = 0xCC
The actual event dispatching to Flutter happens in networkCallback
inside NetworkStreamHandler
. Override onLost()
and onCapabilitiesChanged()
like this:
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onLost(network: Network) {
super.onLost(network)
// Notify Flutter that the network is disconnected
activity?.runOnUiThread { eventSink?.success(Constants.disconnected) }
}
override fun onCapabilitiesChanged(network: Network, netCap: NetworkCapabilities) {
super.onCapabilitiesChanged(network, netCap)
// Pick the supported network states and notify Flutter of this new state
val status =
when {
netCap.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> Constants.wifi
netCap.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> Constants.cellular
else -> Constants.unknown
}
activity?.runOnUiThread { eventSink?.success(status) }
}
}
Make sure to add the import: import android.net.Network
.
The above code may be intimidating, but it’s actually pretty simple. Here’s a breakdown:
- First, you’re sending a
Constants.disconnected
message to theeventSink
callback whenever connectivity is lost. - Then, you’re checking what the network state is in the
onCapabilitiesChanged
method. If the network has WIFI you send thewifi
constant. If it has cellular, you send thecellular
constant. Otherwise you send anunknown
constant.
Note that the OS calls both onLost
and onCapabilitiesChanged
in the background. Since Flutter requires you to send platform channel events on the main thread, runOnUiThread
is used.
Next, you’ll clean up after yourself by tearing down the EventChannel.
Tearing Down the EventChannel
To shut down the event channel, you need to stop listening to network changes and nullify the fields.
Still in NetworkStreamHandler
, call the following statements inside onCancel()
:
stopListeningNetworkChanges()
eventSink = null
activity = null
Like startListeningNetworkChanges()
, stopListeningNetworkChanges()
is already implemented for you.
Run the project on Android. Toggle Wifi and Mobile Data and you’ll see the UI update:
That’s all for Android! Time to head over to iOS.
Event Dispatching on iOS
Now that you’re in iOS land, you’ll write this section in Swift with Xcode.
In the ios folder in starter project open Runner.xcworkspace with Xcode.
As you did for Android, you’ll set up an EventChannel with the same name as Flutter. Then, you’ll listen for network state changes and dispatch the events to Flutter.
First, you’ll set up the EventChannel.
Setting Up EventChannel
Inside AppDelegate.swift, declare the channel name above application()
in AppDelegate
:
private let networkEventChannel = "platform_channel_events/connectivity"
You’ll be using Reachability, a popular iOS library, to listen for changes to network states.
Still in AppDelegate
, paste these statements inside application()
, above GeneratedPluginRegistrant.register(with: self)
:
let controller = window?.rootViewController as! FlutterViewController
FlutterEventChannel(name: networkEventChannel, binaryMessenger: controller.binaryMessenger)
.setStreamHandler(NetworkStreamHandler(reachability: reachability))
In the statements above, you:
- Get a reference to the root
ViewController
and instantiate anEventChannel
with the same name you used in Flutter and Android. - Then pass an instance of
NetworkStreamHandler
as the stream handler to the EventChannel.NetworkStreamHandler
is responsible for dispatching the events to Flutter.
Next up you’ll dispatch the actual connectivity events.
Dispatching Events
Once again, in Constants.swift add the following constants as members of Constants
:
static let wifi = 0xFF
static let cellular = 0xEE
static let disconnected = 0xDD
static let unknown = 0xCC
Now, open NetworkStreamHandler.swift. Then declare the event callback right below the Reachability variable in NetworkStreamHandler
:
private var eventSink: FlutterEventSink? = nil
Next, you’ll set up Reachability in NetworkStreamHandler
. First, create a function that Reachability calls when the network state changes. In this function, you’ll use switch
to check the state and send the appropriate event to Flutter.
Below onCancel()
add:
@objc func connectionChanged(notification: NSNotification) {
let reachability = notification.object as! Reachability
switch reachability.connection {
case .wifi:
eventSink?(Constants.wifi)
case .cellular:
eventSink?(Constants.cellular)
case .unavailable:
eventSink?(Constants.disconnected)
case .none:
eventSink?(Constants.unknown)
}
}
Now, you need to tell Reachability, “Hey, buddy, call connectionChanged
when you detect a network state change :]”.
Inside onListen()
, right above the return statement, add:
eventSink = events
NotificationCenter.default.addObserver(self, selector: #selector(connectionChanged(notification:)), name: Notification.Name.reachabilityChanged, object: reachability)
do {
try reachability.startNotifier()
} catch {
return FlutterError(code: "1", message: "Could not start notififer", details: nil)
}
Here you assign eventSink
and observe network changes with Reachability.
Now, you’ll tear down the EventChannel
.
Tearing Down the EventChannel
To clean up the EventChannel, you need to nullify eventSink
and stop observing network state changes.
Inside onCancel
, paste this code above the return statement:
reachability.stopNotifier()
NotificationCenter.default.removeObserver(self, name: .reachabilityChanged, object: reachability)
eventSink = nil
Run the project on iOS. You’ll see something like this:
Congratulations! You completed the first part of this tutorial!
Next up, you’ll learn how to stream an image from native code to Dart.
Streaming Arbitrary Bytes
The solution above works when you want an infinite stream of events. But what if you just want to stream a sequence of bytes and terminate? For example, maybe you want to stream an image, video or file.
In this section, you’ll solve this problem by streaming an image from native Android and iOS to Flutter. Additionally, a progress indicator will visualize the stream’s progress.
You’ll start by listening for images in the form of raw bytes.
Receiving, Concatenating and Consuming Bytes on Flutter
Since you’re streaming an arbitrary list of bytes, you’ll need some way to know that you’re actually at the end of that list. To do that, you’ll need a special delimiter signal.
In constants.dart, add the following Constants
:
/// Event that denotes an end of the stream
static const eof = -0xFFFFFF;
You’ll use this value at the end of the stream to signify the end of your image. Note that like the other constants in Constants
, this can be any value as long as it’s consistent across Android and iOS.
Next, inside main.dart, find build()
of _MyHomePage
. Replace the the Expanded widget with const Expanded(child: ImageStreamWidget())
. Then import 'network_stream_widget.dart'
.
body
should now look like this:
body: Column(
children: [
const NetworkStreamWidget(),
const Expanded(child: ImageStreamWidget()),
],
),
To receive and process the events, you’ll divide the image into three events:
- File size: The first event.
- The actual image bytes: Received in chunks. Successive bytes are concatenated.
- End of stream: The last event.
In _ImageStreamWidgetState
, below startImageStream()
, add the following function.
void onReceiveImageByte(dynamic event) {
// Check if this is the first event. The first event is the file size
if (imageSize == null && event is int && event != Constants.eof) {
setState(() => imageSize = event);
return;
}
// Check if this is the end-of-file event.
// End-of-file event denotes the end of the stream
if (event == Constants.eof) {
imageSubscription?.cancel();
setState(() => streamComplete = true);
return;
}
// Receive and concatenate the image bytes
final byteArray = (event as List<dynamic>).cast<int>();
setState(() => imageBytes.addAll(byteArray));
}
This function handles processing the events.
Make sure to import 'constants.dart'
as well.
Next, you need a channel to handle the events. Declare the image stream EventChannel in _ImageStreamWidgetState
anywhere above build()
like this:
final eventChannel = const EventChannel('platform_channel_events/image');
Import 'package:flutter/services.dart'
into the same file.
Note that this channel’s name is different than what you used for streaming connectivity because this is a new channel that serves a different purpose.
Now, you need to receive the events from the channel. Paste this statement inside startImageStream()
:
imageSubscription = eventChannel.receiveBroadcastStream(
{'quality': 0.9, 'chunkSize': 100}).listen(onReceiveImageByte);
In the statements above, you:
- Listen to the event stream and call
onReceiveImageByte()
when there’s new data. - You pass
quality
andchunkSize
to the native ends.quality
is the quality of the image you want to receive andchunkSize
determines the number of chunks you want to split the image bytes into.
Run the project. You’ll see something like this:
Tap Stream Image. At this point, you won’t see any UI changes. You’ll also see a MissingPluginException
error in the log. Don’t worry, that’s expected!
Next up you’ll send the image on Android.
Streaming Bytes on Android
In Constants.kt, inside Constants
add:
const val eof = -0xFFFFFF
Go back to MainActivity.kt. Below networkEventChannel
, declare the image event channel name like this:
private val imageEventChannel = "platform_channel_events/image"
Next, in configureFlutterEngine()
, right below the call to super
, call this statement:
EventChannel(flutterEngine.dartExecutor.binaryMessenger, imageEventChannel)
.setStreamHandler(ImageStreamHandler(this))
Now, open ImageStreamHandler.kt, and declare the event callback above onListen()
like so:
private var eventSink: EventChannel.EventSink? = null
This should all look pretty similar, since it’s exactly the same setup you used for connectivity changes!
Next, paste this function in ImageStreamHandler.kt below onCancel()
:
private fun dispatchImageEvents(quality: Double, chunkSize: Int) {
GlobalScope.launch(Dispatchers.IO) {
if (activity == null) return@launch
// Decode the drawable
val bitmap = BitmapFactory.decodeResource(activity!!.resources, R.drawable.cute_cat_unsplash)
// Compress the drawable using the quality passed from Flutter
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, (quality * 100).toInt(), stream)
// Convert the compressed image stream to byte array
val byteArray = stream.toByteArray()
// Dispatch the first event (which is the size of the array/image)
withContext(Dispatchers.Main) {
eventSink?.success(byteArray.size)
}
// Split the array into chunks using the chunkSize passed from Flutter
val parts = byteArray.size / chunkSize
val chunks = byteArray.toList().chunked(parts)
// Loop through the chunks and dispatch each chuck to Flutter
chunks.forEach {
// Mimic buffering with a 50 mills delay
delay(50)
withContext(Dispatchers.Main) {
eventSink?.success(it)
}
}
withContext(Dispatchers.Main) {
// Finally, dispatch an event to indicate the end of the image stream
eventSink?.success(Constants.eof)
}
}
}
And add the following import statements:
import kotlinx.coroutines.* import java.io.ByteArrayOutputStream import android.graphics.Bitmap import android.graphics.BitmapFactory
That’s a lot of code! Luckily you don’t really need to know much about it – all you need to know is that it gets an image contained in the project and converts it into a list of bytes on a background thread using Coroutines.
Next, you’ll assign eventSink
and get the parameters you passed from Flutter. Then, you’ll call dispatchImageEvents()
with these parameters.
Write these statements inside onListen()
:
eventSink = events
val quality = (arguments as Map<*, *>)["quality"] as Double
val chunkSize = arguments["chunkSize"] as Int
dispatchImageEvents(quality, chunkSize)
Then, clean up by nullifying eventSink
and activity
in onCancel()
:
eventSink = null
activity = null
Run the app on Android and tap Stream Image. You’ll see something like this:
This beautiful cat photo is by Cédric VT on Unsplash
Now, you’ll replicate the same steps you did on Android for iOS but in Swift.
Streaming Bytes on iOS
In Constants.swift, add a new member to Constants
:
static let eof = -0xFFFFFF
Declare the event name in AppDelegate
below networkEventChannel
like this:
private let imageEventChannel = "platform_channel_events/image"
Next, you’ll set the handler of the image stream events.
Navigate to application()
. Inside AppDelegate
, above GeneratedPluginRegistrant.register(with: self)
, add:
FlutterEventChannel(name: imageEventChannel, binaryMessenger: controller.binaryMessenger)
.setStreamHandler(ImageStreamHandler())
Then, navigate to ImageStreamHandler.swift. In ImageStreamHandler
, below onCancel()
, paste:
func dispatchImageEvents(quality: Double, chunkSize: Int, eventSink: @escaping FlutterEventSink) -> Void {
// Load the image into memory
guard let image = UIImage(named: "cute_dog_unsplash.jpg"),
// Compress the image using the quality passed from Flutter
let data = image.jpegData(compressionQuality: CGFloat(quality)) else {return}
// Get the size of the image
let length = data.count
// Dispatch the first event (which is the size of the image)
eventSink(length)
DispatchQueue.global(qos: .background).async {
// Split the image into chunks using the chunkSize passed from Flutter
let parts = length / chunkSize
var offset = 0
// Repeat this block of statements until you have dispatched the last chunk
repeat {
// Mimic buffering with a 1 mill delay
usleep(1000)
let thisChunkSize = ((length - offset) > parts) ? parts : (length - offset)
let chunk = data.subdata(in: offset..<offset + thisChunkSize)
// Dispatch each chunk to Flutter
eventSink(chunk)
offset += thisChunkSize
} while (offset < length)
// Dispatch an event to indicate the end of the stream
eventSink(Constants.eof)
}
}
Just like on Android, the above code is just chunking an image into a list of bytes.
Finally, you need to pass the arguments you passed from Flutter to dispatchImageEvents
.
Paste this function in onListen()
, right above the return statement:
let args = arguments as! Dictionary<String, Any>
let quality = args["quality"] as! Double
let chunkSize = args["chunkSize"] as! Int
dispatchImageEvents(quality: quality, chunkSize: chunkSize, eventSink: events)
Run the app on iOS and tap Stream Image. You'll see something like this:
This beautiful dog photo is by Marliese Streefland on Unsplash
Congratulations! You did such an amazing job. Now, go out there and continue building world-class apps!
Where to Go From Here?
You can find the completed project inside completed in files you downloaded earlier or by clicking Download Materials at the top or bottom of this tutorial.
In this tutorial, you learned how to stream from predetermined events to arbitrary bytes from native Android and iOS to Flutter.
To learn more about the differences between MethodChannel and EventChannel, see this Stackoverflow discussion. If you're interested in knowing how Flutter Platform Channels works under the hood, checkout this post by Mikkel Ravn.
I hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!