Think functionality first, then APIs

Often when writing a plugin, you are trying to use an existing native library. The natural thing to do is to look at that library’s API and expose Dart equivalent API for it. Problems arise when APIs don’t match across platforms and you are forced to expose an intersection of the API often with crippling consequences…

When writing plugins, think about functionality first, not APIs. For instance, if you are writing a local storage plugin, you probably need APIs that support storing values of various types along with an ability to recall them. After you established what kind of functionality you want to provide, then think about your Dart API design:

Only after you have the Dart API, start looking at the public API and see your API calls need to be adapted to the platform specific library API calls:

If there’s a functionality you would like to provide which is not supported by one platform, it is OK for that API to gracefully fail for that platform. You should not crash the app and the behavior should be thoroughly documented. You should also request the missing functionality from the platform so your plugin can be complete.

Avoid platform-specific API methods

Flutter is platform agnostic so it is out of character for Flutter apps to be forced to write platform specific code. If the plugin user is doing the following in their application:

if (Platform.isIos) {

myPlugin.doIOSThing();

} else if (Platform.isAndroid) {

myPlugin.doAndroidThing();

}

consider moving the platform specific logic to the plugin itself so that, they can write:

myPlugin.doThing();

Avoid supporting only one platform

You might be tempted to start small and write a plugin for a functionality you want just for Android. Unfortunately, when the plugin is published, this creates confusion for plugin users when their app misbehaves on unsupported platforms.

There are counterexamples to this such as Android Intent. Intents are applicable only on Android so it is OK to expose an Android-only plugin for them.

Make your plugin easy to navigate and test

As a rule of thumb, your plugin should contain mostly Dart code.

Native layer is non-trivial to test since it typically deals with libraries that do not function on emulators.

Flutter is mostly written in Dart. Flutter apps are entirely written in Dart. When tracing code, it would be nice if the majority of the logic can be derived from Dart.

The only reason why you would need native layer is if the implementation on that platform requires you to keep state and/or do processing. One good example is biometric authentication which has its own complex lifecycle on Android that needs to be managed on the native side.

If you need to write extensive native code, consider moving the functionality to individual classes that can be unit tested.

Ideally, your native layer will simply listen for method calls, forward them to an existing library and pipe the results back to Dart. If you are doing anything more than that, try to see if it can be moved to Dart side. You will end up writing less code that way since the logic will likely need to be repeated across several platforms.

Avoid writing static (or global) methods

It is tempting to expose just:

Future<User> authenticate() async {

// Some code

}

However, prefer:

class AuthenticatePlugin {

Future<User> authenticate() async {

// Some code

}

}

which makes the plugin mock or fake friendly. Users can easily swap out the real implementation so their apps work on an emulator or a test harness.