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.

156
pub.dev likes
130
pub points
470
monthly downloads
5+ yr
in production
TL;DR
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:

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:

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:

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:

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

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

Related reading

← Back to ishaqhassan.dev