Building Production Flutter Plugins: A 156-Likes pub.dev Case Study
Publishing a Flutter plugin is a weekend. Maintaining one with 156 likes and 470 monthly downloads is a different game. The honest engineering story behind document_scanner_flutter, what worked, and what I'd build differently in 2026.
The native code is rarely the hard part of a plugin. The hard parts are: lifecycle correctness across both iOS and Android, permission flows, async cancellation, supporting users who file half-formed issues, and shipping breaking changes without breaking the world. A successful plugin costs 4 to 8 engineer-weeks before launch and a few hours per month forever after.
The plugin in one paragraph
document_scanner_flutter is a cross-platform Flutter plugin that lets an app capture a photo of a document, auto-detect the paper edges, perform perspective correction, and return the cropped, rectified image. It uses Apple's Vision framework on iOS and Google ML Kit's document scanner on Android. Apps using it include KYC flows, expense reporting tools, and field-data collection apps. It's been in production since 2021, currently sits at version 0.4.0 with 156 likes on pub.dev, and has been a steady source of issues, PRs, and lessons.
The architecture, layer by layer
A Flutter plugin lives at the edge between the Dart VM and the native platform. There are four layers, and getting any one of them wrong shows up as a 1-star review.
1. The Dart-facing API
This is what your users see. It must read like a normal Dart package: simple async functions, well-documented parameters, sensible defaults, type-safe results. For document_scanner_flutter, the entry point is one method:
final scannedDocs = await DocumentScannerFlutter.launch(
context,
source: ScannerFileSource.CAMERA,
labelsConfig: { ... },
);
One method, three optional parameters, returns Future<File?>. Users that want a simple flow get a simple flow. Users with custom needs can pass labelsConfig.
2. The platform channel
Dart talks to native code through a MethodChannel. Calls are async, named ("scanDocument"), and arguments are JSON-encodable. The channel is the contract: Dart and native must agree on the method name and the argument shape, or you get an opaque MissingPluginException.
Lesson learned the hard way: name your channel under your domain. biz.cv.documentScanner/document_scanner is fine; document_scanner alone collides with any other plugin that picks the same name. We renamed once after a user reported a clash, and it was a breaking change.
3. The native bridge
On iOS, the bridge is a Swift class that registers as a FlutterPlugin and implements handle(_ call: FlutterMethodCall, result: @escaping FlutterResult). On Android, it's a Kotlin class with onMethodCall. Both must:
- Validate arguments and return a structured error on bad input (
FlutterErrororresult.error). - Hold no implicit state across calls; use the call ID/result pattern for any async work.
- Handle the case where the user cancels mid-flow (most plugins forget this).
- Tear down listeners on detach (otherwise you leak when hot-reload happens during dev).
4. The native UI / system call
For document_scanner_flutter, this is where the actual work happens: launch the system camera, run Vision/ML Kit document detection, run perspective correction. This layer is platform-specific and tends to be where 80% of the user-reported bugs hide. iOS users on iPad split-view: edge case. Android users with no camera permission and TYPE_CAMERA: edge case. Foldable Galaxy with camera-rotation: edge case.
Lifecycle: the silent killer
Flutter plugin lifecycle is the most frequently misunderstood topic. The plugin is attached to a Flutter engine, but the engine can be detached and re-attached when the user backgrounds and foregrounds the app. If your plugin holds an Activity reference (Android) or a UIViewController reference (iOS) past detach, you crash the next time the engine reattaches.
The fix on Android: implement ActivityAware and store the Activity reference only between onAttachedToActivity and onDetachedFromActivity. The fix on iOS: never hold the Flutter engine's UIViewController; resolve it lazily from UIApplication.shared.keyWindow?.rootViewController.
Federated plugins: when to bother
The "federated plugin" pattern splits the package into:
document_scanner_flutter: the app-facing API package.document_scanner_flutter_platform_interface: the abstract contract.document_scanner_flutter_ios: the iOS implementation.document_scanner_flutter_android: the Android implementation.
This pattern lets multiple maintainers work on different platforms independently and lets users pick implementations (e.g., a third party could publish document_scanner_flutter_web without forking the API package).
Should you federate from day one? No. Start as a single package with iOS + Android folders. Federate only when you have either a credible second platform (web, desktop) or a co-maintainer for one of the platforms. Federating prematurely is overhead for one engineer.
The pub.dev scoring mechanic
Pub points are deterministic. They reward:
- Sound null safety (10 points)
- Valid pubspec with all required fields (10 points)
- Platform support declarations (20 points)
- Comprehensive README with example (10 points)
- Documentation on public API (20 points)
- Up-to-date dependencies (10 points)
- Static analysis clean (50 points)
document_scanner_flutter sits at 130 points. Hitting 160 requires more inline doc comments on edge methods. Hitting 100% requires zero analyzer warnings and a tight transitive dependency tree.
Likes and downloads are social signals on top, not part of the score. A well-scored plugin with zero downloads beats a popular plugin with bad scores in pub.dev's search ranking.
Verified publisher: a trust multiplier
The shield icon next to my publisher name on pub.dev means the package is published under a verified domain (ishaqhassan.com). Setting it up requires DNS TXT verification, takes 10 minutes, and meaningfully shifts pub.dev's trust signal. If you publish more than one plugin, get verified.
The support burden nobody mentions
The thing they don't tell you about publishing a plugin: if it's any good, you'll get issues forever. Most issues will be:
- "It doesn't work" with no reproduction steps
- "Crashes on Android 14" without a stack trace
- "How do I do X?" where X is in the README
You can either burn out or build templates. I have a GitHub issue template that asks for the Flutter doctor output, the platform, the OS version, and a code snippet. Issues without these get a polite "please update with more info" and auto-close after 2 weeks of no response. This single change took me from 30 minutes per issue to 5.
What I'd build differently in 2026
- Use Pigeon from day one. Pigeon is Flutter's official tool for type-safe platform channels. Hand-rolled MethodChannel is fine until your API surface grows, then it becomes a maintenance trap.
- Federated from second platform. Once Android worked, I should have federated before adding iOS, not after.
- Native UI in dev mode only. Most Flutter plugins fight to render their own UI in Flutter. For complex flows like document scanning, the system UI is faster, accessible, and free. I shipped Flutter UI on top of native first; I'd ship native-only now.
- Snapshot tests for the native bridge. Capture the JSON payload from each method call and snapshot it. Catches contract drift early.
The full roster
document_scanner_flutter is the most-liked package in my open source list, but it's not the only one. flutter_alarm_background_trigger is a scheduled-alarm plugin for Android. assets_indexer is a build-time Dart code generator that produces strongly-typed asset references. nadra_verisys_flutter wraps Pakistan's NADRA Verisys CNIC verification SDK.
Each of those packages has a similar shape: a small Dart API, a focused native or build-time bridge, comprehensive README, and ongoing user support. Each took roughly 6 to 10 weeks to ship to a production-quality state.
Where to go from here
If you're building your first Flutter plugin, start by reading Flutter's official plugin guide. Then read the source of two or three popular plugins on pub.dev to see what production-quality looks like. Then write yours, ship at 0.1.0, and iterate based on real user issues.
The three-tree architecture deep dive on this site explains why Flutter's rendering pipeline imposes the constraints plugins must respect. The framework contributions page lists the merged PRs into Flutter itself, which is the next step up from publishing a plugin: contributing to the framework that hosts every plugin.
Need a custom Flutter plugin?
Custom Flutter plugins, native bridge work, and SDK wrapping are common Flutter consulting engagements. Get in touch if you have a native API that needs a Flutter front end.
Contact