Flutter is a great framework for developing cross-platform applications, enabling developers to implement expressive and beautifully-designed user interfaces. A lot of developers may find that they only need to deal with working at the framework level where all of their code is written in Dart. However, things become more complicated when applications need to make use of native platform APIs (e.g. bluetooth) or platform-specific libraries/SDKs (e.g AppAuth). In the event that a plugin hasn't been published by another member in the community, the solution is to start creating a plugin ourselves. There are a lot of articles on Flutter but they tend to focus on areas that are more on the framework level (e.g. state management, animations etc). Having written a few plugins myself (see here), I thought I'd share some advice and learnings. This is by no means an exhaustive list but should help collect some of the various resources I have made use of in a more centralised location

  • Read this guide on writing platform-specific code. Accessing platform-specific APIs makes use of platform channels. It's important to understand how they work and the associated APIs. There's also a table that describes how Dart values and types are interpreted in Android and iOS, and vice versa. This highlights some of the limitations on the types that can be transferred over the wire.

    • It also demonstrates how an error can be returned from the native platform back to the Flutter side. Determine the scenarios where it makes sense to do so with error codes and messages that indicate what went wrong. These will trigger a platform exception that can be caught by consumers of your plugin
  • Read the introduction on how to develop plugins available here. It walks through how to use create a new plugin via the tooling, including the optional parameters that can be specified e.g. what language should be used to write the Android and iOS code.

  • Determine what languages you'll be using on the Android and iOS side. By default, the tooling will require developers to write their code in Java for the Android side whilst the iOS side requires implementing the plugin in Objective-C. Kotlin and Swift are more modern languages for Android and iOS respectively that developers may find easier to learn. Furthermore, the syntax is more similar to each other. However, bear in mind that this may result in some friction when developers need to make use of your plugin (e.g. see this issue tracked on the Flutter repository)

  • Read this article about writing a good plugin that was written by one of the engineers at Google

  • Migrate the Android side of the plugin and example application (one is created automatically by the tooling) to AndroidX. Google has deprecated the Android support libraries that are used to allow older versions of Android to access new functionality. This can be done by opening the Android head project (i.e. the Android side under /<your_plugin>/example/android) of the example application in Android Studio and following the migration guide. I would imagine that future updates to the Flutter SDK would mean the plugin and applications created by the tooling will support AndroidX out of the box, thereby eliminating the need to follow this step. If you have an existing plugin that is being migrated to AndroidX, increment the version following semantic versioning to indicate it's a breaking change (more on this a bit below)

  • Make it easier for developers (including yourself) to test their application and your plugin. If you look at the generated code for a new plugin, you'll see that the methods within the plugin's class are static. Change this so that it's no longer static so that developers can mock your plugin (e.g. with the mockito package) when writing unit tests. Once this is done, you could also look at creating unit tests for your plugin. A guide on how to do write unit tests in Flutter can be found here

  • Structure your plugin so that consumers of your plugin will only need add a single import statement to access it once it's been added as a dependency. A guide on how to do this can be found here

  • Follow the semantic versioning guidelines when you need to increment the version when releasing updates. Incrementing the correct version element (major, minor and patch) will aid developers in identifying breaking changes besides having them mentioned in the changelog. This is admittedly something I had missed and so did some of the Flutter team from the looks of it. Guess we all make mistakes :)

  • Avoid exposing platform-specific methods and platform-specific plugins where possible. If Flutter has been picked as the framework you're using to create your applications then that is likely because you're creating a cross-platform application. Consider the following code

    if (Platform.isIOS) {
      batteryLevelPlugin.getBatteryLeveliOS();
    } else if (Platform.isAndroid) {
      batteryLevelPlugin.getBatteryLevelAndroid();
    }
    ...
    

    This doesn't make for a pleasant "developer experience" as branches in the code are introduced for each platform. This will be harder to maintain when more platforms are supported as well. Expose a single method that abstracts what needs to happen for each platform. In the case that there is functionality that may require platform-specific configuration/details, allow these to be passed through the parameters passed when invoking a method. In the local notifications plugin that I've written, there is a method for initialising the plugin and each platform can be configured in different ways. For example, one can specify the default icon for notifications on Android while on iOS one can specify if a sound should be played by default when a notification appears. I've separated this by creating a class that holds platform-specific information, which I've been calling these "platform-specifics". This has a different meaning in the Xamarin.Forms ecosystem but I think it has a nice ring to it

    class InitializationSettings {
      /// Settings for Android
      final AndroidInitializationSettings android;
    
      /// Settings for iOS
      final IOSInitializationSettings ios;
    
      const InitializationSettings(this.android, this.ios);
    }
    

    These are then passed by calling the method for initializing the plugin

    var flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
    var initializationSettingsAndroid =
        new AndroidInitializationSettings('app_icon');
    var initializationSettingsIOS = new IOSInitializationSettings(
        onDidReceiveLocalNotification: onDidRecieveLocalNotification);
    var initializationSettings = new InitializationSettings(
        initializationSettingsAndroid, initializationSettingsIOS);
    flutterLocalNotificationsPlugin.initialize(initializationSettings);
    

    Doing so allows you target the lowest common denominator whilst providing the extensionability of able to do more platform-specific work.

  • Comment the Dart code for your plugin using three forward slashes. Upon publishing your plugin, API docs are generated and these comments will appear in the docs to help developers understand the functionality available in your plugin

  • Create an example that demonstrates the functionality available in your plugin. An example application is generated out of the box for your plugin to encourage developers follow this practice. In the absence of having detailed documentation for every scenario, it'll help others with being able to have access to sample code and see the results when they run the application

  • Use the code the Flutter team has written for their plugins at this repository as a reference. These provide a good reference on practices to follow and native APIs that you may need to make use of whilst developing your plugin. This is especially useful if you are a new to doing either Android or iOS development when it comes to the using the native APIs and understanding the how each platform works. A couple of examples include

  • If you are developing a plugin that will act as a wrapper around an Android and/or iOS library, familiarise yourself with how a library/dependency can be installed on each platform. On Android, Gradle is used to handle dependencies and add them your build.gradle file as per this guide. On iOS, CocoaPods are used and dependencies are added in your plugin's Podspec file. In the plugin I've written as a wrapper around the AppAuth SDKs, you can see how I've added it as a dependency in Android and iOS. Once done, you should be able to make sure of the APIs provided by the library being consumed as a dependency

Hopefully, these collection of tips and links would be of use to other developers. Whilst I have had some exposure with each platform due to my experience with Xamarin, writing plugins for Flutter has provided me with a good opportunity to learn more about each platform, pick up new programming languages and learn more Flutter itself. Flutter's plugin ecosystem is still growing and by writing plugins you can help contribute to its growth and to the community.