abhishekbrt avatar

flutter-architecture

Feature-first architecture patterns for scalable Flutter apps. Covers project structure, dependency

by abhishekbrt|Open Source

Flutter Architecture

Core Principle

Feature-First, Layer-Second: Group by feature (auth, home, profile), then by layer (data, domain, presentation) within each feature.

When to Use What

DecisionGuidance
New projectStart with core/ + first feature folder
New featureCreate features/{name}/ with data/domain/presentation
Shared widgetOnly in core/widgets/ if used by 3+ features
Shared logiccore/ for network, error handling, providers

Detailed Guides

TopicGuideUse When
Project Structurefeature-first-structure.mdSetting up folders, creating features
Dependency Injectiondependency-injection.mdRiverpod DI, provider dependencies, testing
Repository Patternrepository-pattern.mdData layer, caching, error handling

Quick Reference

Feature Module Template

features/auth/
├── data/
│   ├── datasources/
│   │   ├── auth_remote_source.dart
│   │   └── auth_local_source.dart
│   ├── models/
│   │   └── user_model.dart           # JSON serialization
│   ├── repositories/
│   │   └── auth_repository_impl.dart  # Implements interface
│   └── providers/
│       └── auth_repository_provider.dart  # Riverpod provider
├── domain/
│   ├── entities/
│   │   └── user.dart                  # Pure business object
│   └── repositories/
│       └── auth_repository.dart       # Interface (abstract class)
└── presentation/
    ├── providers/
    │   └── auth_provider.dart         # State management (Riverpod)
    ├── screens/
    │   └── login_screen.dart
    └── widgets/
        └── login_form.dart

Interface-First Pattern (TDD)

// 1. Define interface in domain/repositories/
abstract class AuthRepository {
  Future<User> login(String email, String password);
  Future<void> logout();
  Future<User?> getCurrentUser();
}

// 2. Create implementation shell in data/repositories/
class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteSource _remoteSource;
  final AuthLocalSource _localSource;

  AuthRepositoryImpl({
    required AuthRemoteSource remoteSource,
    required AuthLocalSource localSource,
  }) : _remoteSource = remoteSource,
       _localSource = localSource;

  @override
  Future<User> login(String email, String password) {
    throw UnimplementedError(); // RED phase
  }
  
  // ... other methods
}

Provider Registration (Riverpod)

// features/auth/data/providers/auth_repository_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'auth_repository_provider.g.dart';

@riverpod
AuthRepository authRepository(Ref ref) {
  return AuthRepositoryImpl(
    remoteSource: ref.watch(authRemoteSourceProvider),
    localSource: ref.watch(authLocalSourceProvider),
  );
}

// features/auth/presentation/providers/auth_provider.dart
@riverpod
class Auth extends _$Auth {
  @override
  FutureOr<User?> build() async {
    final repo = ref.watch(authRepositoryProvider);
    return repo.getCurrentUser();
  }

  Future<void> login(String email, String password) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      return ref.read(authRepositoryProvider).login(email, password);
    });
  }

  Future<void> logout() async {
    await ref.read(authRepositoryProvider).logout();
    state = const AsyncData(null);
  }
}

Alternative: BLoC with GetIt

For complex event-driven features, use BLoC pattern with GetIt:

// core/di/injection.dart (alternative approach)
import 'package:get_it/get_it.dart';

final sl = GetIt.instance;

void configureDependencies() {
  // Core
  sl.registerLazySingleton<ApiClient>(() => ApiClient());
  
  // Feature: Auth
  _initAuth();
}

void _initAuth() {
  sl.registerLazySingleton<AuthRemoteSource>(
    () => AuthRemoteSource(client: sl()),
  );
  sl.registerLazySingleton<AuthRepository>(
    () => AuthRepositoryImpl(remoteSource: sl(), localSource: sl()),
  );
  sl.registerFactory<AuthBloc>(
    () => AuthBloc(repository: sl()),
  );
}

State Management Choice

ScenarioUse
Most featuresRiverpod AsyncNotifierProvider
Simple sync stateRiverpod NotifierProvider
Complex event flowsBLoC (with GetIt DI)

See state-management/SKILL.md for detailed guidance.

Anti-Patterns

AvoidInstead
Widget directly calling APIWidget → Provider → Repository → DataSource
Business logic in widgetsMove to Notifier/BLoC
Concrete class dependenciesDepend on abstract interfaces
Circular feature dependenciesExtract shared code to core/
God objects (one class does everything)Single responsibility per class
Passing ref to business classesInject dependencies directly