Skip to content

🏗️ Software Principles for Flutter Development

Table of Contents

SOLID Principles

Single Responsibility Principle (SRP)

Rule: Each class should have only one reason to change.

Flutter Implementation Rules

  1. Separate UI from business logic - Widgets should only handle UI concerns
  2. Create dedicated service classes for specific functionalities
  3. Use separate classes for validation, data transformation, and API calls
  4. Avoid god classes that handle multiple responsibilities
dart
// ✅ Good: Single responsibility
class UserValidator {
  bool validateEmail(String email) {
    return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
  }
  
  bool validatePassword(String password) {
    return password.length >= 8;
  }
}

class UserRepository {
  Future<User> getUser(int id) async {
    // Only handles user data retrieval
  }
  
  Future<void> saveUser(User user) async {
    // Only handles user data persistence
  }
}

// ❌ Bad: Multiple responsibilities
class UserManager {
  bool validateEmail(String email) { /* validation */ }
  Future<User> getUser(int id) async { /* API call */ }
  Widget buildUserWidget(User user) { /* UI rendering */ }
  void logUserAction(String action) { /* logging */ }
}

Open/Closed Principle (OCP)

Rule: Software entities should be open for extension but closed for modification.

Flutter Implementation Rules

  1. Use abstract classes for base functionality
  2. Implement interfaces for different behaviors
  3. Use composition over inheritance for flexibility
  4. Create extension points through abstract methods
dart
// ✅ Good: Open for extension, closed for modification
abstract class PaymentProcessor {
  Future<PaymentResult> processPayment(double amount);
}

class CreditCardProcessor extends PaymentProcessor {
  @override
  Future<PaymentResult> processPayment(double amount) async {
    // Credit card specific implementation
  }
}

class PayPalProcessor extends PaymentProcessor {
  @override
  Future<PaymentResult> processPayment(double amount) async {
    // PayPal specific implementation
  }
}

class PaymentService {
  final PaymentProcessor processor;
  
  PaymentService(this.processor);
  
  Future<PaymentResult> makePayment(double amount) {
    return processor.processPayment(amount);
  }
}

Liskov Substitution Principle (LSP)

Rule: Objects of a superclass should be replaceable with objects of its subclasses.

Flutter Implementation Rules

  1. Maintain behavioral contracts in inheritance hierarchies
  2. Ensure subclasses don't weaken base class guarantees
  3. Use proper method overrides without changing expected behavior
  4. Test substitutability in unit tests
dart
// ✅ Good: Proper substitution
abstract class Animal {
  void makeSound();
  void move();
}

class Dog extends Animal {
  @override
  void makeSound() => print('Woof');
  
  @override
  void move() => print('Running');
}

class Bird extends Animal {
  @override
  void makeSound() => print('Tweet');
  
  @override
  void move() => print('Flying');
}

// Can substitute any Animal implementation
void playWithAnimal(Animal animal) {
  animal.makeSound();
  animal.move();
}

Interface Segregation Principle (ISP)

Rule: Clients should not be forced to depend on interfaces they don't use.

Flutter Implementation Rules

  1. Create focused interfaces with specific purposes
  2. Avoid fat interfaces with too many methods
  3. Use composition to combine multiple interfaces
  4. Separate concerns into different interfaces
dart
// ✅ Good: Segregated interfaces
abstract class Readable {
  Future<String> read();
}

abstract class Writable {
  Future<void> write(String data);
}

abstract class Deletable {
  Future<void> delete();
}

// Implement only what you need
class ReadOnlyFile implements Readable {
  @override
  Future<String> read() async {
    // Implementation
  }
}

class FullAccessFile implements Readable, Writable, Deletable {
  @override
  Future<String> read() async { /* implementation */ }
  
  @override
  Future<void> write(String data) async { /* implementation */ }
  
  @override
  Future<void> delete() async { /* implementation */ }
}

Dependency Inversion Principle (DIP)

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

Flutter Implementation Rules

  1. Depend on abstractions not concrete implementations
  2. Use dependency injection for loose coupling
  3. Create interfaces for external dependencies
  4. Inject dependencies through constructors
dart
// ✅ Good: Dependency inversion
abstract class ApiClient {
  Future<Map<String, dynamic>> get(String url);
}

class DioApiClient implements ApiClient {
  final Dio _dio = Dio();
  
  @override
  Future<Map<String, dynamic>> get(String url) async {
    final response = await _dio.get(url);
    return response.data;
  }
}

class UserService {
  final ApiClient _apiClient;
  
  UserService(this._apiClient);
  
  Future<User> getUser(int id) async {
    final data = await _apiClient.get('/users/$id');
    return User.fromJson(data);
  }
}

// Dependency injection
void setupDependencies() {
  GetIt.instance.registerSingleton<ApiClient>(DioApiClient());
  GetIt.instance.registerFactory<UserService>(() => 
    UserService(GetIt.instance<ApiClient>()));
}

Additional Principles

DRY (Don't Repeat Yourself)

Rule: Avoid code duplication by extracting common functionality.

Flutter Implementation Rules

  1. Extract common widgets into reusable components
  2. Create utility functions for repeated logic
  3. Use mixins for shared behavior
  4. Centralize constants and configurations
dart
// ✅ Good: DRY implementation
class AppConstants {
  static const String apiBaseUrl = 'https://api.example.com';
  static const Duration requestTimeout = Duration(seconds: 30);
  static const String defaultErrorMessage = 'Something went wrong';
}

class ValidationUtils {
  static bool isValidEmail(String email) {
    return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
  }
  
  static bool isValidPhone(String phone) {
    return RegExp(r'^\+?[1-9]\d{1,14}$').hasMatch(phone);
  }
}

// Reusable widget
class CustomButton extends StatelessWidget {
  final String text;
  final VoidCallback onPressed;
  final bool isLoading;
  
  const CustomButton({
    required this.text,
    required this.onPressed,
    this.isLoading = false,
  });
  
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: isLoading ? null : onPressed,
      child: isLoading 
        ? const CircularProgressIndicator()
        : Text(text),
    );
  }
}

KISS (Keep It Simple, Stupid)

Rule: Keep code simple and easy to understand.

Flutter Implementation Rules

  1. Prefer simple solutions over complex ones
  2. Use clear, descriptive names for variables and methods
  3. Avoid unnecessary abstractions in early development
  4. Break complex problems into smaller, manageable pieces
dart
// ✅ Good: Simple and clear
class User {
  final String name;
  final String email;
  final int age;
  
  User({required this.name, required this.email, required this.age});
  
  bool isAdult() => age >= 18;
  
  String getDisplayName() => name;
}

// ❌ Bad: Overly complex
class UserEntity {
  final PersonalIdentificationData _pid;
  final ContactInformationData _cid;
  final DemographicData _dd;
  
  UserEntity(this._pid, this._cid, this._dd);
  
  bool evaluateLegalAdulthoodStatus() {
    return _dd.calculateAgeInYears() >= 
           LegalFrameworkConstants.MINIMUM_ADULT_AGE_THRESHOLD;
  }
}

YAGNI (You Aren't Gonna Need It)

Rule: Don't implement functionality until it's actually needed.

Flutter Implementation Rules

  1. Implement only current requirements - avoid over-engineering
  2. Don't create abstractions until you have multiple implementations
  3. Avoid premature optimization - optimize when you have performance issues
  4. Keep code flexible but don't add unnecessary complexity
dart
// ✅ Good: Implement only what's needed
class ProductService {
  Future<List<Product>> getProducts() async {
    // Simple implementation for current needs
    final response = await http.get(Uri.parse('/products'));
    return (jsonDecode(response.body) as List)
        .map((json) => Product.fromJson(json))
        .toList();
  }
}

// ❌ Bad: Over-engineered for current needs
abstract class ProductRepository {
  Future<List<Product>> getProducts();
  Future<Product> getProductById(int id);
  Future<List<Product>> searchProducts(String query);
  Future<List<Product>> getProductsByCategory(String category);
  Future<List<Product>> getProductsByPriceRange(double min, double max);
  Future<List<Product>> getProductsByRating(double minRating);
  Future<List<Product>> getProductsByAvailability(bool available);
  Future<List<Product>> getProductsByTags(List<String> tags);
  // ... 20 more methods that aren't used yet
}

Composition over Inheritance

Rule: Favor object composition over class inheritance.

Flutter Implementation Rules

  1. Use mixins for shared behavior
  2. Compose objects instead of deep inheritance
  3. Prefer interfaces over abstract base classes
  4. Use delegation for extending functionality
dart
// ✅ Good: Composition
class Logger {
  void log(String message) => print('LOG: $message');
}

class EmailService {
  final Logger _logger;
  
  EmailService(this._logger);
  
  Future<void> sendEmail(String to, String subject, String body) async {
    _logger.log('Sending email to $to');
    // Email sending logic
  }
}

class NotificationService {
  final Logger _logger;
  
  NotificationService(this._logger);
  
  Future<void> sendNotification(String message) async {
    _logger.log('Sending notification: $message');
    // Notification logic
  }
}

// ❌ Bad: Deep inheritance
class BaseService {
  void log(String message) => print('LOG: $message');
}

class EmailService extends BaseService {
  Future<void> sendEmail(String to, String subject, String body) async {
    log('Sending email to $to');
    // Email logic
  }
}

class NotificationService extends BaseService {
  Future<void> sendNotification(String message) async {
    log('Sending notification: $message');
    // Notification logic
  }
}

Common Violations

DO NOT Violate These Rules

  1. Don't create god classes that handle multiple responsibilities
  2. Don't use deep inheritance hierarchies
  3. Don't duplicate code across multiple files
  4. Don't over-engineer solutions for future requirements
  5. Don't create unnecessary abstractions early in development
  6. Don't ignore interface segregation - keep interfaces focused
  7. Don't depend on concrete classes - use abstractions
  8. Don't create complex solutions when simple ones work
  9. Don't implement features until they're actually needed
  10. Don't mix UI logic with business logic in widgets

ALWAYS Follow These Rules

  1. Apply SOLID principles consistently in your code
  2. Extract common functionality to avoid duplication
  3. Keep code simple and easy to understand
  4. Implement only current requirements - avoid over-engineering
  5. Use composition over inheritance when possible
  6. Separate concerns properly (UI, business logic, data)
  7. Create focused interfaces with specific purposes
  8. Use dependency injection for loose coupling
  9. Write self-documenting code with clear names
  10. Refactor regularly to maintain code quality