An iOS Share extension is a separate process and target, not another Flutter screen.
That single sentence decides most of the architecture. When a user picks your app out of the iOS share sheet, your Dart code is not running. A second binary is, with its own bundle, its own entitlements and its own memory ceiling. Flutter's documentation states the boundary plainly: the containing app and the app extension do not communicate directly, and the containing app might not be running while the user interacts with the extension (Adding iOS app extensions). Everything below follows from that.
What happens when a user taps your app in the share sheet?

iOS launches a second executable embedded in your app bundle, hands it the shared item, and expects it to finish quickly. The containing app is not part of that transaction. It may be suspended. It may not have been launched since the last reboot.
Which means "send the data to my Flutter app" is the wrong model. The extension writes the payload somewhere both targets can read, then exits or hands control to the main app through a URL. The main app picks it up the next time it runs. Actually — that understates the timing problem. The main app may read that payload seconds later, or three days later, or never, and your Dart code has to be correct in all three cases.
This is the same process-boundary thinking that shows up when you drop from FlutterFlow into pure Flutter for custom code: the moment a feature stops being a widget tree, the tooling that made the widget tree easy stops helping you.
The hardest part belongs in Xcode
The hardest part of a Flutter Share extension is accepting that some of the implementation belongs in Xcode. Not in lib/. In a target you create through Xcode's menus, configured in tabs, signed with its own provisioning profile.
Teams resist this, and the resistance is expensive. Flutter's cross-platform promise is about the UI layer, and an extension is not a UI layer problem first — it is a packaging, entitlement and lifecycle problem that happens to render something. You can push a long way with a plugin wrapper, and most apps should. But the target still has to exist, the capability still has to be added to both targets, and the build phases still have to be ordered correctly, and none of that has a Dart API.
The practical version: budget a day for the Xcode work the first time, treat the Swift file as real code that belongs in review, and stop expecting flutter create to produce it. If you are scoping this into a quote, it belongs in the same bucket as the other native work that quietly shapes what mobile app development actually costs.
Setting up the target without breaking the build
The order of these steps matters, and two of them are the ones people skip.
- In Xcode, choose File > New > Target, pick Share Extension, name it, and select Activate when prompted.
- Open the General tab for both Runner and your extension target and make the Minimum Deployments iOS value match. Mismatched deployment targets produce link errors that read like something else entirely.
- Select the Runner target, open Build Phases, and drag Embed Foundation Extensions above Run Script. Flutter's docs call this out explicitly, and getting it wrong produces an app that builds and an extension that never appears.
- In Signing & Capabilities, add the App Groups capability to both targets and put them in the same group. The identifier starts with
group.and is shared verbatim by both. - Rebuild the iOS project configuration from the command line:
flutter build ios --config-only
Step 3 is the one that costs an afternoon. The build is green, the archive uploads, and the share sheet simply does not list your app, because the extension was never embedded before the Flutter script ran.
How do App Groups share data between the app and the extension?

An App Group gives both targets one shared container, the only sanctioned way to exchange data when neither process can call the other. Flutter's documentation lists three shapes for that container, and they are not interchangeable.
| Mechanism | Plugin | Use it for | Where it hurts |
|---|---|---|---|
UserDefaults key/value | shared_preference_app_group | A URL, a flag, a small JSON blob | No schema, no size discipline, easy to leave stale |
| Files in the group container | path_provider | Images, video, anything already on disk | You own cleanup; the extension cannot assume the app will delete anything |
| SQLite in the group container | path_provider + sqflite | A queue of shared items | Two processes, one file — write with that in mind |
For a share flow, files plus a small queue is usually the honest answer. The extension copies the incoming attachment into the group container, appends a row describing it, and exits. The app drains the queue when it next launches. That design survives the app being force-quit, which the "hand it over directly" design does not.
One detail that bites: the extension writes as a different process with different lifetime guarantees, so anything it leaves in the container is garbage until the app proves otherwise. Validate on read.
Do you need FlutterViewController inside the extension?
Usually not: the Flutter engine costs more memory than an extension is given, so native UI is the default and an embedded engine the exception. Flutter's documentation advises only modifying an app extension's UI when the extension supports at least 100MB of memory, which is a polite way of saying the engine is not free. An open Flutter issue from 2023 puts the Share extension ceiling at 120MB and reports that a debug build of a minimal Flutter extension exceeds it on launch, while release builds sit around 60–80MB (flutter/flutter#135243).
It gets sharper. In March 2025 a P1 issue reported that the officially documented pattern itself leaked: the FlutterViewController was not released after dismissal, so the extension crashed after being opened several times in release mode (flutter/flutter#165829). That issue is closed now, but the lesson stands — embedding the engine in a process with a hard memory cap is the part of this feature most likely to fail in the field rather than in CI.
Write the extension UI in SwiftUI or UIKit when it is a preview, a text field and a Save button. Reach for FlutterEngine and GeneratedPluginRegistrant only when the extension genuinely needs your Dart business logic, and then measure it.
What CUSTOM_GROUP_ID does in the plugin route
Most teams never write the Swift. receive_sharing_intent carries the extension boilerplate for you: your extension class inherits from RSIShareViewController, you link the plugin's Swift package to the extension target under General > Frameworks and Libraries, and you define a user-defined build setting called CUSTOM_GROUP_ID in both targets holding your App Group identifier. That setting is how two targets that cannot see each other's Dart configuration agree on one container name.
As of version 1.9.0 the package reports roughly 147,000 downloads and 805 likes on pub.dev, with 150 pub points. It is also Swift Package Manager only now, which brings us to the thing that changed this year.
Signing is where this fails quietly
Both targets need a provisioning profile carrying the App Groups entitlement. Automatic signing handles it once the capability is added in Xcode, and when it does not, the symptom is an extension that installs fine and then cannot open the shared container. No build error. Read each target's entitlements file before you go debugging Dart.
What Swift Package Manager changed
Flutter 3.44 made Swift Package Manager the default for iOS and macOS. In the announcement on 30 April 2026, the Flutter team reported that 61% of the top 100 iOS plugins had already migrated, and that the CocoaPods registry becomes permanently read-only on 2 December 2026 (Saying goodbye to CocoaPods).
For extension work this is not cosmetic. Under CocoaPods, a new target inherited dependencies through the Podfile. Under SwiftPM you attach packages to the target in Xcode, which means the extension target has an explicit, visible dependency list — better, but a new place to get it wrong. If a plugin you rely on has not migrated, Flutter still falls back to CocoaPods for now; that fallback has an expiry date on it.
A test matrix that catches the real failures
Four states, and the bugs live in the last two.
| App state | What to verify | Common failure |
|---|---|---|
| App never launched | Extension still writes to the group container | Container path resolved from app-only setup code |
| App cold (killed) | Payload survives until next launch | Extension handed data to an in-memory sink |
| App warm (backgrounded) | App picks up the payload on resume | Listener attached only in initState of a first route |
| Shared data missing or corrupt | App degrades without crashing | Unvalidated read of a file the extension never finished writing |
Debug builds complicate this. Flutter's docs require the simulator for debug-mode extension testing and warn that physical devices in debug mode can run out of memory, so release-mode verification on a real handset is not optional polish — it is the only run that reflects the memory profile users get. Use flutter run --release for it.
The same boundary applies to home screen widgets and action extensions. Different entry point, identical rule: separate process, shared container, no direct call.
Where this sits in a stack decision
If you are still choosing a framework and extensions are core to the product, this is one of the places where React Native versus Flutter stops being about rendering and starts being about how much native surface your team is willing to own. Both frameworks hand you the same Xcode target. Neither hides it.
And if the shared payload ends up in a backend anyway (a saved article, a clipped receipt, an uploaded image), then the group container is a buffer, not storage. The real sync belongs in the app, with the same care you would give any other Flutter and Firebase mobile backend write path.
Open your project's Build Phases tab right now and check whether Embed Foundation Extensions sits above Run Script. If it does not, and your extension has ever failed to appear in the share sheet, you just found it.
