TL;DR
The native code is rarely the hard part of a Flutter 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 numbers
| Metric | Value |
|---|---|
| pub.dev likes | 156 |
| pub points | 130 |
| Monthly downloads | 470 |
| In production | 5+ years |
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 Vision on iOS and Google ML Kit on Android. Apps using it include KYC flows, expense reporting tools, and field-data collection apps.
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.
2. The platform channel. Dart talks to native code through a MethodChannel. Calls are async, named, and arguments are JSON-encodable. Lesson learned the hard way: name your channel under your domain. Generic names collide with other plugins.
3. The native bridge. On iOS, a Swift FlutterPlugin class. On Android, a Kotlin FlutterPlugin. Both must validate arguments, hold no implicit state, handle cancellation, and tear down listeners on detach.
4. The native UI / system call. Where 80% of user-reported bugs hide. iPad split-view, Android camera permissions, foldable rotations: all edge cases.
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.
On Android, implement ActivityAware and store the Activity reference only between onAttachedToActivity and onDetachedFromActivity. On iOS, never hold the Flutter engine UIViewController; resolve it lazily.
Federated plugins: when to bother
The federated plugin pattern splits the package into API + platform interface + per-platform implementations. 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.
Pub.dev scoring is deterministic
Pub points reward sound null safety, valid pubspec, OS support declarations, comprehensive README with example, documentation on public API, up-to-date dependencies, and clean static analysis. Likes and downloads are social signals on top.
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). DNS TXT verification, takes 10 minutes, meaningfully shifts trust.
What I would build differently in 2026
- Use Pigeon from day one for type-safe platform channels.
- Federate from second platform, not from launch.
- Native UI in dev mode only: ship native-only flow first, Flutter-styled UI only when truly needed.
- Snapshot tests for the native bridge to catch contract drift early.
The full version with the support burden chapter, the iOS/Android lifecycle deep dive, and the full pub.dev scoring breakdown is on my site:
Read the complete article on ishaqhassan.dev
I am a verified pub.dev publisher (ishaqhassan.com) and Flutter Framework Contributor with 6 merged PRs into flutter/flutter. More writing at ishaqhassan.dev/blog/.
Top comments (0)