
We shipped a Flutter app to production about eight months ago for a client in the logistics space, and I want to write down the stuff that actually bit us, because most "Flutter in production" posts I read beforehand were either marketing fluff or way too basic. This one's going to be messier and more specific, closer to what actually happened.
Quick context: cross-platform requirement (iOS + Android), tight timeline, a small team of three, and a backend already built in Node.js that we had to integrate with, not design from scratch. That combination shaped a lot of the decisions below. On the client side, this was a website development company Ludhiana, led engagement, with our team handling the Flutter build specifically while the client's existing web presence stayed with their original team.
State Management: We Switched Mid-Project and It Was Worth It
We started with Provider because it's what the docs push you toward early on, and honestly, it's fine for small apps. Once our widget tree got deeper and we had cross-screen state that needed to survive navigation (cart contents, auth state, a multi-step form), Provider started producing a lot of boilerplates and some annoying rebuild issues we couldn't cleanly debug.
We migrated to Riverpod around week six. Painful in the short term, genuinely worth it by the end. The dependency injection is cleaner, testing providers in isolation is much easier, and we stopped fighting context-based lookups breaking when widgets moved around in the tree.
final cartProvider = StateNotifierProvider<CartNotifier, CartState>((ref) {
return CartNotifier(ref.read(apiClientProvider));
});
If I were starting fresh today, I'd just start with Riverpod (or Bloc if your team prefers a stricter pattern) and skip Provider entirely, unless the app is genuinely tiny and staying that way.
Platform Channels Are Where the Pain Lives
Flutter's "write once" promise holds up well for UI. It holds up much less well the moment you need something platform-specific, in our case, a barcode scanner integration and background location updates for delivery tracking. We ended up writing native platform channel code for both, and this is where the estimate blew past what we'd budgeted.
The barcode scanner especially, we tried three different Flutter plugins before landing on a combination of a maintained plugin plus custom native fallback code for a specific Android device model our client's driver actually used in the field. Turns out that device had a known camera focus bug that only showed up in production, never in our testing on newer phones. Lesson: test on the actual hardware your users have, not whatever's sitting in your office.
static const platform = MethodChannel('com.example.app/scanner');
Future<String?> scanBarcode() async {
try {
final result = await platform.invokeMethod<String>('startScan');
return result;
} on PlatformException catch (e) {
// handle scanner-specific failures here
return null;
}
}
Build Times Got Bad, Then We Fixed Them
By month four, our CI build times had crept up to almost 20 minutes for a full iOS build, which killed our iteration speed. Most of it came from an over-bloated dependency list, we'd added packages for things we ended up building custom solutions for anyway and never removed the old dependencies. A dependency audit cut our pubspec down significantly and shaved several minutes off build time.
We also moved to splitting our CI pipeline, so Android and iOS builds ran in parallel instead of sequentially, which sounds obvious in hindsight but wasn't how our pipeline was originally set up. Small process fix, meaningful time saved across a team running multiple builds a day. It's a fix I'd now recommend to any website developer in Ludhiana working on a mobile-and-web hybrid team, since the CI habits that work fine for a single web repo often don't scale cleanly once mobile builds get added into the same pipeline.
API Integration and Error Handling Needed More Structure Than We Planned For
This is the unglamorous part nobody talks about enough. Our backend team (working separately, coordinated through a mobile app development in Ludhiana partnership on our side) had solid API docs, but real-world network conditions, spotty connectivity for delivery drivers moving between areas, exposed gaps in our error handling that our happy-path testing never caught.
We ended up building a proper retry-with-backoff layer and a local queue for actions taken while offline, syncing once connectivity returned. Should have built this from day one instead of bolting it on in month five once drivers started reporting "lost" data that was actually just stuck in a failed request nobody retried.
Future<T> withRetry<T>(Future<T> Function() action, {int retries = 3}) async {
for (int attempt = 0; attempt < retries; attempt++) {
try {
return await action();
} catch (e) {
if (attempt == retries - 1) rethrow;
await Future.delayed(Duration(seconds: pow(2, attempt).toInt()));
}
}
throw Exception('Retry logic failed unexpectedly');
}
Testing Discipline Slipped Under Deadline Pressure, and We Paid For It
We had decent widget test coverage early on. Then the deadline got tighter, and testing was the first thing that got quietly deprioritized, which is a classic mistake, and we knew it was a classic mistake even while doing it. Two production bugs that shipped in month six would have been caught by tests we didn't have time to write. Not catastrophic, but embarrassing, and it cost more time to hotfix than it would have taken to just write the tests properly the first time.
If you're working with an app developer Ludhiana team or any external partner on a Flutter project, it's worth setting a hard rule upfront about minimum test coverage for anything touching payments, auth, or offline sync, the categories where bugs are expensive rather than just annoying.
What I'd Actually Do Differently Next Time
Start with Riverpod, not Provider. Budget real time for platform channel work if there's any hardware integration at all, don't assume a plugin will "just work" on every device. Set up parallel CI builds from day one. And don't let testing slip just because the deadline is tight; it always costs more time later than it saves now.
Flutter's genuinely good for cross-platform work when the app is UI-heavy and doesn't need deep hardware integration. The moment hardware or background processes get involved, budget extra time and extra patience, because that's where the framework's abstractions start leaking.
Top comments (0)