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
🔹 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));
});
});
}