Skip to content
Tech Stack3 October 2026 · 9 min read

Building iOS Share Extensions in a Flutter App

A Share extension is a second iOS process with its own memory ceiling, not another Flutter screen. Target setup, App Group storage, the memory numbers, and the test matrix.

Building iOS Share Extensions in a Flutter App

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?

Two process blocks side by side: the left one brightly inked and open, the right one dim and closed, with the direct connector between them broken and the only continuous path dropping through a shared tray below.

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.

  1. In Xcode, choose File > New > Target, pick Share Extension, name it, and select Activate when prompted.
  2. 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.
  3. 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.
  4. 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.
  5. 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?

A shared container with its lid lifted, divided into three compartments holding a single thin card, a loose stack of sheets and a ruled solid block, with two separate hands reaching in from opposite sides.

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.

MechanismPluginUse it forWhere it hurts
UserDefaults key/valueshared_preference_app_groupA URL, a flag, a small JSON blobNo schema, no size discipline, easy to leave stale
Files in the group containerpath_providerImages, video, anything already on diskYou own cleanup; the extension cannot assume the app will delete anything
SQLite in the group containerpath_provider + sqfliteA queue of shared itemsTwo 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 stateWhat to verifyCommon failure
App never launchedExtension still writes to the group containerContainer path resolved from app-only setup code
App cold (killed)Payload survives until next launchExtension handed data to an in-memory sink
App warm (backgrounded)App picks up the payload on resumeListener attached only in initState of a first route
Shared data missing or corruptApp degrades without crashingUnvalidated 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.

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

How do you add a Share extension to a Flutter app?

You add it as a new Xcode target, not as Dart code. In Xcode choose File > New > Target, pick Share Extension, name it, and activate the scheme. Then make the Minimum Deployments iOS value match between Runner and the extension, drag Embed Foundation Extensions above Run Script in Runner's Build Phases, add the App Groups capability to both targets, and rebuild the project configuration with flutter build ios --config-only. Flutter's own guide walks the same sequence (Adding iOS app extensions). The extension itself is a separate binary with its own entitlements and its own memory budget.

What is an App Group and why does a Flutter share extension need one?

An App Group is a shared container that two targets of the same app can both read and write, and it is the only sanctioned way for a Share extension to reach the containing app. The two processes cannot call each other and may never run at the same time, so the extension writes into the group container and the app reads from it later. Flutter documents three shapes for that container: key/value storage through shared_preference_app_group, files through path_provider, and a SQLite database through path_provider plus sqflite. The group identifier starts with group. and both targets must use it verbatim.

Can you run Flutter code inside an iOS share extension?

You can, by instantiating a FlutterEngine and a FlutterViewController in the extension's Swift code, but the memory budget usually argues against it. Flutter advises only modifying extension UI when the extension supports at least 100MB, and an open issue reports the Share extension ceiling at 120MB with debug Flutter builds exceeding it on launch while release builds sit around 60–80MB (flutter/flutter#135243). A separate P1 report found the documented pattern itself leaking the view controller until it crashed. Native SwiftUI or UIKit is the safer default for a preview-and-save extension UI.

Why doesn't my Flutter share extension appear in the iOS share sheet?

The most common cause is build-phase ordering: Embed Foundation Extensions must sit above Run Script in the Runner target, and when it does not, the app builds cleanly and the extension is never embedded. Nothing fails loudly. The second common cause is a mismatched Minimum Deployments value between Runner and the extension target, which surfaces as link errors that read like an unrelated problem. Check both before you start reading logs, then rebuild with flutter build ios --config-only so the Xcode project picks up the regenerated configuration.

Should you use receive_sharing_intent or write the Swift yourself?

Use the plugin unless the extension needs custom UI or custom processing, because it carries the target boilerplate for you. Your extension class inherits from RSIShareViewController, you link the plugin's Swift package to the extension target, and you set a CUSTOM_GROUP_ID build setting in both targets so they agree on one App Group identifier. Version 1.9.0 reports roughly 147,000 downloads and 805 likes on pub.dev. Write the Swift yourself when you need a bespoke compose screen, extra validation before the payload lands, or behaviour the plugin does not expose.