Ever heard comments like this?
"Flutter is for small projects!"
"Flutter is a toy framework abandoned by Google!"
Well, I'm using Flutter to develop an app that serves over 1 million users — and I have some thoughts.
About the App
It's a B2C e-commerce app targeting the Japanese domestic market, focusing on a specific vertical. I joined the project from day zero as a senior developer, wrote about 30% of the production code (around 70,000 lines), and led the development of critical components like the API client, RBAC system, and overall feature-first architecture design.
Why Flutter?
There are many reasons to choose Flutter, but the biggest one for us was its cross-platform capability. In most cases, you don't have to worry about native APIs — which is a huge win for a small team trying to move fast.
That said, you can't completely avoid native development. Some dependencies — like ffmpeg
(see this article about its deprecation) — are either outdated or poorly maintained. I had to dive into the native layer several times to patch things up.
If you're coming from Swift or Kotlin, I'd say Flutter is actually a great next step. Your native experience will definitely come in handy.
Architecture & Key Decisions
We adopted Clean Architecture, which has a learning curve but brings long-term benefits. In our Flutter implementation, we structured the app as follows:
- Presentation Layer: Contains UI widgets and their corresponding ViewModels.
- Application Layer: Includes use cases and logic that orchestrates domain rules.
- Domain Layer: Defines core business entities and interfaces.
- Data Layer: Handles API clients, local DB access, and repository implementations.
For state management, we chose Riverpod with code generation. It turned out to be a solid choice — it reduces the mental load of managing providers manually and scales well as your app grows.
Our CI/CD is powered by Fastlane (see this guide on Flutter + Fastlane). Because our Git provider doesn't support Apple Silicon-based builds very well, we trigger the pipeline manually from a local Mac. It's not perfect, but it works reliably.
What I've Learned (and What I'd Do Differently)
🔧 Mobile performance: Backend matters
One thing I learned the hard way: client-side rate limiting is not reliable.
If you expect traffic spikes (say, during campaigns or promotions), stay in sync with your backend team. Prepare for surge handling together.
🚀 Flutter-side optimizations
That said, there's still a lot you can do on the client side:
- Image caching: I recommend using
cached_network_image: ^3.4.1
. But if your app is media-heavy, you should definitely look into using a CDN too. - Environment flavors: Always separate your environments — mock, staging, production. Absolutely make sure no one accidentally uploads a staging build to the App Store. Trust me, it happens.
- TDD & code quality: These days, everyone's using AI tools and agents to write code. That's great — until it creates unmanageable tech debt.
I adopted Test-Driven Development: writing unit tests and widget tests first, then building features around them. If it passes tests, it ships. Simple and safe.
Final Words
Flutter is not just for toy projects — we've proven it can power real, high-scale apps.
Feel free to reach out or explore more of my writing (and photography) at hokkaido.blue.
Xiaolong Li
Published on May 30, 2024