In previous lessons, we learned about error handling and logging, mastering core techniques for making programs more robust. Today we'll focus on the fundamental guarantees of code quality — code conventions and static analysis. Consistent code not only improves team collaboration efficiency but also reduces potential errors, while static analysis tools help us find issues before code runs, serving as important safeguards for high-quality code.
I. Why Do We Need Code Conventions?
In individual projects, code style might only affect yourself, but in team collaboration, inconsistent code styles can lead to:
- Extra mental effort when reading others' code due to style differences
- Inefficient code reviews with debates focusing on formatting rather than logic
- Numerous meaningless "formatting change" commits in version control
- Hidden logical errors (messy formatting Covering up code problems)
The official Dart documentation provides a detailed Style Guide that defines a set of consistent code conventions, aiming to make all Dart code look like it was written by the same person.
II. Core Content of Dart's Official Style Guide
- Naming Conventions Naming is the most basic and important part of code conventions. Dart has clear naming requirements for different elements:
Element Type | Naming Style | Example | Description |
---|---|---|---|
Classes, enums, mixins | PascalCase | class UserService {} |
Each word starts with a capital letter, no underscores |
Functions, methods, variables | camelCase | void getUserInfo() {} |
First word lowercase, subsequent words start with capital letters |
Constants | UPPER_CASE_WITH_UNDERSCORES | const MAX_SIZE = 100; |
Words separated by underscores, emphasizing immutability |
Libraries, packages, directories | lowercase_with_underscores | user_service.dart |
Avoid uppercase letters for filesystem compatibility |
Private members | camelCase with leading underscore | int _count = 0; |
Underscore prefix indicates privacy, visible only within the library |
Examples:
// Correct class naming
class OrderProcessor {
// Correct private variable
int _totalAmount = 0;
// Correct method naming
void calculateTotal() {
// Implementation logic
}
}
// Correct constant naming
const DEFAULT_TIMEOUT = 5000;
const MAX_RETRY_COUNT = 3;
// Correct library import
import 'data/user_repository.dart';
Note: Dart doesn't have a true "private" keyword. Instead, privacy is convention-based using underscore prefixes — a "soft constraint".
2. Code Formatting Rules
(1) Indentation and Line Breaks
- Use 2 spaces for indentation (Tabs are not recommended)
- Keep line length between 80-120 characters (break longer lines)
- Opening braces { stay on the same line as declarations
- Closing braces } go on their own line, aligned with the corresponding declaration
Examples:
// Correct formatting
if (user.isActive) {
sendNotification(user);
} else {
print('User is inactive');
}
// Line break for long expressions
final result = calculate(
firstParameter,
secondParameter,
thirdParameter,
);
(2) Space Usage
- Add space after keywords (if, for, return, etc.)
- Add space after commas ,
- Add spaces around operators (+, =, ==, etc.)
- Add spaces between parameters in function parameter lists
Examples:
// Correct space usage
void updateUser(String name, int age) {
if (age > 18) {
this.name = name;
this.age = age;
}
}
// Incorrect example (missing necessary spaces)
void updateUser(String name,int age){
if(age>18){
this.name=name;
}
}
(3) Blank Line Usage
- Leave one blank line between functions/methods
- Leave blank lines between logical blocks (e.g., between variable declarations and business logic)
- Avoid consecutive blank lines
Examples:
class UserManager {
final UserRepository _repository;
UserManager(this._repository);
// Blank line between methods
Future<User> getUser(String id) async {
return await _repository.fetchUser(id);
}
// Blank line between methods
Future<void> saveUser(User user) async {
// Blank line between logical blocks
if (user.isValid) {
await _repository.insertUser(user);
} else {
throw InvalidUserException();
}
}
}
3. Comment Conventions
- Single-line comments: Use // with a space between // and comment content
- Documentation comments: Use /// (single-line) or /** ... */ (multi-line) for describing classes, methods, and variables. These can be parsed by DartDoc to generate documentation.
- Comment placement: Single-line comments usually go above the code they describe or to the right of the same line (but avoid long end-of-line comments)
Examples:
/// User service class that handles user-related operations
///
/// Provides user query, creation, update, and other functions,
/// relying on [UserRepository] for data persistence.
class UserService {
final UserRepository _repository;
/// Creates a [UserService] instance
///
/// The [repository] parameter must not be null, otherwise
/// an [ArgumentError] will be thrown.
UserService(this._repository) {
if (_repository == null) {
throw ArgumentError('repository must not be null');
}
}
/// Retrieves user information
///
/// [userId]: Unique user identifier
/// Returns: [Future<User>] containing user information
Future<User> getUser(String userId) async {
// Check user ID format (temporary comment, should be moved to validation method)
if (userId.isEmpty) {
throw ArgumentError('userId cannot be empty');
}
return await _repository.findById(userId);
}
}
4. Other Important Conventions
- Avoid global variables: Prefer class member variables or local variables
- Prefer final and const: Clearly indicate whether variables are mutable for better performance
- Avoid dynamic type: Specify concrete types unless necessary to leverage Dart's type checking
- Collection initialization: Prefer literal syntax ([] instead of List(), {} instead of Map())
- Condition checks: Avoid unnecessary == true or == false (use if (isValid) instead of if (isValid == true))
III. Automatic Formatting: dart format
Manually following all formatting conventions is tedious. Dart provides the official formatting tool dart format that can automatically adjust code to match the style guide.
1. Basic Usage
Execute these commands in your project root directory:
# Format a specific file
dart format lib/main.dart
# Format an entire directory (commonly used)
dart format lib/
# Format all Dart files in the current directory
dart format .
The tool will directly modify file contents to format code according to standard styles.
2. Dry Run Mode
To check which files need formatting without actually modifying them, use the --dry-run option:
dart format --dry-run lib/
Example output:
Formatted lib/src/user_service.dart
Formatted lib/main.dart
The displayed files are those that need formatting.
3. Integration in Development Tools
Mainstream Dart/Flutter development tools have built-in dart format support:
-
VS Code:
- After installing Dart and Flutter extensions, formatting happens automatically on save by default
- Manual trigger: Right-click menu > "Format Document" or use shortcut Shift+Alt+F (Windows) / Shift+Option+F (Mac)
-
Android Studio/IntelliJ:
- Install Dart and Flutter plugins
- Configure auto-formatting: File > Settings > Languages & Frameworks > Flutter > Formatting > Format on save
- Manual trigger: Right-click menu > "Reformat Code" or use shortcut Ctrl+Alt+L (Windows) / Option+Command+L (Mac)
Recommendation: In team projects, configure "format on save" uniformly to avoid formatting inconsistencies.
IV. Static Analysis: dart analyze
While dart format mainly handles code formatting issues, dart analyze is a static code analysis tool that detects potential errors, style issues, and performance concerns before code runs.
1. Basic Usage
Execute in your project root directory:
dart analyze
The tool will analyze all Dart files in the project and output a list of issues, example:
error • lib/src/user_service.dart:15:7 • The parameter 'userId' can't have a value of 'null' because of its type 'String', but the implicit default value is 'null'. • missing_default_value_for_parameter
info • lib/utils/validator.dart:23:10 • This function has a return type of 'bool', but doesn't end with a return statement. • missing_return
warning • lib/main.dart:45:20 • Unused local variable 'temp'. • unused_local_variable
Each issue includes:
- Severity (error > warning > info)
- File path and line number
- Issue description
- Issue code (rule name)
2. Issue Severity Levels
- error: Problems that may prevent code from running (e.g., type mismatches, undefined variables)
- warning: Issues that won't cause compilation errors but are likely logical errors (e.g., unused variables, dead code)
- info: Code that runs correctly but doesn't follow best practices (e.g., unnecessary type casts)
3. Configuring Analysis Rules
You can customize analysis rules through the analysis_options.yaml file in your project root:
# analysis_options.yaml
include: package:lints/recommended.yaml
linter:
rules:
# Enable additional rules
prefer_const_constructors: true
avoid_print: true
# Disable rules that don't apply
omit_local_variable_types: false
analyzer:
# Exclude files that don't need analysis
exclude:
- '**/*.g.dart' # Exclude code-generated files
- 'test/mocks/**'
include: package:lints/recommended.yaml imports the officially recommended rule set, containing most useful checks. Common rules:
- prefer_const_constructors: Recommends using const constructors (performance optimization)
- avoid_print: Discourages using print (recommends logging tools instead)
- unused_import: Detects unused imports
- non_constant_identifier_names: Checks if variable names follow camelCase convention
- dead_code: Detects code that will never execute
4. Real-time Checking in Development Tools
VS Code and Android Studio run static analysis in real-time and display issues in the editor:
- Errors: Red wavy underlines
- Warnings: Yellow wavy underlines
- Info: Gray wavy underlines
Hovering over the wavy lines shows issue descriptions and fix suggestions.
5. Integration with CI/CD
In team development, it's recommended to integrate dart analyze into your continuous integration (CI) process to ensure all submitted code passes static checks:
Example GitHub Actions configuration (.github/workflows/analyze.yml):
name: Analyze
on: [pull_request, push]
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dart-lang/setup-dart@v1
- run: dart pub get
- run: dart analyze
This way, static analysis runs automatically with each code submission or PR creation, and merging is blocked if it fails.
V. Other Useful Tools
1. The lints Package
The lints package provides officially maintained code check rule sets, forming the basis of analysis_options.yaml:
- Add the dependency:
dev_dependencies:
lints: ^6.0.0
2 . Import in analysis_options.yaml:
# Basic rule set (suitable for all projects)
include: package:lints/core.yaml
# Recommended rule set (adds more best practices to core)
# include: package:lints/recommended.yaml
2. dartdoc for Documentation Generation
Generates HTML documentation from code documentation comments (/// or /** ... */):
# Install dartdoc (if not installed)
dart pub global activate dartdoc
# Generate documentation
dartdoc
Generated documentation is in the doc/api directory and can be viewed in a browser.
3. flutter_lints (For Flutter Projects)
Flutter projects should use the flutter_lints package, which includes Flutter-specific check rules:
dev_dependencies:
flutter_lints: ^6.0.0
# analysis_options.yaml
include: package:flutter_lints/flutter.yaml
VI. Comprehensive Practice: Example of Conventional Code
Below is an example of code that follows Dart conventions, including naming, formatting, comments, and other best practices:
/// Handles network requests related to users
///
/// Encapsulates API calls for user registration, login,
/// information updates, etc. Internally uses [_dio] for
/// network requests and [_logger] for logging.
class UserApiClient {
final Dio _dio;
final Logger _logger;
/// Creates a [UserApiClient] instance
///
/// [dio]: Configured Dio instance, must not be null
/// [logger]: Logger instance, must not be null
UserApiClient({
required Dio dio,
required Logger logger,
}) : _dio = dio,
_logger = logger;
/// User login
///
/// [username]: User name (non-null)
/// [password]: Password (non-null, at least 6 characters)
/// Returns: [Future<LoginResponse>] containing login result
/// May throw [NetworkException] or [ApiException]
Future<LoginResponse> login({
required String username,
required String password,
}) async {
try {
_logger.i('User login', extra: {'username': username});
final response = await _dio.post(
'/auth/login',
data: {
'username': username,
'password': password,
},
);
return LoginResponse.fromJson(response.data);
} on DioException catch (e) {
_logger.e('Login failed', error: e);
throw NetworkException.fromDioError(e);
} catch (e) {
_logger.e('Login processing failed', error: e);
throw ApiException('An error occurred during login');
}
}
// Other methods...
}
Analysis of convention points in the above code:
- Class name UserApiClient uses PascalCase, clearly expressing its function
- Private variables _dio and _logger use underscore prefixes
- Method name login uses camelCase and starts with a verb
- Detailed documentation comments explaining functionality, parameters, return values, and exceptions
- Reasonable blank lines separating logical blocks
- Function parameters use named parameters with required for clear required items
- Clear exception handling using custom exceptions
Top comments (0)