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

State management options we use in production
OptionFits whenWhere we have used it
RiverpodMost SaaS apps: testable providers, async data, clear dependency graphCropFlow
BlocComplex, event-heavy features where explicit states and transitions pay offGIF Maker editor
GetXSmall, fast apps where simplicity matters more than strict structurePhoto 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