Feature-first folder structure
Organise code by feature (auth, onboarding, dashboard, billing, settings) with a shared core for networking, storage, theming and utilities. Each feature owns its screens, state and any feature-specific models. Large features can further split into data, domain and presentation layers. This is how our larger apps are organised, and it keeps a growing team from colliding in the same files.
The repository layer
Screens never call the API directly. A repository per domain (UserRepository, SubscriptionRepository, ProjectsRepository) exposes typed methods, decides between cache and network, handles pagination and retries, and maps errors into something the UI can show. Swapping Firebase for a REST API, or adding an offline cache, happens behind this layer.
Choosing state management
| Option | Fits when | Where we have used it |
|---|---|---|
| Riverpod | Most SaaS apps: testable providers, async data, clear dependency graph | CropFlow |
| Bloc | Complex, event-heavy features where explicit states and transitions pay off | GIF Maker editor |
| GetX | Small, fast apps where simplicity matters more than strict structure | Photo Compressor, BG Remover AI, MetaClean |
Auth session and entitlements
- A single auth session object exposed to the app, with token refresh handled in the core
- Entitlements fetched from the backend after sign-in and on purchase events, cached with a short lifetime
- Feature gates that read entitlements, so paywalls appear consistently
- Sign-out clears the cache and local database
Offline behaviour and caching
Decide per feature whether it must work offline. Read-mostly screens can use a local cache (Isar, Hive, Drift or Firestore's offline persistence) with pull-to-refresh. Write operations that must survive a lost connection need a queue and conflict rules. Design these deliberately; an app that silently drops writes loses trust fast.
Heavy work off the UI thread
Anything CPU-heavy (image processing, parsing large payloads, encryption) belongs in a background isolate so the interface stays smooth. Our media apps run segmentation, compositing and export in dedicated isolates; the same pattern applies to a SaaS app that processes documents or data locally.
Testing the architecture
- Unit tests on repositories, parsers and controllers, where most bugs live
- Widget tests for critical flows: sign-in, paywall, the core workflow
- Fake repositories for fast, deterministic tests
- Security rules tests for the Firebase backend