How to create a custom plugin in Flutter with Pigeon
Tags: flutter, dart, android, ios
Published: 2022-07-24 19:11:35

# Introduction
In this tutorial, we will create a Flutter plugin which targets the Android and iOS platforms using the Pigeon package.

With Flutter being a UI framework, communicating with the native platform is not something we always need or use. And usually when we do need to do so, there are tons of packages out there that already do this for specific use cases.

But there are cases when the functionality the native platform has to offer is not yet available in a public package on [pub.dev](https://pub.dev). In these cases, it's up to us! We need to write some Dart code on the Flutter side to call the platform, as well as the native platform code to call the functionality we need (and that's once per platform your app supports!).

# Native platform communication
## Platform channels
One way to communicate with the native platform in Flutter is by using a `MethodChannel`. We covered how to build a plugin or call native platform code in Flutter using method channels in a [previous post](https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-to-call-native-platform-code).

While using `MethodChannel` is relatively straightforward, it can actually be quite time-consuming; when calling methods through the `MethodChannel`, you can only pass arguments of simple types such as `int`, `double`, `bool` or `String`, as well as maps of list with such types as values. You can see the full data types support [here](https://docs.flutter.dev/development/platform-integration/platform-channels?tab=type-mappings-java-tab#codec). If you need to pass a complex object as an argument, you would need to have logic to parse this to, let's say, a map of string keys to their values. In addition, you need to do the same for any data that is returned from the native platform.

We could work around having to introduce parsing logic by using a package such as [`json_serializable`](https://pub.dev/packages/json_serializable) to parse data to and from JSON to save ourselves some time. However, you'd need to make sure the native platforms are returning the data in the exact format you are expecting, and vice versa. Otherwise the parsing will fail.

## Pigeon
Pigeon is a code generator package which generates all the code necessary to communicate between Flutter and any host platform. All you have to do is define the API. This is convenient, because you don't have to worry about any parsing logic, and the communication is guaranteed to be type-safe.

As of July 10th 2022, Pigeon only supports Android and iOS, and generates Java and Objective-C code (Swift is experimental) respectively. The generated code is still accessible to Kotlin or Swift. There is also experimental Windows support with C++.

# Creating a plugin
In this post, we will create a simple plugin using Pigeon. What we will build will be identical to the (fake) app usage plugin we built [previously](https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-to-call-native-platform-code), so we can compare the results.

The plugin is simple; it should return a list of all apps and their usage, as well as support the ability to set time limits on specific apps. We won't actually implement this functionality on the native side as it's outside the scope of this tutorial, but rather return dummy data from the native side instead.

## The plugin template
To get started, we'll create a Flutter plugin using `flutter create` with the plugin template.
```sh
flutter create --org dev.dartling --template=plugin --platforms=android,ios app_usage_pigeon
```

This will generate the code for the plugin, as well as an example project that uses this plugin. By default, the generated Android code will be in Kotlin, and iOS in Swift, but you can specify either Java or Objective-C with the -a and -i flags respectively. (`-a java` and/or `-i objc`).

There is quite a bit of code included with the plugin template. We go into the Dart, Kotlin and Swift code into more detail in [this post](https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-to-call-native-platform-code), if you're curious. For the context of this tutorial, it's enough to know the following:

On the Dart side, there are three classes:
* `AppUsagePlatform` - the "interface"/API of the plugin, which implements `PlatformInterface`.
* `MethodChannelAppUsage` - an implementation of `AppUsagePlatform` using method channels.
* `AppUsage` - the class exposing the methods to be used by any apps which need to use our plugin.

The Kotlin code generated is `AppUsagePlugin.kt`, which uses method channels. It is defined in our `pubspec.yaml` as the plugin class for the Android platform, so we'll still be needing it, though we will make some changes to it later. The same applies to the Swift code, which includes `SwiftAppUsagePlugin.swift` as well as `AppUsagePlugin.h` and `AppUsagePlugin.m`.

In this tutorial, we will write an implementation of `AppUsagePlatform` which uses Pigeon rather than method channels. We can delete `MethodChannelAppUsage` as we won't be needing it.

Note: the new plugin template using `PlatformInterface` introduces quite a bit of code that you might not really need if you just want to call some native code in your app. If you wanted, you could still use Pigeon without creating a plugin or a separate package, but that's what we'll be doing in this tutorial.

# Using Pigeon
## Installing the `pigeon` package
Let's install the package:
```sh
flutter pub add --dev pigeon
```

Alternatively, add this to your `pubspec.yaml`:
```sh
dev_dependencies:
  pigeon: ^3.2.3
```

## Defining the App Usage API
The way Pigeon works is pretty simple; we define our API in a Dart class outside the `lib` folder (as Pigeon is a dev dependency). The API class should be an abstract class with the `@HostApi()` decorator, and its methods should have the `@async` decorator.

Let's define our App Usage API in a new directory named `pigeons`:
```dart
// pigeons/app_usage_api.dart
import 'package:pigeon/pigeon.dart';

enum State { success, error }

class StateResult {
  final State state;
  final String message;

  StateResult(this.state, this.message);
}

class UsedApp {
  final String id;
  final String name;
  final int minutesUsed;

  UsedApp(this.id, this.name, this.minutesUsed);
}

@HostApi()
abstract class AppUsageApi {
  @async
  String? getPlatformVersion();

  @async
  List<UsedApp> getApps();

  @async
  StateResult setAppTimeLimit(String appId, int minutesUsed);
}
```

## Caveats and limitations
Defining the API was relatively simple, but there are a few things to mention:

### Futures
We do not need to specify the return values as `Future`s, but in the generated code they will be. So `getPlatformVersion` will actually return a `Future<String?>` in the generated Dart code.

### No imports allowed
No imports other than `package:pigeon/pigeon.dart` are allowed. This means EVERY model class should be defined in this Pigeon API file.

### Supported data types
As mentioned before, only simple JSON-like values are supported. This means we can't use useful Dart types such as `DateTime` or `Duration`. Which means we might still need additional mapping logic to convert the Pigeon model to the model we want to use within the app. For `minutesUsed` in `UsedApp`, we'll need to manually create a `Duration` out of the minutes, though it would be nice to have this as a `Duration` in the first place.

### Enums aren't yet supported for primitive return types
We cannot return an enum from a method, but we can have an enum as a method parameter. We can still return enums, but only if we wrap them in a separate class, like below.

```dart
// Not valid, enums cannot be returned.
enum ResultState { success, error }

@HostApi()
abstract class AppUsageApi {
  @async
  ResultState getState();
}
```

```dart
// Valid, enums can be method parameters and fields of returned objects.
enum ResultState { success, error }

class ApiResult {
  final ResultState state;
  final String message;

  ApiResult(this.state, this.message);
}

@HostApi()
abstract class AppUsageApi {
  @async
  ApiResult getResult();

  @async
  void setState(ResultState state);
}
```

### Generics are supported, but can only be used with nullable types
We can still define them as non-nullable in our `HostApi` definition, e.g. `List<Something>`, but the generated Dart class will have `List<Something?>` instead.

## Generating the code
### Running the generator
After defining the API, we can generate code using `flutter pub run pigeon`. This command requires quite a few arguments:
```sh
flutter pub run pigeon \
  --input pigeons/app_usage_api.dart \
  --dart_out lib/app_usage_api.dart \
  --java_package "dev.dartling.app_usage" \
  --java_out android/src/main/java/dev/dartling/app_usage/AppUsage.java \
  --experimental_swift_out ios/Classes/AppUsage.swift
```

We will store this in a `pigeon.sh` file, just so it's easy to find and run in the future.

We're going with Swift which has experimental support for now rather than Objective-C, but for Objective-C we can simply drop the `experimental_swift_out` argument in favor of these three:

```sh
  --objc_header_out ios/Classes/AppUsageApi.h \
  --objc_source_out ios/Classes/AppUsageApi.m \
  --objc_prefix FLT
```

#### Dart
The `input` argument should be the file we defined the API in, and `dart_out` should be in our `lib` folder, as it's the code we'll actually be using in our app.

#### Java
`java_package` is the full package name, in this case `dev.dartling.app_usage` and `java_out` is the path to the Java file that will be generated.

Note: Make sure the generated Java class name does NOT match the name of the Pigeon `HostApi`. In our case, the generated Java class will be `AppUsage`, and will include a nested public `AppUsageApi` interface, taken from the `HostApi` class name defined in Dart. If we used the same names (which is what I did initially!), compilation will fail due to duplicate names.

Note: if your plugin template uses Kotlin, like we did in this one, you will need to create the `java/dev/dartling/app_usage` directory manually under `src/main`, as only `kotlin/dev/dartling/app_usage` was generated as part of the plugin template.

#### Swift
`experimental_swift_out` is the path to the Swift file that will be generated.

#### Objective-C
The `objc_header_out` and `objc_source_out` arguments determine the generated files on the Objective-C side, and the `objc_prefix` is optional and determines the prefix of the generated class names.

### Understanding the generated code
The code generated by Pigeon after running `flutter pub run pigeon` is not something we should really have to look at often. All we need to know is that the Java class will have an `AppUsageApi` interface which our implementation class should implement; this can be both in either Java or Kotlin. In Objective-C, there will be a `FLTAppUsageApi` protocol equivalent (notice the `FLT` prefix which is an argument when running the generator), and in Swift an `AppUsageApi` protocol.

## Native platform implementation
### Android
We have our interface, now all we need to do is have an implementation for it. To keep things simple, we will simply the existing `AppUsagePlugin` Kotlin class to implement `AppUsageApi`, in addition to the existing `FlutterPlugin` interface.

Here is the full class:
```kotlin
// AppUsagePlugin.kt
class AppUsagePlugin : FlutterPlugin, AppUsageApi {
    val usedApps: MutableList<UsedApp> = mutableListOf(
        usedApp("com.reddit.app", "Reddit", 75),
        usedApp("dev.hashnode.app", "Hashnode", 37),
        usedApp("link.timelog.app", "Timelog", 25),
    )

    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        AppUsageApi.setup(flutterPluginBinding.binaryMessenger, this)
    }

    override fun getPlatformVersion(result: Result<String>?) {
        result?.success("Android ${android.os.Build.VERSION.RELEASE}")
    }

    override fun getApps(result: Result<MutableList<UsedApp>>?) {
        result?.success(usedApps);
    }

    override fun setAppTimeLimit(
        appId: String,
        durationInMinutes: Long,
        result: Result<TimeLimitResult>?
    ) {
        val stateResult = TimeLimitResult.Builder()
            .setState(ResultState.success)
            .setMessage("Timer of $durationInMinutes minutes set for app ID $appId")
            .build()
        result?.success(stateResult)
    }

    private fun usedApp(id: String, name: String, minutesUsed: Long): UsedApp {
        return UsedApp.Builder()
            .setId(id)
            .setName(name)
            .setMinutesUsed(minutesUsed)
            .build();
    }

    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        AppUsageApi.setup(binding.binaryMessenger, null)
    }
}
```

The `onAttachedToEngine` and `onDetachedFromEngine` are from `FlutterPlugin`. Previously they set things up to work for the method channel implementation. Now, we are calling the `AppUsageApi#setup` method to get it to work with Pigeon's generated code.

The other three functions we override are from the `AppUsageApi` interface. These are actually `void` functions, and we "return" the results by making use of `result`, which was actually generated as nullable. To return, we simply use `result?.success(...)`, and in case we want to throw an error, we can use `result?.error(...)` and pass a `Throwable`; this will be wrapped into a `PlatformException` on the Dart side.

### iOS
Very similarly to Android, we will make the existing `SwiftAppUsagePlugin` implement the `AppUsageApi` protocol in addition to being a `FlutterPlugin`. Rather than `result`, we have `completion` which we call by passing the result as the argument. We also make some changes to `register` to use the static `AppUsageApiSetup#setUp` function to set things up with the generated file.

```swift
public class SwiftAppUsagePlugin: NSObject, FlutterPlugin, AppUsageApi {
    var usedApps = [
        UsedApp(id: "com.reddit.app", name: "Reddit", minutesUsed: 75),
        UsedApp(id: "dev.hashnode.app", name: "Hashnode", minutesUsed:37),
        UsedApp(id: "link.timelog.app", name: "Timelog", minutesUsed: 25)
    ]

    public static func register(with registrar: FlutterPluginRegistrar) {
        let messenger : FlutterBinaryMessenger = registrar.messenger()
        let api : AppUsageApi & NSObjectProtocol = SwiftAppUsagePlugin.init()
        AppUsageApiSetup.setUp(binaryMessenger: messenger, api: api)
    }

    func getPlatformVersion(completion: @escaping (String?) -> Void) {
        completion("iOS " + UIDevice.current.systemVersion)
    }

    func getApps(completion: @escaping ([UsedApp]) -> Void) {
        completion(usedApps)
    }

    func setAppTimeLimit(appId: String, durationInMinutes: Int32, completion: @escaping (TimeLimitResult) -> Void) {
        completion(TimeLimitResult(state: ResultState.success, message: "Timer of \(durationInMinutes) minutes set for app ID \(appId)"))
    }
}
```

Note: I've faced some weird issues with the iOS build sometimes not succeeding due to `AppUsageApi` not being found in the scope. If you run into the same issue, the quick hacky way is to copy everything in the generated `AppUsage.swift` into the existing `SwiftAppUsagePlugin.swift` file. Then it should work! If you figure out how/why this happens and how to fix it, please let me know in the comments!

## Using the plugin
We now have everything in place. The native platform implementations are done, and the `AppUsageApi` Dart class can be used to communicate with the native platforms.

All we have to do is create an instance of `AppUsageApi` and invoke its method... but wait, we're building a plugin! We should not use `AppUsageApi` directly (though we could!). Remember the `MethodChannelAppUsage` Dart class we deleted a while ago? We need to introduce an alternative that will use Pigeon instead of method channels.

Firstly, let's add make sure all methods that are part of our Pigeon `HostApi` are also defined in our `AppUsagePlatform`.

```dart
abstract class AppUsagePlatform extends PlatformInterface {
  ...

  Future<String?> getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }

  Future<List<UsedApp>> get apps async {
    throw UnimplementedError('apps has not been implemented.');
  }

  Future<TimeLimitResult> setAppTimeLimit(String appId, Duration duration) async {
    throw UnimplementedError('setAppTimeLimit() has not been implemented.');
  }
}
```

Now, our `AppUsagePlatform` implementation with Pigeon is very simple. We're simply going to invoke the methods of `AppUsageApi`, which was generated by Pigeon.
```dart
// app_usage_pigeon.dart
/// An implementation of [AppUsagePlatform] that uses Pigeon.
class PigeonAppUsage extends AppUsagePlatform {
  final AppUsageApi _api = AppUsageApi();

  @override
  Future<String?> getPlatformVersion() {
    return _api.getPlatformVersion();
  }

  @override
  Future<List<UsedApp>> get apps {
    return _api
        .getApps()
        .then((apps) => apps.where((e) => e != null).map((e) => e!).toList());
  }

  @override
  Future<TimeLimitResult> setAppTimeLimit(String appId, Duration duration) async {
    return _api.setAppTimeLimit(appId, duration.inMinutes);
  }
}
```

Note that in `apps` we filter null values and use the `!` operator, as `AppUsageApi#getApps()` returns `List<UsedApp?>`, due to Pigeon's current limitations.

Lastly, `AppUsage`, our main plugin class, should also be updated. All it does is delegate method calls to `AppUsagePlatform.instance`.

```dart
class AppUsage {
  Future<String?> getPlatformVersion() {
    return AppUsagePlatform.instance.getPlatformVersion();
  }

  Future<List<UsedApp>> get apps {
    return AppUsagePlatform.instance.apps;
  }

  Future<TimeLimitResult> setAppTimeLimit(String appId, Duration duration) {
    return AppUsagePlatform.instance.setAppTimeLimit(appId, duration);
  }
}
```

And let's not forget, that `AppUsagePlatform.instance` should now return an instance of `PigeonAppUsage` rather than `MethodChannelAppUsage`:

```dart
abstract class AppUsagePlatform extends PlatformInterface {
  ...

  static AppUsagePlatform _instance = PigeonAppUsage();

  /// The default instance of [AppUsagePlatform] to use.
  ///
  /// Defaults to [PigeonAppUsage].
  static AppUsagePlatform get instance => _instance;

  ...
}
```

I won't share snippets of the UI code and widgets, but you can take look at these [here](https://github.com/dartling/app_usage_pigeon/blob/main/example/lib/main.dart). Using the plugin in any app is simple; we initialize an instance of `AppUsage` and call its methods we need them.

Here is the final result:
![android.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1648588947386/x81wxSmPZ.png)
![ios.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1648588469952/DCIGFo5NI.png)

## Comparing Pigeon and method channels
We built an almost identical plugin and example app in a [previous article](https://dartling.dev/how-to-create-a-custom-plugin-in-flutter-to-call-native-platform-code), so we can compare Pigeon with using method channels.

Overall, Pigeon is definitely an improvement. We only have to define our API and models once; the generated Android/iOS will include these models for us.

We also don't have to worry about serializing data we want to pass to the platform side or deserialize data coming from the platform side, and the opposite for the platform side; we won't have to worry about deserializing data coming from the Dart side and serializing data we return to the Dart side.

Thanks to the two points above, we needed significantly less lines of code to write a plugin using Pigeon rather than method channels.

## Wrapping up
In this tutorial, we introduced Pigeon as a way to simplify native platform communication, and created a custom Flutter plugin with Android and iOS implementations to call (fake) native functionality, using Pigeon rather than method channels.

You can find the full source code [here](https://github.com/dartling/app_usage_pigeon).

If you found this helpful and would like to be notified of any future tutorials, please sign up with your email [here](https://www.getrevue.co/profile/dartling).