Platform-Specific Code With Flutter Method Channel: Getting Started
Learn how to communicate with some platform-specific code with Flutter method channels and extend the functionality of the Flutter application. 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
Platform-Specific Code With Flutter Method Channel: Getting Started
30 mins
- Getting Started
- Understanding the Method Channel
- Setting up Method Channel on iOS With Swift
- Understanding PhotoKit
- Requesting Permissions on iOS
- Fetching all Photos From iOS
- Reading Image Data From iOS
- Setting up the Method Channel on Flutter
- Building a Custom Image Provider in Flutter
- Rendering Images From the Host Device
- Rendering Selected Images
- Setting up Method Channel on Android with Kotlin
- Understanding Android’s Media API
- Requesting User Permissions on Android
- Fetching all Images From Android
- Reading Image Bytes on Android
- Consuming Method Calls From Host Platforms in Flutter
- Where to Go From Here?
Reading Image Data From iOS
For better performance, read the image data on demand, i.e., when Flutter actually needs to display that particular image. To read an image, you need the ID along with the width and height of the image widget. Start by getting the PHAsset
of the corresponding ID from fetchResult
with a linear search and read the image data from the asset.
Write this function below getPhotos()
:
private func fetchImage(args: Dictionary<String, Any>?, result: @escaping FlutterResult) {
// 1
let id = args?["id"] as! String
let width = args?["width"] as! Double
let height = args?["height"] as! Double
// 2
self.fetchResult?.enumerateObjects { (asset: PHAsset, count: Int, stop: UnsafeMutablePointer<ObjCBool>) in
if (asset.localIdentifier == id) {
// 3
stop.pointee = true
// 4
self.requestImage(width: width, height: height, asset: asset) { data in
// 5
result(data)
}
return
}
}
}
In this code, you:
- Get the method arguments.
- Get the matching
PHAsset
fromfetchResult
. - Stop the search, since a match has been found.
- Read the image data from
PHAsset
.requestImage
will be declared later. - Return the image data to Flutter.
Both the method argument and result handler were passed directly to fetchImage()
to avoid concurrency issues since Flutter will fire off image requests as the user scrolls across the grid of images.
Having retrieved PHAsset
, the next step is to fetch the image data. So, write this function below fetchImage()
:
private func requestImage(width: Double, height: Double, asset: PHAsset, onComplete: @escaping (Data) -> Void) {
let size = CGSize.init(width: width, height: height)
let option = PHImageRequestOptions()
option.isSynchronous = true
PHImageManager
.default()
.requestImage(for: asset, targetSize: size, contentMode: .default, options: option) { image, _ in
guard let image = image,
let data = image.jpegData(compressionQuality: 1.0) else { return }
onComplete(data)
}
}
The instructions above fetched the image data and then called the onComplete
closure with the data.
The final step for this session is to add another case to the method handler, like so:
case "fetchImage":
self?.fetchImage(args: call.arguments as? Dictionary<String, Any>, result: result)
Run the project with Xcode or your Flutter IDE, and you should notice that nothing has changed visually:
That’s all for the iOS side. Good job!
Setting up the Method Channel on Flutter
Start this session by going back to Android Studio or VSCode and opening constants.dart inside the common directory. Then, add this import statement above the MyStrings
class:
import 'package:flutter/services.dart';
Next, declare the method channel below the MyStrings
class:
const methodChannel = MethodChannel('com.raywenderlich.photos_keyboard');
Notice that the channel’s name is the same as the one you used earlier in Swift.
Head to home.dart in the widgets directory and write this function below onImageTap()
:
void getAllPhotos() async {
// 1
gridHeight = getKeyboardHeight();
// 2
final results = await methodChannel.invokeMethod<List>('getPhotos', 1000);
if (results != null && results.isNotEmpty) {
setState(() {
images = results.cast<String>();
// 3
showGrid = images.isNotEmpty;
// 4
focus.unfocus();
});
}
}
Here’s what this code does:
- Gets the height of the soft keyboard. This height is used as the height of the images grid widget.
- Calls the method. The method name is
getPhotos
and the argument is1000
. Recall that earlier you used this size to decide the number of images to fetch in Swift.invokeMethod()
wasawait
ed on because it returns aFuture
. Since you expect a list of objects, you specifyList
as the type forinvokeMethod()
. - Shows the images grid only if the host platform returns images.
- Dismisses the soft keyboard.
To put a closure on this step, add the following logic to the empty togglePhotos()
below build()
:
if (showGrid) {
setState(() {
showGrid = false;
focus.requestFocus();
});
} else {
getAllPhotos();
}
Note that BottomContainer
calls togglePhotos()
when the user taps the gallery icon.
Building a Custom Image Provider in Flutter
Flutter supports loading images from various sources, none of which fit the current use case. So, how do you render the images? The answer to this question lies partly in the implementation of FileImage, an ImageProvider used for decoding images from filehandles. You’ll implement something similar, but instead of reading the bytes from a file, you’ll use the method channel to read the bytes from the host platform.
Start by creating adaptive_image.dart inside the common directory. Then, declare an AdaptiveImage
class, which sublasses ImageProvider
:
class AdaptiveImage extends ImageProvider<AdaptiveImage> {
final String id;
final double width;
final double height;
AdaptiveImage({required this.id, required this.width, required this.height});
@override
ImageStreamCompleter load(AdaptiveImage key, DecoderCallback decode) {
// TODO: implement load
throw UnimplementedError();
}
@override
Future<AdaptiveImage> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<AdaptiveImage>(this);
}
}
This is the skeleton of this class, but here’s a breakdown:
-
load()
uses the variables of the class to load the appropriate image from the host platform. -
obtainKey
generates a key object. The key object describes the properties of the image to load. Since there’s no asynchronous task being done in this function,SynchronousFuture
is simply returned.SynchronousFuture
is aFuture
that completes immmediately.
Now, add the following import
statements:
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'constants.dart';
Subsequently, declare a function below obtainKey()
, like so:
Future<Codec> _loadAsync(AdaptiveImage key, DecoderCallback decode) async {
assert(key == this);
// 1
final bytes = await methodChannel.invokeMethod<Uint8List>(
'fetchImage', {'id': id, 'width': width, 'height': height});
// 2
if (bytes == null || bytes.lengthInBytes == 0) {
PaintingBinding.instance!.imageCache!.evict(key);
throw StateError("Image for $id couldn't be loaded");
}
return decode(bytes);
}
In this code, you:
- Request the image bytes from the host platform.
- Remove the image key from cache if the request fails.
Since you’re using the class as the key for the image, you need to implement a proper equality relation. To do this, override both the equality operator and hashCode
. Write this code below _loadAsync()
:
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AdaptiveImage &&
runtimeType == other.runtimeType &&
id == other.id &&
width == other.width &&
height == other.height;
@override
int get hashCode => id.hashCode ^ width.hashCode ^ height.hashCode;
This means that given two AdaptiveImage
objects, they’re both equal if they’re the same object and their id
, width
and height
properties are equal.
Although not required, you can also override toString()
for debugging purpose. So, add this function below the hashCode
override:
@override
String toString() {
return '${objectRuntimeType(this, 'AdaptiveImage')}('
'"$id", width: $width, height: $height)';
}
The final step is to implement load()
. Just like ImageFile
, you’ll return an instance of MultiFrameImageStreamCompleter
, like so:
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
debugLabel: id,
informationCollector: () sync* {
yield ErrorDescription('Id: $id');
});
MultiFrameImageStreamCompleter
is responsible for converting the bytes to a format that the Image widget can display. You can read more about it in Flutter’s documentation.
Run the project, and you shouldn’t see any changes yet.
Congrats! You’ve successfully implemented a custom image provider.