arrow_back Blog home Home
Flutter Mobile Dart Architecture

Flutter Architecture Patterns for Production Apps

The widget tree is not your architecture. Here's how to structure Flutter apps that don't collapse under their own weight.

calendar_today December 2025 schedule 10 min read person Saptarshi Sadhu

Flutter's widget-first mental model is one of its greatest strengths for prototyping — and one of its greatest risks for production. When business logic leaks into widgets, you end up with a UI layer that's impossible to test, impossible to reason about, and impossible to hand off to another developer. This article documents the architecture patterns I've settled on after shipping Flutter apps to the Play Store.

info
Stack used Flutter 3.x · Dart · Riverpod 2.x · Freezed · GoRouter · Dio · Hive

folder_open Clean Architecture in Flutter

The folder structure communicates intent. I use a feature-first organization within a clean architecture shell:

text
lib/
├── core/              # shared infra (router, DI, theme, HTTP)
├── features/
│   ├── auth/
│   │   ├── data/      # repository impl, remote/local sources
│   │   ├── domain/    # entities, repository interface, use cases
│   │   └── presentation/  # providers, screens, widgets
│   └── home/
│       └── ...
└── main.dart

The key rule: nothing in presentation/ imports from data/ directly. The domain layer defines interfaces; the data layer implements them. Dependency inversion means you can swap SQLite for Supabase without touching a single widget.

manage_accounts State Management with Riverpod 2

Riverpod's code-generation approach (@riverpod annotations) eliminates boilerplate while making providers statically analyzable. The pattern I use for async feature state:

dart
// Domain: pure functions, no Flutter imports
abstract class UserRepository {
  Future<List<User>> getUsers();
}

// Presentation: AsyncNotifier for loading/error/data
@riverpod
class UsersNotifier extends AsyncNotifier<List<User>> {
  @override
  Future<List<User>> build() async {
    final repo = ref.read(userRepositoryProvider);
    return repo.getUsers();
  }

  Future<void> refresh() async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() => build());
  }
}

route Routing with GoRouter

Flutter's Navigator 2.0 is powerful but verbose. GoRouter gives you declarative, URL-based routing that works identically on mobile and web.

dart
final router = GoRouter(
  initialLocation: '/',
  redirect: (ctx, state) {
    final authed = ctx.read(authProvider).isAuthenticated;
    if (!authed && !state.location.startsWith('/auth'))
      return '/auth/login';
    return null;
  },
  routes: [
    GoRoute(path: '/',        builder: (_, __) => const HomeScreen()),
    GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()),
    GoRoute(path: '/auth/login', builder: (_, __) => const LoginScreen()),
  ],
);

deployed_code Immutable Data with Freezed

Mutable domain entities are a source of subtle bugs. Freezed generates immutable value objects with copyWith, pattern matching, and JSON serialization:

dart
@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,
    required String email,
    @Default(false) bool isPremium,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json)
      => _$UserFromJson(json);
}
"A widget that owns state is a widget that you can't reuse without duplicating bugs."
Lift state up, keep widgets dumb, and your widget tree becomes a pure rendering function.
3
Architecture layers
0
Logic in widgets
100%
Domain testability

tips_and_updates Production Checklist

check_circle
The payoff Apps built with clean architecture survive team growth, feature creep, and major refactors. The upfront investment in structure pays compounding returns.