Flutter plug-in development - (advanced)

I. Overview

Flutter also has its own Dart Packages repository. The development and reuse of plug-ins can improve development efficiency and reduce the coupling degree of projects . Common functional modules such as network requests (http), user authorization (permission_handler) and other client-side development, we only need to introduce corresponding plug-ins to quickly integrate for the project Relevant capabilities, so as to focus on the realization of specific business functions.

In addition to using popular components in the warehouse, developers still need to develop new components when faced with common business logic splitting or the need to encapsulate native capabilities during the development of a Flutter project. This article takes a specific native_image_view plug-in as an example, and will introduce the creation, development, testing, and release of Flutter components from multiple aspects, trying to fully demonstrate the development and release process of the entire Flutter component.

2. Communication between Flutter and Native

During the development of Flutter plug-ins, data interaction between Flutter and Native is almost always required. Therefore, before developing plug-ins, let's briefly understand the Platform Channel mechanism .


The communication between Flutter and Native is realized through Platform Channel, which is a C/S model, in which Flutter acts as Client, and the iOS and Android platforms act as Host. Flutter sends messages to Native through this mechanism, and Native calls the platform after receiving the message. Implement its own API, and then return the processing result to the Flutter page.

The Platform Channel mechanism in Flutter provides three interaction methods:

  • BasicMessageChannel: used to pass strings and semi-structured information;

  • MethodChannel: used to pass method calls and handle callbacks;

  • EventChannel: used for monitoring and sending data streams.

Although these three channels have different purposes, they all contain three important member variables:

(1)String name

Indicates the name of the channel. There may be many channels in a project, and each channel should use a unique name, otherwise it may be overwritten. The recommended naming method is the name of the organization plus the name of the plug-in, for example: com.tencent.game/native_image_view, if a plug-in contains multiple channels, it can be further distinguished according to the functional modules.

(2)BinaryMessager messager

As the communication carrier between Native and Flutter, it can transfer the binary data converted by codec between Native and Flutter. Each channel must generate or provide a corresponding messager when it is initialized. If the channel registers a corresponding handler, the messager will maintain a mapping relationship between name and handler.

After the Native platform receives the message from the other party, meesager will distribute the content of the message to the corresponding handler for processing. After the processing is completed, the processing result can be returned to Flutter through the callback method result.

 

(3)MessageCodec/MethodCodec codec

It is used for encoding and decoding during the communication between Native and Flutter. The sender can encode the basic type of Flutter (or Native) into binary for data transmission, and the receiver Native (or Flutter) can convert the binary into the basic type that the handler can recognize. .

Note: The native_image_share plug-in implemented in this article only uses the most commonly used MethodChannel communication. Flutter passes the remote image address or local image file name to the native side through the MethodChannel. After the iOS and Android platforms get the image, convert it into binary and return it through result. More examples of MessageChannel and EventChannel can be provided for reference at the end of the article for extended reading.

3. Plug-in creation 

Flutter components can be divided into two types according to whether they contain native code:

  • Flutter Package (package) : Contains only dart code, generally an encapsulation implementation of flutter-specific functions, such as http packages for network requests.

  • Flutter Plugin (plugin) : In addition to the dart code, it also includes the code implementation of the Android and iOS platforms. It is often used to encapsulate the native capabilities of the client and provide them to the flutter project. For example, the flutter_keyboard_visibility plug-in used to determine the visible state of the keyboard monitors the keyboard opening and closing events on the iOS and Android sides respectively, and then passes the corresponding events to the Flutter project through the Platform Channel.

  • Flutter plugins can be created through Android Studio (Dart and Flutter plugins need to be installed in Android Studio), or created using the command line.

  • Create a Flutter plugin

flutter create --org com.qidian.image --template=plugin --platforms=android,ios -i objc -a java native_image_view 

  • Using the --template=plugin statement creates a plugin that contains both iOS and Android code;

  • Use the --org option to specify the organization, generally using reverse domain name notation;

  • Use the -i option to specify the iOS platform development language, objc or swift;

  • Use the -a option to specify the Android platform development language, java or kotlin.

The lib directory is used to store the code implementation of the package, and the Flutter scaffolding will automatically generate a dart file with the same name as the package.
The pubspec.yaml file must be very familiar to students who have done Flutter development. The package or plugin we rely on to develop the package needs to be declared in this file.

4. Plug-in development 

The development and release process of Plugin and Package are basically the same. In contrast, Plugin also involves the development of iOS and Android, and its implementation is more complicated.

In the scenario where Flutter is embedded in a native project, a common problem is that when the same image is used in both Flutter and the native project, both sides will be stored separately, that is, the image will be stored twice. Unlike Weex, Hippy and other JS-based cross-platform frameworks that rely on native image acquisition and display, Flutter manages images by itself and draws them directly through the Skia engine.


In response to this problem, this article will develop a Flutter plug-in (native_image_view) to hand over the download and cache of Flutter images to Native, and the Flutter side is only responsible for drawing the images. In addition, we can also define a special protocol to handle the call of local images, and at the same time solve the problem that Flutter cannot reuse local images of native projects.

 Note: The plug-in developed in this article is only used to introduce the development and release process of the plug-in. It is not recommended to use it directly in the production environment. For the issue of image secondary caching, you can also refer to the article about Texture (external texture) in the extended reading.

1. Flutter-side development

We first declare the MethodChannel of the plug-in on the Flutter side, and then initiate a method call to the Native side through invokeMethod (method name, parameter) in the initState method. Call setState, and use the Image.memory method to draw the binary data into a picture display.

native_image_view.dart:

class _NativeImageViewState extends State<NativeImageView> {
  Uint8List _data;
  static const MethodChannel _channel =
      const 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,
          );

2. Native development

(1) iOS development

The iOS platform of the plug-in uses the SDWebImage component to download and cache network images, so declare dependencies in the native_image_view.podspec file.


s.dependency 'Flutter'
s.dependency 'SDWebImage'
s.platform = :ios, '8.0'

The Flutter scaffolding automatically generates the NativeImageViewPlugin.m file and the registerWithRegistrar method for us. This method is the entry point for component execution and will be automatically called by Flutter's plug-in manager.

In this method, we create a MethodChannel with the same name as the Flutter side, and create an instance of the plug-in object to handle the method call on the Flutter side. The handleMethodCall method will be triggered after the MethodChannel receives a method call from the Flutter side. Developers can obtain the method name and parameters through FlutterMethodCall, and return the image content through FlutterResult.

NativeImageViewPlugin.m:

#import "NativeImageViewPlugin.h"
#import <SDWebImage/SDWebImage.h>

@implementation NativeImageViewPlugin
//组件注册接口,Flutter自动调用
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)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{
  if(call.arguments != nil && call.arguments[@"url"] != nil){
      NSString *url = call.arguments[@"url"];
      if([url hasPrefix:@"localImage://"]){
        //获取本地图片
        NSString *imageName = [url stringByReplacingOccurrencesOfString:@"localImage://" withString:@""];
        UIImage *image = [UIImage imageNamed:imageName];
        if(image != nil){
            NSData *imgData = UIImageJPEGRepresentation(image,1.0);
            result(imgData);
        }else{
            result(nil);
        }
      }else {
        //获取网络图片
        UIImage *image = [[SDImageCache sharedImageCache] imageFromCacheForKey:url];
        if(!image){
          //本地无缓存,下载后返回图片
          [[SDWebImageDownloader sharedDownloader]
            downloadImageWithURL:[[NSURL alloc] initWithString: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);
        }
      }
  }
}
@end

When processing an image call initiated by Flutter, first determine whether Flutter requests a local or network image. If it is a local image, read the binary data of the image directly from the UIImage object and return it; if it is a network image, first determine whether there is a local cache. If there is a cache, it will return directly. If there is no cache, you need to download the image first and then return the data. 

(2) Android development

The Android platform of the plug-in uses the Glide component to download and cache network images, and the dependencies need to be declared in the build.gradle file.

dependencies { implementation 'com.github.bumptech.glide:glide:4.11.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'}

In order to be compatible with the historical version, the Android-side plug-in needs to implement the same MethodChannel registration and monitoring logic in the onAttachedToEngine and registerWith methods. onMethodCall is used to process method calls in Flutter, and also provides MethodCall and Result objects similar to the iOS platform.

android/src/main/xxxx/NativeImageViewPlugin.java:

//新的插件注册接口
@Override
public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
  channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "com.tencent.game/native_image_view");
  channel.setMethodCallHandler(this);
  setContext(flutterPluginBinding.getApplicationContext());
}

@Override
public void onDetachedFromEngine(FlutterPluginBinding binding) {
  channel.setMethodCallHandler(null);
}

// Flutter-1.12之前的插件注册接口,功能与onAttachedToEngine一样
public static void registerWith(Registrar registrar) {
  NativeImageViewPlugin plugin = new NativeImageViewPlugin();
  plugin.setContext(registrar.context());
  final MethodChannel channel = new MethodChannel(registrar.messenger(), "com.tencent.game/native_image_view");
  channel.setMethodCallHandler(plugin);
}

@Override
public void onMethodCall(final MethodCall call,final Result result) {
  if (call.method.equals("getImage")) {
    getImageHandler(call,result);
  } else {
    result.notImplemented();
  }
}

The code implementation logic on the Android side is consistent with that on iOS. First, it is judged whether Flutter is calling a local or network image. For a local image, the Bitmap of the image is first obtained according to the file name, and then converted into a byte array and returned; the cache and download of the network image is based on Glide The component realizes that after obtaining the file cache or download path, it reads the file as a byte array and returns it.

public void getImageHandler(final MethodCall call,final Result result){
  HashMap map = (HashMap) call.arguments;
  String urlStr = map.get("url").toString();
  Uri uri = Uri.parse(urlStr);
  if("localImage".equals(uri.getScheme())){
    String imageName = uri.getHost();
    int lastIndex = imageName.lastIndexOf(".");
    if(lastIndex > 0){
      imageName = imageName.substring(0,lastIndex);
    }
    String imageUri = "@drawable/"+imageName;
    int imageResource = context.getResources().getIdentifier(imageUri, null, context.getPackageName());
    if(imageResource > 0){
      Bitmap bmp = BitmapFactory.decodeResource(context.getResources(),imageResource);
      ByteArrayOutputStream stream = new ByteArrayOutputStream();
      bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
      byte[] byteArray = stream.toByteArray();
      result.success(byteArray);
    }else{
      result.error("NOT_FOUND","file not found",call.arguments);
    }
  }else {
    Glide.with(context).download(urlStr).into(new CustomTarget<File>() {
      @Override
      public void onResourceReady(@NonNull File resource, @Nullable Transition<? super File> 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) {
          e.printStackTrace();
          result.error("READ_FAIL",e.toString(),call.arguments);
        }
      }
      @Override
      public void onLoadFailed(@Nullable Drawable errorDrawable) {
        super.onLoadFailed(errorDrawable);
        result.error("LOAD_FAIL","image download fail",call.arguments);
      }
      @Override
      public void onLoadCleared(@Nullable Drawable placeholder) {
        result.error("LOAD_CLEARED","image load clear",call.arguments);
      }
    });
  }
}

5. Plug-in test

The Flutter scaffolding automatically generates an example project when creating a plug-in. This project references the components we are developing by specifying the plug-in path, allowing us to fully test the plug-in before releasing it.

native_image_view:

path: ../

In addition to development and debugging, the example project is also a good example of plug-in usage. Compared with documentation, many developers prefer to directly look at the code implementation of the plug-in example. We demonstrate the use of network images in main.dart. Local images need to have corresponding files in the native project.

main.dart:


String url = "";
//String url = "localImage://xxx.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. Plug-in release

After the development of the plug-in is completed, it enters the release link. In order to facilitate subsequent maintenance and user feedback, we maintain the plug-in on github and fill in the warehouse address in the pubspec.yaml file of the plug-in

name: native_image_view

description: 该组件提供了一种方式,可以让flutter通过methodChannel调用原生的本地和网络图片的加载

version: 0.0.1

repository: 

Before committing to the warehouse, we need to run the dry-run command to check whether the component currently meets the release requirements.


flutter pub publish --dry-run

 The LICENSE file created for us by Flutter scaffolding is empty, and developers need to fill in the open source agreement of the plug-in by themselves. If you don't fill it in, dry-run will not prompt, but it will still report an error at the step of publishing in the warehouse.

The previous article taught how to create a LICENSE file, so I won't go into details.

Guess you like

Origin blog.csdn.net/RreamigOfGirls/article/details/130224297