Appearance
🏗️ 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
- Separate UI from business logic - Widgets should only handle UI concerns
- Create dedicated service classes for specific functionalities
- Use separate classes for validation, data transformation, and API calls
- 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
- Use abstract classes for base functionality
- Implement interfaces for different behaviors
- Use composition over inheritance for flexibility
- 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
- Maintain behavioral contracts in inheritance hierarchies
- Ensure subclasses don't weaken base class guarantees
- Use proper method overrides without changing expected behavior
- 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
- Create focused interfaces with specific purposes
- Avoid fat interfaces with too many methods
- Use composition to combine multiple interfaces
- 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
- Depend on abstractions not concrete implementations
- Use dependency injection for loose coupling
- Create interfaces for external dependencies
- 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
- Extract common widgets into reusable components
- Create utility functions for repeated logic
- Use mixins for shared behavior
- 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
- Prefer simple solutions over complex ones
- Use clear, descriptive names for variables and methods
- Avoid unnecessary abstractions in early development
- 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
- Implement only current requirements - avoid over-engineering
- Don't create abstractions until you have multiple implementations
- Avoid premature optimization - optimize when you have performance issues
- 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
- Use mixins for shared behavior
- Compose objects instead of deep inheritance
- Prefer interfaces over abstract base classes
- 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
- Don't create god classes that handle multiple responsibilities
- Don't use deep inheritance hierarchies
- Don't duplicate code across multiple files
- Don't over-engineer solutions for future requirements
- Don't create unnecessary abstractions early in development
- Don't ignore interface segregation - keep interfaces focused
- Don't depend on concrete classes - use abstractions
- Don't create complex solutions when simple ones work
- Don't implement features until they're actually needed
- Don't mix UI logic with business logic in widgets
✅ ALWAYS Follow These Rules
- Apply SOLID principles consistently in your code
- Extract common functionality to avoid duplication
- Keep code simple and easy to understand
- Implement only current requirements - avoid over-engineering
- Use composition over inheritance when possible
- Separate concerns properly (UI, business logic, data)
- Create focused interfaces with specific purposes
- Use dependency injection for loose coupling
- Write self-documenting code with clear names
- Refactor regularly to maintain code quality