In the development of the Flutter project, we needed to create various plug-ins according to our business requirements. Here is a document about the process of creating and using the Flutter plug-ins.

The official documentation

The following points are mainly recorded here:

  • Plug-in creation
  • Get the Context in Android
  • Dart calls plug-in methods and passes parameters
  • The plug-in calls Dart methods and passes parameters
  • The plug-in listens for the Activity’s lifecycle and common callback methods
  • Write the plug-in as a Delegate
  • Dependencies on plug-ins (pub, local, Git)
  • Plug-in upload and notes

Plug-in creation

There are two ways to create plug-ins. One is to use an IDE (Android Studio or Idea). The other is to create it by command.

Use the IDE to create plug-ins

Select File -> New -> New Flutter Project from the menu bar to display the following screen

Select the Flutter Plugin and go all the way to Next.

Use commands to create plug-ins

flutter create --org com.example --template=plugin plugin_name
Copy the code

Com.example is part of the plug-in package name, and plugin_name is the plug-in name. The full package of the plug-in is called com.example.plugin_name

Directory structure for plug-ins

Once created using either of the above two methods, the directory structure of the plug-in is as follows:

The main directories in the figure are Android, Example, ios and lib:

  • The Android directory is a complete Android project to develop android side of the plug-in features
  • The example directory is used to test Android or IOS plugins
  • The ios directory is a complete ios project for developing ios plugins
  • Files in the lib directory are responsible for interacting with Android or IOS

Get the Context in Android

When we created plug-ins, Android project will have a better file generated, this is FlutterPluginDemoPlugin. Java, is as follows:

/** FlutterPluginDemoPlugin */
public class FlutterPluginDemoPlugin implements MethodCallHandler {
  /** Plugin registration.*/
  public static void registerWith(Registrar registrar) {
    final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter_plugin_demo");
    channel.setMethodCallHandler(new FlutterPluginDemoPlugin());
  }

  @Override
  public void onMethodCall(MethodCall call, Result result) {
    // Omit some code}}Copy the code

This class has a MethodChannel corresponding to the Dart layer.

At this point, if we add a pop-up Toast method. Toast requires a Context argument, but the class does not provide a method like this.getContext() to do so. The Registrar class is needed to obtain the Context. As follows:

public class FlutterPluginDemoPlugin implements MethodCallHandler {
    // Context Context
    private final Context context;

    public FlutterPluginDemoPlugin(Registrar registrar) {
        this.context = registrar.context();
    }

    /** * Plugin registration. */
    public static void registerWith(Registrar registrar) {
        final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter_plugin_demo");
        channel.setMethodCallHandler(new FlutterPluginDemoPlugin(registrar));
    }

    @Override
    public void onMethodCall(MethodCall call, Result result) {
        if (call.method.equals("getPlatformVersion")) {
            result.success("Android " + android.os.Build.VERSION.RELEASE);
        } else if (call.method.equals("showToast")) {
            / / pop up the Toast
            Toast.makeText(context, "Toast from Android", Toast.LENGTH_SHORT).show();
        } else{ result.notImplemented(); }}}Copy the code

Dart file to trigger the pop-up Toast:

class FlutterPluginDemo {
  static const MethodChannel _channel =
      const MethodChannel('flutter_plugin_demo');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
  
  / / / pop up the Toast
  static Future<void> showToast() async {
    await _channel.invokeMethod("showToast"); }}Copy the code

Then call the example project:

floatingActionButton: FloatingActionButton(
  onPressed: () async {
    /// Call the plugin's Toast function
    await FlutterPluginDemo.showToast();
  },
  child: Icon(Icons.add),
),
Copy the code

Dart passes parameters when calling native methods

Change the showToast method above to accept a single argument:

/// [message] Toast contents
static Future<void> showToast({String message}) async {
    await _channel.invokeMethod("showToast", message);
}
Copy the code

In the Java layer you need to receive this parameter:

String message = String message = call.arguments();
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
Copy the code

If you need to pass multiple parameters, you can pass a Map as follows:

/// pass parameters of type map
static Future<void> showToast() async {
    Map<String.String> params = Map<String.String> (); params['name'] = 'Lili';
    params['age'] = '20';
    params['country'] = 'China';
    await _channel.invokeMethod("showToast", params);
}
Copy the code

Java layer receives:

Map<String, String> params = call.arguments();
Toast.makeText(context, params.get("name"), Toast.LENGTH_SHORT).show();
Copy the code

Notifications are sent to the Dart layer natively

Here we use EventChannel to let native send notifications to the Dart layer, using EventChannel as follows:

  • The Dart layer defines an EventChannel
  • The Dart layer listens on that EventChannel to receive events on that EventChannel
  • The native side defines an EventChannel
  • The native end sends messages to the Dart layer

The Dart layer defines EventChannel

static const EventChannel _eventChannel = EventChannel("flutter_plugin_event");
Copy the code

The Dart layer listens on this EventChannel

// Data is sent from the native end
_eventChannel.receiveBroadcastStream().listen((data) {
  //streamController.sink.add(data);
});
Copy the code

The native side defines the EventChannel

In the native end, you need to define an EventChannel, and then you need to set up a StreamHandler for it. In the StreamHandler onListen method, there is an eventChannel. EventSink. This parameter can be used to send messages to the Dart layer.

privateEventChannel.EventSink eventSink; .final EventChannel eventChannel = new EventChannel(registrar.messenger(), "flutter_plugin_event");
eventChannel.setStreamHandler(new EventChannel.StreamHandler() {
    @Override
    public void onListen(Object o, EventChannel.EventSink eventSink) {
        FlutterPluginDemoPlugin.this.eventSink = eventSink;
    }

    @Override
    public void onCancel(Object o) {
        FlutterPluginDemoPlugin.this.eventSink = null; }});Copy the code

The native end sends messages to the Dart layer

// Notify the Dart layer
if (null! = eventSink) { eventSink.success(Dart call to original method successful);
}
Copy the code

Callbacks to plugins that listen for the Activity’s usual lifecycle methods

Lifecycle method callback

Sometimes we need to do something in an Activity’s lifecycle method, such as adding code to the Activity’s onResume() and onPause() methods when counting friends.

To monitor the onCreate (), onStart (), onResume () method of correction, need to use the Application. The ActivityLifecycleCallbacks this interface.

First to write a class LifeCycleCallbacks to implement the Application. The ActivityLifecycleCallbacks this interface, then it is registered to the context of the Application.

Application. ActivityLifecycleCallbacks interface provided by life cycle method is as follows:

public interface ActivityLifecycleCallbacks {
    void onActivityCreated(Activity activity, Bundle savedInstanceState);
    void onActivityStarted(Activity activity);
    void onActivityResumed(Activity activity);
    void onActivityPaused(Activity activity);
    void onActivityStopped(Activity activity);
    void onActivitySaveInstanceState(Activity activity, Bundle outState);
    void onActivityDestroyed(Activity activity);
}
Copy the code

So we just need to write a class to implement it. Then write the corresponding code in the corresponding method.

Then came the registration:

public FlutterPluginDemoPlugin(Registrar registrar) {... .// Register to declare the listener of the periodic method
    ((Application) registrar.context())
        .registerActivityLifecycleCallbacks(new LifeCycleCallbacks());
}
Copy the code

Finally, unregister in the onActivityDestroyed lifecycle method

class LifeCycleCallbacks implements Application.ActivityLifecycleCallbacks {... .@Override
    public void onActivityDestroyed(Activity activity) {
        if (activity == registrar.activity()) {
            ((Application) registrar.context()).unregisterActivityLifecycleCallbacks(this); }}}Copy the code

Permission listening callback

public FlutterPluginDemoPlugin(Registrar registrar) {... .// Permission listening callback
        registrar.addRequestPermissionsResultListener(new PluginRegistry.RequestPermissionsResultListener() {
        @Override
        public boolean onRequestPermissionsResult(int i, String[] strings, int[] ints) {
            return false; }}); }Copy the code

StartActivityForResult callback

public FlutterPluginDemoPlugin(Registrar registrar) {... ./ / startActivityForResult callback
    registrar.addActivityResultListener(new PluginRegistry.ActivityResultListener() {
        @Override
        public boolean onActivityResult(int requestCode, int responseCode, Intent intent) {
            return false; }}); }Copy the code

Write a Delegate class to handle the business logic

In the previous section, we handled callback methods for Activity declaration cycles, callback methods for permission requests, and callback methods for Activity jumps in the FlutterPluginDemoPlugin class. At this point, the code for the FlutterPluginDemoPlugin class becomes very large.

We can write a class to help our plug-in classes handle these things. Here we write a PluginDelegate class to do this:

public class PluginDelegate implements
        Application.ActivityLifecycleCallbacks.PluginRegistry.RequestPermissionsResultListener.PluginRegistry.ActivityResultListener {

    private final Context context;
    private final Application application;

    public PluginDelegate(PluginRegistry.Registrar registrar) {
        this.context = registrar.context();
        this.application = (Application) context;
    }

    public void methodA(MethodCall call, MethodChannel.Result result){
        // do something...
    }

    public void methodB(MethodCall call, MethodChannel.Result result){
        // do something...}... . .@Override
    public void onActivityDestroyed(Activity activity) {
        application.unregisterActivityLifecycleCallbacks(this);
    }

    @Override
    public boolean onRequestPermissionsResult(int i, String[] strings, int[] ints) {
        return false;
    }

    @Override
    public boolean onActivityResult(int i, int i1, Intent intent) {
        return false; }}Copy the code

As you can see, the PluginDelegate class implements the interfaces for the three callbacks we dealt with in the previous section, so we can do this in the FlutterPluginDemoPlugin class:

public class FlutterPluginDemoPlugin implements MethodCallHandler {... .private final PluginDelegate delegate;
    
    // constructor
    public FlutterPluginDemoPlugin(Registrar registrar, PluginDelegate delegate) {...this.delegate = delegate; .// Declare periodic callback
        ((Application) context).registerActivityLifecycleCallbacks(delegate);

        // Permission declaration callback
        registrar.addRequestPermissionsResultListener(delegate);

        // Page jump callback
        registrar.addActivityResultListener(delegate);
    }
    
    /** * Plugin registration. */
    public static void registerWith(Registrar registrar) {
        final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter_plugin_demo");
        // Initialize PluginDelegate
        final PluginDelegate delegate = new PluginDelegate(registrar);
        channel.setMethodCallHandler(new FlutterPluginDemoPlugin(registrar, delegate));
    }
    
    @Override
    public void onMethodCall(MethodCall call, Result result) {
        // Invoke the proxy class method demo
        if (call.method.equals("methodA")) {
            delegate.methodA(call, result);
        }else if(call.method.equals("methodB")){ delegate.methodB(call, result); }}}Copy the code

How plug-ins are dependent

The official documentation

There are three ways to rely on plug-ins

  • The pub rely on
  • Git is dependent on
  • Relying on local

The pub rely on

This is the most common way to rely directly on projects in pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  Add toast dependencies
  toast: ^ 0.1.5
Copy the code

Git is dependent on

Many times, a plugin on pub does not fully meet our actual business needs, such as UI or some logic problems, so we can download the source code and modify it according to our business needs. These changes are usually uploaded to the company’s private repository (GitHub or GitLab), which is then relied on for the project

dependencies:
 toast:
    git:
      url: http://xxx/toast.git
Copy the code

It is also possible to rely on the repository to specify code on the branch, such as the remote dev branch

dependencies:
  toast:
     git:
      ref: dev
      url: http://xxx/toast.git
Copy the code

Relying on local

When you need to test a local plug-in in a project, you can use local dependencies to rely on the plug-in

dependencies:
    toast:
        path: user/xxx/toast/
Copy the code

Plugin upload

Here is the upload to pub.dev

Use the following command to check for problems with the plug-in before uploading:

flutter packages pub publish --dry-run
Copy the code

Also need to do is the need to clean the plug-in before uploading, avoid plug-in too large cannot upload

flutter clean
Copy the code

Run the following command to upload the plug-in

flutter packages pub publish
Copy the code

Last github address