Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
Dart is a client-optimized programming language for fast apps on multiple platforms. It is developed by Google and is used to build mobile, desktop, server, and web applications.
Dart, despite being a modern and well-designed language, has several common anti-patterns that can lead to performance issues, maintenance problems, and bugs. Here are the most important anti-patterns to avoid when writing Dart code.
// Anti-pattern: Not using null safety (pre-Dart 2.12)
String getName(User user) {
return user.name; // Might crash if user or user.name is null
}
// Better approach: Use null safety (Dart 2.12+)
String getName(User user) {
return user.name ?? 'Unknown'; // Safe, returns 'Unknown' if name is null
}
// Even better with sound null safety
String getName(User? user) {
return user?.name ?? 'Unknown'; // Safe, handles null user and null name
}
Always use Dart’s null safety features to prevent null reference exceptions. With sound null safety (Dart 2.12+), you can catch null reference errors at compile time rather than runtime.
// Anti-pattern: Callback hell
void fetchUserData() {
fetchUser((user) {
fetchUserPosts(user, (posts) {
fetchPostComments(posts[0], (comments) {
// Process data...
// Deeply nested and hard to follow
});
});
});
}
// Better approach: Use async/await
Future<void> fetchUserData() async {
final user = await fetchUser();
final posts = await fetchUserPosts(user);
final comments = await fetchPostComments(posts[0]);
// Process data...
// Linear and easy to follow
}
Avoid excessive nesting of callbacks. Use async
/await
for more readable and maintainable asynchronous code.
// Anti-pattern: Not using const for immutable objects
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0), // Creates a new instance each build
child: Text('Hello'), // Creates a new instance each build
);
}
// Better approach: Use const constructors
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16.0), // Reused instance
child: const Text('Hello'), // Reused instance
);
}
Use const
constructors for immutable objects to improve performance by reusing instances rather than creating new ones each time.
// Anti-pattern: Excessive setState calls
void updateUserData() {
setState(() {
user.name = 'John';
});
setState(() {
user.age = 30;
});
setState(() {
user.email = 'john@example.com';
});
}
// Better approach: Batch setState calls
void updateUserData() {
setState(() {
user.name = 'John';
user.age = 30;
user.email = 'john@example.com';
});
}
Avoid calling setState()
multiple times in succession. Batch your state changes into a single setState()
call to reduce unnecessary rebuilds.
// Anti-pattern: Not using keys in lists
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index].title),
);
},
);
// Better approach: Use keys for list items
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
key: ValueKey(items[index].id), // Unique key based on item identity
title: Text(items[index].title),
);
},
);
Always use keys when building lists of widgets to help Flutter efficiently update the UI when the list changes.
// Anti-pattern: Ignoring Future results
void saveData() {
uploadData(); // Future result ignored, no error handling
}
// Better approach: Properly handle Futures
Future<void> saveData() async {
try {
await uploadData();
// Handle success
} catch (e) {
// Handle error
print('Error uploading data: $e');
}
}
Don’t ignore the results of Future
s. Either await
them and handle errors, or use .then()
and .catchError()
to process results and handle errors.
// Anti-pattern: Excessive use of dynamic
dynamic processData(dynamic data) {
return data['result'] * 2; // No type checking, potential runtime errors
}
// Better approach: Use specific types
int processData(Map<String, int> data) {
return data['result']! * 2; // Type-safe, compiler can catch errors
}
Avoid using dynamic
type excessively. Use specific types to catch errors at compile time and make your code more self-documenting.
// Anti-pattern: Using positional parameters for complex functions
Widget createButton(String text, Color color, double width, double height, VoidCallback onPressed) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(primary: color),
child: Container(
width: width,
height: height,
child: Text(text),
),
);
}
// Usage is unclear
createButton('Save', Colors.blue, 100, 50, () => saveData());
// Better approach: Use named parameters
Widget createButton({
required String text,
required Color color,
required double width,
required double height,
required VoidCallback onPressed,
}) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(primary: color),
child: Container(
width: width,
height: height,
child: Text(text),
),
);
}
// Usage is clear
createButton(
text: 'Save',
color: Colors.blue,
width: 100,
height: 50,
onPressed: () => saveData(),
);
Use named parameters for functions with multiple parameters to make your code more readable and self-documenting.
// Anti-pattern: Unsafe late initialization
class UserViewModel {
late User user; // Might be accessed before initialization
Future<void> loadUser() async {
user = await fetchUser();
}
String getUserName() {
return user.name; // Might throw if loadUser hasn't completed
}
}
// Better approach: Safe initialization with null safety
class UserViewModel {
User? user; // Explicitly nullable
Future<void> loadUser() async {
user = await fetchUser();
}
String getUserName() {
return user?.name ?? 'Unknown'; // Safe access
}
}
Be careful with late
initialization. Only use late
when you’re certain the variable will be initialized before it’s accessed.
// Anti-pattern: Not using factory constructors for caching
class Configuration {
final String apiKey;
final String baseUrl;
Configuration(this.apiKey, this.baseUrl);
}
// Creates new instance each time
var config1 = Configuration('key', 'https://api.example.com');
var config2 = Configuration('key', 'https://api.example.com');
// Better approach: Use factory constructor for caching
class Configuration {
final String apiKey;
final String baseUrl;
static final Map<String, Configuration> _cache = {};
factory Configuration(String apiKey, String baseUrl) {
final key = '$apiKey-$baseUrl';
return _cache.putIfAbsent(
key,
() => Configuration._internal(apiKey, baseUrl)
);
}
Configuration._internal(this.apiKey, this.baseUrl);
}
Use factory constructors when you need to control instance creation, such as for caching or returning instances of subclasses.
// Anti-pattern: Not cancelling stream subscriptions
class DataService {
StreamSubscription? _subscription;
void startListening() {
_subscription = dataStream.listen((data) {
// Process data
});
}
// No cleanup method
}
// Better approach: Properly manage stream subscriptions
class DataService {
StreamSubscription? _subscription;
void startListening() {
// Cancel existing subscription if any
_subscription?.cancel();
_subscription = dataStream.listen((data) {
// Process data
});
}
void dispose() {
_subscription?.cancel();
_subscription = null;
}
}
Always cancel stream subscriptions when they’re no longer needed to prevent memory leaks and unexpected behavior.
// Anti-pattern: Inadequate error handling
Future<void> fetchData() async {
final response = await http.get(Uri.parse('https://api.example.com/data'));
final data = jsonDecode(response.body); // Might throw
// Process data...
}
// Better approach: Comprehensive error handling
Future<void> fetchData() async {
try {
final response = await http.get(Uri.parse('https://api.example.com/data'));
if (response.statusCode != 200) {
throw HttpException('Failed to fetch data: ${response.statusCode}');
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
// Process data...
} on SocketException catch (e) {
// Handle network errors
print('Network error: $e');
} on FormatException catch (e) {
// Handle JSON parsing errors
print('Invalid response format: $e');
} catch (e) {
// Handle other errors
print('Error fetching data: $e');
}
}
Implement comprehensive error handling to make your code more robust. Handle specific exceptions separately and provide meaningful error messages.
// Anti-pattern: Hard-coded dependencies
class UserRepository {
final ApiClient apiClient = ApiClient();
Future<User> getUser(int id) async {
final data = await apiClient.get('/users/$id');
return User.fromJson(data);
}
}
// Better approach: Dependency injection
class UserRepository {
final ApiClient apiClient;
UserRepository(this.apiClient);
Future<User> getUser(int id) async {
final data = await apiClient.get('/users/$id');
return User.fromJson(data);
}
}
Use dependency injection to make your code more testable and flexible. Pass dependencies to classes rather than creating them internally.
// Anti-pattern: Everything in one file
// main.dart with hundreds of lines of code
// Better approach: Proper code organization
// main.dart
import 'package:my_app/screens/home_screen.dart';
void main() {
runApp(MyApp());
}
// screens/home_screen.dart
import 'package:my_app/widgets/user_list.dart';
import 'package:my_app/repositories/user_repository.dart';
class HomeScreen extends StatelessWidget {
// ...
}
Organize your code into separate files and folders based on functionality. Follow a consistent project structure to make your codebase more maintainable.
// Anti-pattern: Utility functions
String capitalize(String s) {
if (s.isEmpty) return s;
return s[0].toUpperCase() + s.substring(1);
}
// Usage
final name = capitalize(user.name);
// Better approach: Extension methods
extension StringExtensions on String {
String capitalize() {
if (isEmpty) return this;
return this[0].toUpperCase() + substring(1);
}
}
// Usage
final name = user.name.capitalize();
Use extension methods to add functionality to existing classes in a clean and type-safe way.
// Anti-pattern: Passing data through deep widget trees
class GrandparentWidget extends StatefulWidget {
@override
_GrandparentWidgetState createState() => _GrandparentWidgetState();
}
class _GrandparentWidgetState extends State<GrandparentWidget> {
String userData = 'User data';
@override
Widget build(BuildContext context) {
return ParentWidget(userData: userData);
}
}
class ParentWidget extends StatelessWidget {
final String userData;
ParentWidget({required this.userData});
@override
Widget build(BuildContext context) {
return ChildWidget(userData: userData);
}
}
class ChildWidget extends StatelessWidget {
final String userData;
ChildWidget({required this.userData});
@override
Widget build(BuildContext context) {
return Text(userData);
}
}
// Better approach: Use proper state management
// Using Provider as an example
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => UserModel(),
child: MyApp(),
),
);
}
class UserModel extends ChangeNotifier {
String _userData = 'User data';
String get userData => _userData;
void updateUserData(String newData) {
_userData = newData;
notifyListeners();
}
}
class ChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final userData = context.watch<UserModel>().userData;
return Text(userData);
}
}
Use proper state management solutions like Provider, Riverpod, Bloc, or GetX instead of passing data through deep widget trees.
// Anti-pattern: Mutable model classes
class User {
String name;
int age;
User(this.name, this.age);
void updateAge(int newAge) {
age = newAge;
}
}
// Usage
final user = User('John', 30);
user.updateAge(31); // Mutating the object
// Better approach: Immutable model classes
class User {
final String name;
final int age;
const User(this.name, this.age);
User copyWith({String? name, int? age}) {
return User(
name ?? this.name,
age ?? this.age,
);
}
}
// Usage
final user = User('John', 30);
final updatedUser = user.copyWith(age: 31); // Creating a new object
Use immutable model classes with copyWith
methods to create modified copies instead of mutating objects directly.
// Anti-pattern: Manual JSON serialization
class User {
final String name;
final int age;
User(this.name, this.age);
factory User.fromJson(Map<String, dynamic> json) {
return User(
json['name'] as String,
json['age'] as int,
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'age': age,
};
}
}
// Better approach: Use code generation
// user.dart
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final String name;
final int age;
User(this.name, this.age);
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
Use code generation for repetitive tasks like JSON serialization, immutable classes, and equality implementations.
// Anti-pattern: Monolithic widgets
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Column(
children: [
// User header with avatar and name
Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(backgroundImage: NetworkImage('...')),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('John Doe', style: TextStyle(fontSize: 20)),
Text('john@example.com'),
],
),
],
),
),
// Stats section
// ... many more nested widgets
],
),
);
}
}
// Better approach: Proper widget composition
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Column(
children: [
UserHeader(name: 'John Doe', email: 'john@example.com'),
UserStats(),
RecentActivity(),
],
),
);
}
}
class UserHeader extends StatelessWidget {
final String name;
final String email;
const UserHeader({required this.name, required this.email});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(backgroundImage: NetworkImage('...')),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: TextStyle(fontSize: 20)),
Text(email),
],
),
],
),
);
}
}
Break down large widgets into smaller, reusable components to improve readability, maintainability, and testability.