Dart Best Practices

Professional guidelines for writing efficient, maintainable Dart code

⭐ What are Dart Best Practices?

Dart best practices are proven techniques and patterns that help developers write clean, efficient, secure, and maintainable code while avoiding common pitfalls and performance issues.


// Best practice example
class UserService {
  final ApiClient _apiClient;
  
  UserService(this._apiClient);
  
  Future getUser(int id) async {
    try {
      final response = await _apiClient.get('/users/$id');
      return User.fromJson(response.data);
    } catch (e) {
      print('Error fetching user: $e');
      return null;
    }
  }
}
                                    

Best Practices Applied:

✅ Dependency injection

✅ Proper error handling

✅ Null safety

✅ Clear naming

Practice Categories

🛡️

Null Safety

Handle null values properly

String? name;
print(name?.toUpperCase() ?? 'Unknown');

Performance

Write efficient code

// Use const constructors
const Text('Hello World');
// Avoid rebuilding widgets
🔒

Security

Protect against vulnerabilities

// Validate input
if (email.contains('@')) {
  processEmail(email);
}
🧹

Clean Code

Maintainable and readable

// Single responsibility
class EmailValidator {
  bool isValid(String email) => /* ... */;
}

🔹 Null Safety Best Practices

Handle null values safely and effectively:

🔸 Use Null-Aware Operators

// ✅ Good - null-aware operators
String? userName;
print(userName?.toUpperCase() ?? 'Guest');

List? items;
print('Items count: ${items?.length ?? 0}');

// Null-aware assignment
userName ??= 'Default User';

// ❌ Bad - manual null checking
if (userName != null) {
  print(userName.toUpperCase());
} else {
  print('Guest');
}

🔸 Prefer Non-Nullable Types

// ✅ Good - non-nullable when possible
class User {
  final String name;        // Required, non-null
  final String email;       // Required, non-null
  final String? phone;      // Optional, nullable
  
  User({required this.name, required this.email, this.phone});
}

// ✅ Good - factory constructor for validation
factory User.fromJson(Map json) {
  return User(
    name: json['name'] as String,
    email: json['email'] as String,
    phone: json['phone'] as String?,
  );
}

🔹 Performance Best Practices

Write code that runs efficiently:

🔸 Use Const Constructors

// ✅ Good - const constructors save memory
class AppConfig {
  final String appName;
  final String version;
  
  const AppConfig({required this.appName, required this.version});
}

const config = AppConfig(appName: 'MyApp', version: '1.0.0');

// ✅ Good - const collections
const List supportedLanguages = ['en', 'es', 'fr'];
const Map errorMessages = {
  'invalid_email': 'Please enter a valid email',
  'required_field': 'This field is required',
};

🔸 Avoid Expensive Operations in Loops

// ✅ Good - calculate once outside loop
List items = ['apple', 'banana', 'cherry'];
int itemCount = items.length;
for (int i = 0; i < itemCount; i++) {
  print('Item $i: ${items[i]}');
}

// ✅ Better - use for-in loop
for (String item in items) {
  print('Item: $item');
}

// ❌ Bad - expensive operation in loop condition
for (int i = 0; i < items.length; i++) {
  print('Item $i: ${items[i]}');
}

🔸 Use Appropriate Data Structures

// ✅ Good - Set for unique items and fast lookups
Set uniqueIds = {'id1', 'id2', 'id3'};
if (uniqueIds.contains('id1')) {
  print('Found ID');
}

// ✅ Good - Map for key-value relationships
Map userCache = {};
User? getUser(String id) => userCache[id];

// ❌ Bad - List for lookups (O(n) instead of O(1))
List ids = ['id1', 'id2', 'id3'];
if (ids.contains('id1')) {  // Slow for large lists
  print('Found ID');
}

🔹 Error Handling Best Practices

Handle errors gracefully and informatively:

🔸 Use Try-Catch Appropriately

// ✅ Good - specific error handling
Future fetchUser(int id) async {
  try {
    final response = await http.get(Uri.parse('/api/users/$id'));
    if (response.statusCode == 200) {
      return User.fromJson(jsonDecode(response.body));
    } else {
      throw HttpException('Failed to load user: ${response.statusCode}');
    }
  } on SocketException {
    throw NetworkException('No internet connection');
  } on FormatException {
    throw DataException('Invalid user data format');
  } catch (e) {
    throw UnknownException('Unexpected error: $e');
  }
}

// ✅ Good - handle errors at call site
void loadUser() async {
  try {
    final user = await fetchUser(123);
    print('Loaded user: ${user.name}');
  } on NetworkException catch (e) {
    showErrorMessage('Please check your internet connection');
  } on DataException catch (e) {
    showErrorMessage('Data error occurred');
  } catch (e) {
    showErrorMessage('Something went wrong');
  }
}

🔹 Code Organization Best Practices

Structure your code for maintainability:

🔸 Single Responsibility Principle

// ✅ Good - each class has one responsibility
class EmailValidator {
  bool isValid(String email) {
    return email.contains('@') && email.contains('.');
  }
}

class UserRepository {
  Future save(User user) async {
    // Save user to database
  }
  
  Future findById(int id) async {
    // Find user by ID
  }
}

class UserService {
  final UserRepository _repository;
  final EmailValidator _validator;
  
  UserService(this._repository, this._validator);
  
  Future createUser(String name, String email) async {
    if (!_validator.isValid(email)) {
      throw ArgumentError('Invalid email format');
    }
    
    final user = User(name: name, email: email);
    return await _repository.save(user);
  }
}

🔸 Use Dependency Injection

// ✅ Good - dependencies injected
class ApiService {
  final HttpClient _httpClient;
  final String _baseUrl;
  
  ApiService(this._httpClient, this._baseUrl);
  
  Future> get(String endpoint) async {
    final response = await _httpClient.get(Uri.parse('$_baseUrl$endpoint'));
    return jsonDecode(response.body);
  }
}

// ✅ Good - easy to test and configure
void main() {
  final httpClient = HttpClient();
  final apiService = ApiService(httpClient, 'https://api.example.com');
}

🔹 Security Best Practices

Protect your application from common vulnerabilities:

🔸 Input Validation

// ✅ Good - validate all inputs
class UserInput {
  static String? validateEmail(String? email) {
    if (email == null || email.isEmpty) {
      return 'Email is required';
    }
    if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email)) {
      return 'Please enter a valid email';
    }
    return null;
  }
  
  static String? validatePassword(String? password) {
    if (password == null || password.isEmpty) {
      return 'Password is required';
    }
    if (password.length < 8) {
      return 'Password must be at least 8 characters';
    }
    return null;
  }
}

// ✅ Good - sanitize data before use
String sanitizeInput(String input) {
  return input.trim().replaceAll(RegExp(r'[<>]'), '');
}

🔸 Secure Data Storage

// ✅ Good - don't store sensitive data in plain text
class SecureStorage {
  static const String _keyPrefix = 'secure_';
  
  static Future storeToken(String token) async {
    // Use flutter_secure_storage or similar
    await secureStorage.write(key: '${_keyPrefix}auth_token', value: token);
  }
  
  static Future getToken() async {
    return await secureStorage.read(key: '${_keyPrefix}auth_token');
  }
}

// ❌ Bad - storing sensitive data in SharedPreferences
// SharedPreferences.getInstance().then((prefs) {
//   prefs.setString('password', userPassword); // Never do this!
// });

🔹 Testing Best Practices

Write testable and well-tested code:

// ✅ Good - testable code with dependency injection
class Calculator {
  double add(double a, double b) => a + b;
  double multiply(double a, double b) => a * b;
}

// ✅ Good - comprehensive tests
import 'package:test/test.dart';

void main() {
  group('Calculator', () {
    late Calculator calculator;
    
    setUp(() {
      calculator = Calculator();
    });
    
    test('should add two numbers correctly', () {
      expect(calculator.add(2, 3), equals(5));
      expect(calculator.add(-1, 1), equals(0));
    });
    
    test('should multiply two numbers correctly', () {
      expect(calculator.multiply(3, 4), equals(12));
      expect(calculator.multiply(0, 5), equals(0));
    });
  });
}

🧠 Test Your Knowledge

Which operator should you use for null-safe property access?