Developing a Flutter Plugin (native_image_view): Creation, Communication, Testing, and Publishing
The article walks through the complete workflow for building a Flutter plugin—using native_image_view as an example—including platform channel communication, Dart and native (iOS/Android) implementation, testing via an example app, and publishing to public or private Pub repositories.
This article introduces the complete workflow for creating a Flutter plugin, using the native_image_view plugin as a concrete example. It covers the motivation for plugins, the Platform Channel mechanism, plugin creation commands, implementation on both Flutter and native sides (iOS and Android), testing, and publishing to public or private repositories.
1. Overview
Flutter’s ecosystem provides a Dart package repository, but many scenarios require custom plugins that wrap native functionality. The native_image_view plugin demonstrates how to expose image loading (including caching) from native code to Flutter.
2. Flutter‑Native Communication
Flutter communicates with native code via Platform Channels , which follow a client‑server model. Three channel types are available:
BasicMessageChannel – for sending strings or semi‑structured data.
MethodChannel – for invoking methods and receiving callbacks.
EventChannel – for streaming data.
All channels share three essential members:
String name – a unique identifier, e.g., com.tencent.game/native_image_view .
BinaryMessenger messager – the transport layer that passes binary data between Flutter and native.
MessageCodec/MethodCodec codec – handles encoding/decoding of data.
The native_image_view plugin uses only MethodChannel to request an image (local or remote) from native code and return the binary data.
3. Plugin Creation
Two kinds of Flutter components exist:
Flutter Package – contains only Dart code.
Flutter Plugin – contains Dart code plus iOS/Android native implementations.
Creating a Dart package:
flutter create --template=package helloCreating a plugin with native code:
flutter create --template=plugin --org com.tencent.game -i objc -a java native_image_viewThe generated structure includes android/ , ios/ , and example/ directories.
4. Plugin Development
4.1 Flutter side
Define a MethodChannel with the same name as the native side and invoke getImage :
class _NativeImageViewState extends State
{
Uint8List _data;
static const MethodChannel _channel = MethodChannel('com.tencent.game/native_image_view');
@override
void initState() {
super.initState();
loadImageData();
}
loadImageData() async {
_data = await _channel.invokeMethod("getImage", {"url": widget.url});
setState(() {});
}
@override
Widget build(BuildContext context) {
return _data == null
? Container(color: Colors.grey, width: widget.width, height: widget.height)
: Image.memory(_data, width: widget.width, height: widget.height, fit: BoxFit.cover);
}
}4.2 iOS implementation
+ (void)registerWithRegistrar:(NSObject
*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"com.tencent.game/native_image_view" binaryMessenger:[registrar messenger]];
NativeImageViewPlugin* instance = [[NativeImageViewPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"getImage" isEqualToString:call.method]) {
[self getImageHandler:call result:result];
} else {
result(FlutterMethodNotImplemented);
}
}
- (void)getImageHandler:(FlutterMethodCall*)call result:(FlutterResult)result {
NSString *url = call.arguments[@"url"];
if ([url hasPrefix:@"localImage://"]) {
NSString *imageName = [url stringByReplacingOccurrencesOfString:@"localImage://" withString:@""];
UIImage *image = [UIImage imageNamed:imageName];
if (image) {
NSData *imgData = UIImageJPEGRepresentation(image, 1.0);
result(imgData);
} else {
result(nil);
}
} else {
UIImage *image = [[SDImageCache sharedImageCache] imageFromCacheForKey:url];
if (!image) {
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:[NSURL URLWithString:url]
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (finished) {
result(data);
[[SDImageCache sharedImageCache] storeImage:image forKey:url completion:nil];
}
}];
} else {
NSData *imgData = UIImageJPEGRepresentation(image, 1.0);
result(imgData);
}
}
}4.3 Android implementation
public class NativeImageViewPlugin implements FlutterPlugin, MethodCallHandler {
private MethodChannel channel;
private Context context;
@Override
public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "com.tencent.game/native_image_view");
channel.setMethodCallHandler(this);
context = flutterPluginBinding.getApplicationContext();
}
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getImage")) {
getImageHandler(call, result);
} else {
result.notImplemented();
}
}
private void getImageHandler(MethodCall call, Result result) {
String urlStr = (String) call.argument("url");
Uri uri = Uri.parse(urlStr);
if ("localImage".equals(uri.getScheme())) {
// Load local drawable and return byte[]
} else {
Glide.with(context).download(urlStr).into(new CustomTarget
() {
@Override
public void onResourceReady(@NonNull File resource, @Nullable Transition
transition) {
byte[] bytesArray = new byte[(int) resource.length()];
try {
FileInputStream fis = new FileInputStream(resource);
fis.read(bytesArray);
fis.close();
result.success(bytesArray);
} catch (IOException e) {
result.error("READ_FAIL", e.toString(), null);
}
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
result.error("LOAD_FAIL", "image download fail", null);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
result.error("LOAD_CLEARED", "image load clear", null);
}
});
}
}
}5. Plugin Testing
The generated example/ project references the plugin via a relative path, allowing developers to run and verify functionality before publishing.
native_image_view:
path: ../In main.dart the plugin is used as follows:
String url = "https://iknow-pic.cdn.bcebos.com/79f0f736afc379313a380ef3e7c4b74542a91193";
// String url = "localImage://wdqy.jpeg";
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('example')),
body: Center(
child: NativeImageView(url: url, width: 300, height: 200),
),
),
);
}6. Plugin Publishing
For public distribution, update pubspec.yaml with metadata and run:
flutter pub publish --dry-runAfter fixing any warnings, publish with:
flutter pub publishFor private distribution, set up a private Pub server (e.g., using pub_server ) and add a publish_to field:
publish_to: http://192.168.1.3:8081Then publish with:
flutter packages pub publish --server=http://192.168.1.3:8081Consumers can reference the private plugin using the hosted syntax in their own pubspec.yaml .
Conclusion
The article walks through Platform Channel basics, the end‑to‑end creation, native implementation, testing, and both public and private publishing of a Flutter plugin. It demonstrates how encapsulating native capabilities in a plugin improves project modularity and reduces coupling between Flutter and platform code.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.