> For the complete documentation index, see [llms.txt](https://wong-coupon.gitbook.io/flutter/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wong-coupon.gitbook.io/flutter/architecture-code/plugin.md).

# Plugin

In many scenarios, accessing native operating system APIs is necessary to take advantage of functionalities that are not available in Flutter. To achieve this, we can create a plugin to communicate with native code using `MethodChannel`. This plugin allows Flutter to send requests to native code and receive responses from iOS and Android.

Of course, you can implement this directly in the native code of your project. However, separating it into a plugin makes maintaining and fixing the application when issues arise significantly easier.

Below, I will explain how I create a plugin for my applications.

## Overview of Creating a Plugin

### Create a Flutter Plugin Library

Run the following command to create a new plugin:

```bash
flutter create --template=plugin your_plugin
```

Ensure that your plugin avoids, or minimizes as much as possible, the use of third-party libraries. This approach helps to maintain simplicity and reduces external dependencies.

### Plugin Structure

The folder structure of a Flutter plugin is as follows:

```
|--lib
    |-- src
        |-- message_entity.dart
        |-- your_plugin_function.dart
    |-- your_plugin.dart
```

In the file `src/your_plugin_function.dart`, define the communication between Dart and native code. Ensure that all the necessary files are exported in `your_plugin.dart`. This way, you can simply import the plugin library once when using it in your project, improving maintainability and clarity.

## Create a Method for the Plugin

To call native code, you need to use a channel. Through this channel, you can invoke native functions and receive results.

### Set Up Method for Dart

```dart
part of YourPlugin;

class YourPlugin {

  /// STEP I | setup method
  static const String nameMethod = 'YourPlugin';
  static const MethodChannel _methods = MethodChannel('$nameMethod/methods');
  final StreamController<MethodCall> _methodStream = StreamController.broadcast();

  // 1.1: Create stream method
  bool _initialized = false;
    Future<dynamic> _initFlutterBluePlus() async {
    if (_initialized) {
      return;
    }
      _initialized = true;
      _methods.setMethodCallHandler((call) async {
        _updateMethodStream(call);
    });
  }

  // 1.2: Invoke a platform method
  Future<dynamic> _invokeMethod(String method, [dynamic arguments]) async {
    dynamic out;
    print("_invokeMethod | $method | $arguments");
    try {
      _initFlutterBluePlus();
      out = await _methods.invokeMethod(method, arguments);
    } catch (e) {
      rethrow;
    }
    return out;
  }

  // 1.3: Update Stream _methodStream on Flutter
  void _updateMethodStream(MethodCall methodCall) {
    _methodStream.add(methodCall);
  }
}
```

### Create Native Code for Android

Create `TalkingWithFlutter.java`: Handles data transmission between Flutter and native Android through `MethodChannel`.

```java
package com.example.your_plugin;

import android.os.Handler;
import android.os.Looper;
import java.util.HashMap;
import io.flutter.plugin.common.MethodChannel;

public class TalkingWithFlutter {

    TalkingWithFlutter(Utils utils) {
        this.utils = utils;
    }
    public MethodChannel methodChannel;

    public void remove() {
        methodChannel.setMethodCallHandler(null);
        methodChannel = null;
    }

    private final Utils utils;

    // Send data back to Flutter plugin
    public void invokeMethodUIThread(final String method, HashMap<String, Object> data) {
        new Handler(Looper.getMainLooper()).post(() -> {
            // Could already be torn down at this moment
            if (methodChannel != null) {
                methodChannel.invokeMethod(method, data);
            } else {
                utils.log("invokeMethodUIThread: tried to call method on closed channel: " + method);
            }
        });
    }

    public static final int WORKING = 0;
    public static final int FREE = 1;

    public void onListenStateChanged(int state) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("state", state);
        invokeMethodUIThread("OnListenStateChanged", map);
    }
}
```

#### Translation:

This function is used to send data from native code to Flutter. It can be very useful for listening to data, such as over Bluetooth.

```java
public void onListenStateChanged(int state) {
    HashMap<String, Object> map = new HashMap<>();
    map.put("state", state);
    invokeMethodUIThread("OnListenStateChanged", map);
}
```

***

In the `YourPlugin.java` file: The main plugin handles methods called from Flutter and communicates with `TalkingWithFlutter`.

```java
package com.example.your_plugin;

public class YourPlugin implements FlutterPlugin, MethodCallHandler, PluginRegistry.ActivityResultListener {

  /**
   * Step 1: Setup method
   */
  private Context context;
  private ActivityPluginBinding activityBinding;
  private final Utils utils = new Utils();
  private final TalkingWithFlutter talkingWithFlutter = new TalkingWithFlutter(utils);

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    context = flutterPluginBinding.getApplicationContext();

    talkingWithFlutter.methodChannel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "{{name.snakeCase()}}/methods");
    talkingWithFlutter.methodChannel.setMethodCallHandler(this);
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    context = null;
    talkingWithFlutter.remove();
  }

  @Override
  public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    return false;
  }

  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    switch (call.method) {
      case "exTalk": {
        exTalk(call, result);
        break;
      }
      default:
        result.notImplemented();
        break;
    }
  }

  private void exTalk(@NonNull MethodCall call, @NonNull Result result) {
    HashMap<String, Object> data = call.arguments();
    int copies = (int) data.get("ex_message");
    utils.log(copies + "");

    talkingWithFlutter.onListenStateChanged(TalkingWithFlutter.FREE);

    result.success(TalkingWithFlutter.WORKING);
  }
}
```

This code processes the data sent from Flutter, executes it, and returns the result to Flutter.

```java
private void exTalk(@NonNull MethodCall call, @NonNull Result result) {
    HashMap<String, Object> data = call.arguments();
    int copies = (int) data.get("ex_message");
    utils.log(copies + "");

    talkingWithFlutter.onListenStateChanged(TalkingWithFlutter.FREE);

    result.success(TalkingWithFlutter.WORKING);
}
```

***

Create a `Utils.java` file in `android/src/main/kotlin/com/example/your_plugin`:

```java
package com.example.your_plugin;

import android.util.Log;

public class Utils {
    private static final String TAG = "[YourPlugin-Android]";

    void log(String message) {
        Log.d(TAG, message);
    }
}
```

### Create Native Code for iOS

Similar to Android, create a `Utils.swift` file in the `ios/Classes` directory.

Then, create `TalkingWithFlutter.swift`: This handles data transmission between Flutter and native iOS through `MethodChannel`.

```swift
import Flutter
import Foundation
import CoreBluetooth

class TalkingWithFlutter : NSObject{
    
    // Shared instance
    static let shared = TalkingWithFlutter()
    
    override private init() {
        super.init()
    }
    
    deinit {
    }
    
    init(methodChannel: FlutterMethodChannel? = nil) {
        self.methodChannel = methodChannel
    }
    
    var methodChannel: FlutterMethodChannel?

    let WORKING = 0
    let FREE = 1
    
    func onListenStateChanged(state: Int) {
        let response = [
            "state": state,
        ]
        if methodChannel != nil {
            methodChannel!.invokeMethod("OnListenStateChanged", arguments: response)
            Utils.log("OnListenStateChanged \(response)")
        }
    }  
}
```

**Explanation of Functionality:**

This function is used to send data from native code to Flutter. It is particularly useful when listening to data, such as via Bluetooth.

```swift
func onListenStateChanged(state: Int) {
    let response = [
        "state": state,
    ]
    if methodChannel != nil {
        methodChannel!.invokeMethod("OnListenStateChanged", arguments: response)
        Utils.log("OnListenStateChanged \(response)")
    }
}
```

***

In the `YourPlugin.swift` file: This main plugin processes the methods called from Flutter and communicates with `TalkingWithFlutter`.

```swift
import Flutter
import UIKit

public class YourPlugin: NSObject, FlutterPlugin {

    // MARK: Step 1: Setup method
    var talking = TalkingWithFlutter.shared

    public static func register(with registrar: FlutterPluginRegistrar) {
        let instance = YourPlugin()
        instance.talking.methodChannel = FlutterMethodChannel.init(name: "{{name.snakeCase()}}/methods", binaryMessenger: registrar.messenger())

        registrar.addMethodCallDelegate(instance, channel: instance.talking.methodChannel!)
    }

    // FlutterResult returns a result
    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        Utils.log(call.method)
        switch call.method {

            case "exTalk":
                exTalk(call: call, result: result)

            default:
                result(FlutterMethodNotImplemented)
        }
    }

    func exTalk(call: FlutterMethodCall, result: @escaping FlutterResult) {
        let args = call.arguments as? NSDictionary
        let copies = args?["ex_message"] as? Int
        Utils.log("copies \(copies) ")

        talking.onListenStateChanged(state: talking.FREE)

        result(NSNumber(value: talking.WORKING))
    }
}
```

**Explanation:**

This code receives data sent from Flutter, processes it, and sends the result back to Flutter.

```swift
func exTalk(call: FlutterMethodCall, result: @escaping FlutterResult) {
    let args = call.arguments as? NSDictionary
    let copies = args?["ex_message"] as? Int
    Utils.log("copies \(copies) ")

    talking.onListenStateChanged(state: talking.FREE)

    result(NSNumber(value: talking.WORKING))
}
```

## Data Transmission

### Active Data Transmission, Commanding Native Code

There are two main ways to actively communicate with native code. The first and most common method involves Flutter actively sending data to the native layer, which then processes it and returns a result.

In this example, the communication with the native code is facilitated using a method called `'exTalk'`.

```dart
/// STEP II | Talk (and get) to native
/// 2.1 | Type 1 Flutter -> Native -> Flutter
Future<int> exTalk({
    int exMessage = 1,
  }) async {
    final Map<dynamic, dynamic> data = {};
    data['ex_message'] = exMessage;
    return await _invokeMethod('exTalk', data);
}
```

This method ensures smooth communication between Flutter and the native layer while enabling result processing.

### Passive Data Transmission: Listening to Native Code

In certain cases, we need to listen for results from the native layer and send them back to Flutter. For example, if a command is sent from a printer to a device, the native layer will transmit it to Flutter to decode the command.

This approach is referred to as **passive communication**. We use a stream to listen for a method called `"OnListenStateChanged"`:

```dart
/// 2.2 | Type 2 Native -> Flutter: Listen native
Stream<ListenState> get listenState async* {
  yield* _methodStream.stream
      .where((m) => m.method == "OnListenStateChanged")
      .map((m) => m.arguments)
      .map((args) => ListenState.fromMap(args));
}
```

#### Communication Using JSON

Because native and Flutter use different data types, JSON is employed for communication. The native layer converts its data into JSON format and sends it to Flutter. Flutter then interprets the JSON and converts it into the desired data type.

#### Decoding Data from Native to Flutter

In the file `lib/src/message_entity.dart`, let's assume there is a `ListenStateEnum` that manages states in Flutter. It listens for JSON data with the key `'state'`.

For instance, if the native layer sends `{"state": 0}`, Flutter will receive it and decode it into `ListenStateEnum.working`.

```dart
part of YourPlugin;

class ListenState {
  ListenStateEnum state;

  ListenState({
    required this.state
  });

  Map<dynamic, dynamic> toMap() {
    final Map<dynamic, dynamic> data = {};
    data['state'] = state;
    return data;
  }

  factory ListenState.fromMap(Map<dynamic, dynamic> json) {
    return ListenState(
      state: ListenStateEnum.getEnum(json['state']),
    );
  }
}

enum ListenStateEnum {
  working(0),
  free(1);

  const ListenStateEnum(this.code);
  final int code;
  
  static ListenStateEnum getEnum(int code) {
    try {
      return ListenStateEnum.values.firstWhere((element) => element.code == code);
    } catch (e) {
      return ListenStateEnum.working;
    }
  }
}
```

## Practical Implementation

### Create Example Code and Test Plugin Features in Flutter

```dart
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:your_plugin/your_plugin.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

  final _yourPlugin = YourPlugin();

  ListenStateEnum _listenState = ListenStateEnum.working;
  late StreamSubscription<ListenState> _listenStateSubscription;

  @override
  void initState() {
    super.initState();
    _listenStateSubscription = _yourPlugin.listenState.listen((value) {
      _listenState = value.state;
      if (mounted) {
        setState(() {});
      }
    });
  }

  @override
  void dispose() {
    _listenStateSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin Example App | YourPlugin'),
        ),
        body: Center(
          child: SingleChildScrollView(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Text("TYPE 1: Call Native and Get Data"),
                TextButton(
                    onPressed: () async {
                      final a = await _yourPlugin.exTalk(exMessage: 3);
                      print(a);
                    },
                    child: const Text("exTalk")),

                const Text("TYPE 2: Listen for Data from Native"),
                StreamBuilder(
                  stream: _yourPlugin.listenState,
                  builder: (context, s) {
                    return Text("Way 1 - listenState: ${s.data?.state.toString()}");
                  }
                ),
                Text("Way 2 - listenState: $_listenState"),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
```

#### Active Communication

I will send a command from Flutter to the native layer, which will return a response immediately. I will then display the result on the screen as soon as it's available.

```dart
final a = await _yourPlugin.exTalk(exMessage: 3);
print(a);
```

#### 4.3. Passive Communication: Listening to Native Code

My idea is to use a `Stream` to listen for data. Below, I present two approaches to retrieve data from a `Stream`:

1. **Using StreamBuilder for Displaying Results**

The simplest way to display results is by utilizing a `StreamBuilder`. It renders the output directly on the screen.

```dart
StreamBuilder(
    stream: _yourPlugin.listenState,
    builder: (context, s) {
      return Text("Way 1 - listenState: ${s.data?.state.toString()}");
    }
),
```

2. **Using StreamSubscription for Complex Tasks**

If you don't need to display the results but instead wish to handle more complex operations, you should use a `StreamSubscription`. Below is an example of declaring and implementing it within a `StatefulWidget`:

```dart
ListenStateEnum _listenState = ListenStateEnum.working;
late StreamSubscription<ListenState> _listenStateSubscription;

@override
void initState() {
  super.initState();
  _listenStateSubscription = _yourPlugin.listenState.listen((value) {
    _listenState = value.state;
    if (mounted) {
      setState(() {});
    }
  });
}

@override
void dispose() {
  _listenStateSubscription.cancel();
  super.dispose();
}
```

To display the result, the implementation is as simple as:

```dart
Text("Way 2 - listenState: $_listenState"),
```

## Faster Setup with Mason

If you're unfamiliar with Mason, refer to my article on Mason.

{% content-ref url="/pages/6Yh2LXIXAdzRX2g1isw5" %}
[Mason](/flutter/easy-code/mason.md)
{% endcontent-ref %}

Above, I have detailed the process of setting up a plugin I use and the two methods of communication between native and Flutter. As you can see, the process involves quite a few steps, doesn't it?

If we had to repeat all these steps every time we created a new plugin, it would take a lot of time. My solution is to use Mason. I create template code files, so whenever I need to build a new plugin, I only have to use Mason, and all the required code templates are automatically included in the project.

You can check out my `dr_plugin` brick at <https://brickhub.dev/bricks/dr_plugin>.

[Buy Me a Coffee](https://buymeacoffee.com/ducmng12g) | [Support Me on Ko-fi](https://ko-fi.com/I2I81AEJG8)
