Question #112MediumFlutter Basics

Flutter architecture guide lines

#flutter#architecture

Answer

Overview

Flutter architecture guidelines provide best practices for organizing code, managing state, and building scalable, maintainable apps. These guidelines follow principles from Clean Architecture, SOLID, and Flutter-specific patterns.


1. Separation of Concerns

Divide code into distinct layers: Presentation, Domain, and Data.

Layers

text
Presentation (UI)
Domain (Business Logic)
Data (API, Database)

Presentation Layer:

  • Widgets, pages, screens
  • BLoC, ViewModel, or state management
  • User interactions

Domain Layer:

  • Entities (business objects)
  • Use cases (business logic)
  • Repository interfaces

Data Layer:

  • API clients, database access
  • Data models (JSON serialization)
  • Repository implementations

Example

dart
// Domain Layer
class User {
  final String id;
  final String name;
  User({required this.id, required this.name});
}

abstract class UserRepository {
  Future<User> getUser(String id);
}

// Data Layer
class UserRepositoryImpl implements UserRepository {
  final ApiClient apiClient;
  UserRepositoryImpl(this.apiClient);

  
  Future<User> getUser(String id) async {
    final json = await apiClient.fetchUser(id);
    return User.fromJson(json);
  }
}

// Presentation Layer
class UserBloc extends Bloc<UserEvent, UserState> {
  final UserRepository userRepository;

  UserBloc(this.userRepository) : super(UserInitial()) {
    on<FetchUser>(_onFetchUser);
  }

  Future<void> _onFetchUser(FetchUser event, Emitter<UserState> emit) async {
    emit(UserLoading());
    try {
      final user = await userRepository.getUser(event.id);
      emit(UserLoaded(user));
    } catch (e) {
      emit(UserError(e.toString()));
    }
  }
}

2. Dependency Inversion (SOLID)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

dart
// ✅ Good: Depends on abstraction (interface)
class UserBloc {
  final UserRepository userRepository; // Interface, not implementation

  UserBloc(this.userRepository);
}

// ❌ Bad: Depends on concrete implementation
class UserBloc {
  final ApiClient apiClient; // Direct dependency on API client
}

Benefit: Easy to swap implementations (e.g., mock for testing).

dart
// Test with mock repository
final mockRepo = MockUserRepository();
final bloc = UserBloc(mockRepo);

// Production with real repository
final realRepo = UserRepositoryImpl(ApiClient());
final bloc = UserBloc(realRepo);

3. Single Responsibility (SOLID)

Each class should have one reason to change.

dart
// ❌ Bad: Does too much
class UserService {
  Future<User> fetchUser(String id) async { /* API call */ }
  void saveToDatabase(User user) { /* Database */ }
  void logEvent(String event) { /* Analytics */ }
}

// ✅ Good: Separate concerns
class UserRepository {
  Future<User> fetchUser(String id) async { /* API call */ }
}

class UserDatabase {
  void saveUser(User user) { /* Database */ }
}

class AnalyticsService {
  void logEvent(String event) { /* Analytics */ }
}

4. Immutability

Use immutable objects to prevent bugs and simplify state management.

dart
// ✅ Immutable entity
class User {
  final String id;
  final String name;

  const User({required this.id, required this.name});

  User copyWith({String? id, String? name}) {
    return User(
      id: id ?? this.id,
      name: name ?? this.name,
    );
  }
}

// Usage
final user = User(id: '1', name: 'Alice');
final updatedUser = user.copyWith(name: 'Bob'); // New instance

5. Repository Pattern

Use repositories to abstract data sources (API, database, cache).

dart
abstract class ProductRepository {
  Future<List<Product>> getProducts();
  Future<Product> getProductById(String id);
}

class ProductRepositoryImpl implements ProductRepository {
  final ApiClient apiClient;
  final Database database;

  ProductRepositoryImpl(this.apiClient, this.database);

  
  Future<List<Product>> getProducts() async {
    // Try cache first
    final cachedProducts = await database.getProducts();
    if (cachedProducts.isNotEmpty) return cachedProducts;

    // Fetch from API
    final products = await apiClient.fetchProducts();
    await database.saveProducts(products);
    return products;
  }

  
  Future<Product> getProductById(String id) async {
    return apiClient.fetchProductById(id);
  }
}

6. Use Cases (Domain Logic)

Encapsulate business logic in use cases (also called interactors).

dart
class LoginUseCase {
  final AuthRepository authRepository;

  LoginUseCase(this.authRepository);

  Future<User> call(String email, String password) async {
    // Business logic
    if (!email.contains('@')) {
      throw Exception('Invalid email');
    }

    if (password.length < 6) {
      throw Exception('Password too short');
    }

    return authRepository.login(email, password);
  }
}

// Usage in BLoC
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final LoginUseCase loginUseCase;

  AuthBloc(this.loginUseCase) : super(AuthInitial()) {
    on<LoginRequested>((event, emit) async {
      emit(AuthLoading());
      try {
        final user = await loginUseCase(event.email, event.password);
        emit(AuthSuccess(user));
      } catch (e) {
        emit(AuthError(e.toString()));
      }
    });
  }
}

7. Dependency Injection

Use Dependency Injection for loose coupling and testability.

dart
// ✅ Good: Constructor injection
class UserBloc {
  final UserRepository userRepository;
  UserBloc(this.userRepository); // Injected
}

// Setup with get_it or injectable
final getIt = GetIt.instance;

getIt.registerLazySingleton<UserRepository>(
  () => UserRepositoryImpl(ApiClient()),
);
getIt.registerFactory<UserBloc>(
  () => UserBloc(getIt<UserRepository>()),
);

// Usage
final userBloc = getIt<UserBloc>();

8. State Management

Choose state management based on app complexity.

App SizeRecommendation
SmallProvider, Riverpod, setState
MediumBLoC, Riverpod
LargeBLoC + Clean Architecture

Example (BLoC):

dart
// Event
abstract class UserEvent {}
class FetchUser extends UserEvent {
  final String id;
  FetchUser(this.id);
}

// State
abstract class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
  final User user;
  UserLoaded(this.user);
}
class UserError extends UserState {
  final String message;
  UserError(this.message);
}

// BLoC
class UserBloc extends Bloc<UserEvent, UserState> {
  final UserRepository userRepository;

  UserBloc(this.userRepository) : super(UserInitial()) {
    on<FetchUser>(_onFetchUser);
  }

  Future<void> _onFetchUser(FetchUser event, Emitter<UserState> emit) async {
    emit(UserLoading());
    try {
      final user = await userRepository.getUser(event.id);
      emit(UserLoaded(user));
    } catch (e) {
      emit(UserError(e.toString()));
    }
  }
}

9. Error Handling

Handle errors at every layer.

dart
// Data Layer
class UserRepositoryImpl implements UserRepository {
  
  Future<User> getUser(String id) async {
    try {
      final response = await apiClient.get('/users/$id');
      return User.fromJson(response.data);
    } catch (e) {
      throw NetworkException('Failed to fetch user: $e');
    }
  }
}

// Domain Layer (Use Case)
class FetchUserUseCase {
  final UserRepository userRepository;

  FetchUserUseCase(this.userRepository);

  Future<Result<User>> call(String id) async {
    try {
      final user = await userRepository.getUser(id);
      return Result.success(user);
    } catch (e) {
      return Result.failure(e.toString());
    }
  }
}

// Presentation Layer (BLoC)
on<FetchUser>((event, emit) async {
  emit(UserLoading());
  final result = await fetchUserUseCase(event.id);
  result.fold(
    (user) => emit(UserLoaded(user)),
    (error) => emit(UserError(error)),
  );
});

10. Testing

Write tests for each layer.

Unit Tests (Domain Layer):

dart
test('LoginUseCase validates email', () async {
  final useCase = LoginUseCase(mockAuthRepository);

  expect(
    () => useCase('invalid-email', 'password'),
    throwsException,
  );
});

Widget Tests (Presentation Layer):

dart
testWidgets('Shows loading indicator', (tester) async {
  await tester.pumpWidget(MyApp());

  expect(find.byType(CircularProgressIndicator), findsOneWidget);
});

Integration Tests:

dart
testWidgets('Complete login flow', (tester) async {
  await tester.pumpWidget(MyApp());
  await tester.enterText(find.byKey(Key('email')), 'test@example.com');
  await tester.enterText(find.byKey(Key('password')), 'password');
  await tester.tap(find.text('Login'));
  await tester.pumpAndSettle();

  expect(find.text('Welcome'), findsOneWidget);
});

11. Folder Structure

Feature-first structure (recommended for large apps):

text
lib/
  features/
    authentication/
      data/
        models/
        repositories/
        datasources/
      domain/
        entities/
        repositories/
        usecases/
      presentation/
        pages/
        widgets/
        bloc/
    home/
      data/
      domain/
      presentation/
  core/
    utils/
    constants/
    widgets/
  main.dart

12. Code Quality

Linting:

yaml
# analysis_options.yaml
include: package:flutter_lints/flutter.yaml

linter:
  rules:
    - prefer_const_constructors
    - avoid_print
    - use_key_in_widget_constructors

Run:

bash
flutter analyze  # Check for errors
flutter format lib/  # Auto-format code

13. Performance

dart
// ✅ Use const constructors
const Text('Hello');

// ✅ Avoid rebuilding entire tree
class MyWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Header(),  // Never rebuilds
        DynamicContent(), // Only this rebuilds
      ],
    );
  }
}

// ✅ Use ListView.builder for long lists
ListView.builder(
  itemCount: 1000,
  itemBuilder: (context, index) => ListTile(...),
)

14. Security

dart
// ✅ Use secure storage for tokens
final storage = FlutterSecureStorage();
await storage.write(key: 'auth_token', value: token);

// ✅ Use HTTPS only
final response = await http.get(Uri.parse('https://api.example.com'));

// ❌ Never hardcode API keys
// const apiKey = 'sk-1234567890'; ❌

Summary Checklist

  • Separate code into Presentation, Domain, Data layers
  • Use dependency injection (get_it, injectable)
  • Implement repository pattern
  • Use immutable entities (const, copyWith)
  • Write unit, widget, and integration tests
  • Follow SOLID principles
  • Use BLoC or Riverpod for state management
  • Handle errors at every layer
  • Use const constructors for performance
  • Follow Flutter linting rules

Learn more: