Plugins and Native Code
Most Flutter apps call native code somewhere: the camera, payments, push
notifications, secure storage. That code comes from plugins or from your own
android/ and ios/ folders, and it is built differently from your Dart code.
This page explains how the two sides connect, and why the line between them is
also the line between what a Shorebird patch can and can’t change.
Packages, plugins and FFI packages
Section titled “Packages, plugins and FFI packages”Flutter’s docs describe three kinds of package:
- Dart packages contain only Dart code, such as
path,httporflutter_bloc. Everything in them is compiled into your app’s Dart code. - Plugin packages combine a Dart API with platform-specific implementations:
Kotlin or Java for Android, Swift or Objective-C for iOS and macOS, C++ for
Windows and Linux.
url_launcherandcameraare plugins. - FFI packages call C-compatible native code directly through
dart:ffi. A build hook (hook/build.dart) compiles the native code and bundles it with your app.
A plugin’s pubspec.yaml declares the platforms it implements, and that is the
quickest way to tell whether a package ships native code:
An Android, iOS, macOS, Windows or Linux entry with a pluginClass, or with
ffiPlugin: true, ships native code for that platform (web entries are Dart).
An FFI package has no platforms: entry; its hook/build.dart file is the sign
that it builds native code. Large plugins are often
federated:
the package you depend on (url_launcher) pulls in one package per platform
(url_launcher_android, url_launcher_ios), so check those too.
How Dart calls native code
Section titled “How Dart calls native code”Platform channels
Section titled “Platform channels”A platform channel sends messages between your Dart code and the host app. The Dart side calls a method by name, and a handler registered in Kotlin or Swift answers:
Messages are serialized with StandardMessageCodec, which carries JSON-like
values: null, booleans, numbers, strings, typed byte lists, lists and maps.
Calls are asynchronous, and the native handler runs on the platform’s main
thread unless it uses a task queue. Each plugin registers its handlers when the
engine starts, through the generated plugin registrant.
Channels aren’t type safe: both sides have to agree on method names and argument types. Flutter recommends Pigeon, which generates matching Dart and native code from one Dart definition, for new code.
When the Dart side calls a method that has no handler on the native side, or the
handler answers “not implemented”, invokeMethod throws a
MissingPluginException:
In development, that usually means the app needs a full restart after adding a plugin, because hot reload and hot restart don’t rebuild native code. In a release, it means the Dart code expects native code that isn’t in the installed app, which is exactly the situation a patch can create (see below).
dart:ffi calls C functions directly,
without serializing messages, and synchronously. That makes it the faster option
for heavy work such as image processing, cryptography or a database engine. The
C code is compiled for each target platform and shipped inside the app as a
native library. Generators such as ffigen and
jnigen write the Dart bindings for C,
Objective-C, Swift, Java and Kotlin APIs.
What a Code Push patch can change
Section titled “What a Code Push patch can change”A Shorebird patch replaces your app’s compiled Dart code and nothing else (see How Flutter builds your app). For a plugin, that splits along the channel:
| Change | Patch? | Why |
|---|---|---|
| Your Dart code that calls a plugin | Yes | It’s Dart code |
| A Dart-only package added, removed or upgraded | Yes | It compiles into your Dart code |
| The Dart half of a plugin, with its native half unchanged | Yes | Only the Dart code changed |
| A plugin with native code added to the app | No | Its native code isn’t in the installed app |
| A plugin upgraded in a way that changes its native code | No | The installed app still has the old native code |
| Your own Kotlin, Java, Swift or Objective-C code | No | Native code ships only with a store release |
| C code built for FFI | No | It’s a native library inside the installed app |
If a patch needs native code the installed app doesn’t have, the patch still
installs, but the calls fail on users’ devices: a platform channel call throws
MissingPluginException, FFI code can’t find its native library, and a plugin
whose Dart and native halves no longer match can crash the app. Ship native
changes in a new release with shorebird release, then patch on top of it.
shorebird patch helps you catch these changes. It compares the new build with
the release and warns about native changes it can detect. On Android it compares
the app’s .dex files (Java and Kotlin code), so a change to a C or C++ library
alone doesn’t produce a warning. On iOS and macOS it compares Podfile.lock
once the app’s executable differs, so an app whose plugins all come through
Swift Package Manager, with no Podfile.lock, gets no native-changes warning.
The details, and what to do about a warning you didn’t expect, are in
Your app contains native changes warning when creating a patch.
Keeping Dart and native code compatible
Section titled “Keeping Dart and native code compatible”Every patch you ship runs against the native code of the release it patches. Some habits make that easy:
- Commit
pubspec.lock, so a plugin upgrade, including a transitive one, shows up in review rather than in a native-changes warning. - Add a plugin in a release, even if you only start using it later. The Dart code that calls it can then arrive in a patch.
- Treat your own channels as an API. When a patch’s Dart code calls a method
that only newer native code implements, catch
MissingPluginExceptionand fall back, so the same Dart change can also ship as a patch to older releases that don’t have the method yet.
The FAQ’s Can new packages or plugins be added in a patch? has more on checking a package for native code.