mirror of https://github.com/flutter/samples.git
Dart 3.9 / Flutter 3.35 [first LLM release] (#2714)
I got carried away with Gemini and basically rewrote CI and the release process for the new LLM reality. This work was largely completed by Gemini. - Bump all SDK versions to the current beta (3.9.0-0) - Run `flutter channel beta` - Wrote `ci_script.dart` to replace the bash scripts - Converted repository to pub workspace #2499 - Added llm.md and release.md - Added redirect for deprecated Samples Index ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I have added sample code updates to the [changelog]. - [x] I updated/added relevant documentation (doc comments with `///`).pull/2723/head
parent
0aa5415d5e
commit
2999d738b8
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"dart": {
|
||||||
|
"command": "dart",
|
||||||
|
"args": [
|
||||||
|
"mcp-server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contextFileName": "/.prompts/llm.md"
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
# Code Health and Style Guide Analysis
|
||||||
|
|
||||||
|
This document provides instructions for an AI assistant to analyze this repository against the Dart and Flutter style guide (`.prompts/llm.md`) and log opportunities for improvement.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
The AI assistant must follow these steps exactly:
|
||||||
|
|
||||||
|
### 1. Setup
|
||||||
|
|
||||||
|
1. **Create Log Directory**: If it does not already exist, create a directory named `logs` in the repository root.
|
||||||
|
2. **Create Log File**: Create a new file inside the `logs` directory named `YYYY-MM-DD_HH-MM-SS-freshness-scores.md`, where `YYYY-MM-DD_HH-MM-SS` is the current timestamp.
|
||||||
|
|
||||||
|
### 2. Project Identification
|
||||||
|
|
||||||
|
1. **Find Projects**: Identify all sample projects by searching for `pubspec.yaml` files within the repository. Each directory containing a `pubspec.yaml` should be considered a separate project.
|
||||||
|
2. **Create a Queue**: Compile a list of the absolute paths to these project directories to process them one by one.
|
||||||
|
|
||||||
|
### 3. Analysis Loop
|
||||||
|
|
||||||
|
For each project directory identified in the previous step, perform the following:
|
||||||
|
|
||||||
|
1. **Check for Dart Files**:
|
||||||
|
* Verify that the project directory contains at least one `.dart` file.
|
||||||
|
* If not, add an entry to the log file stating that the project was skipped for this reason and proceed to the next project.
|
||||||
|
|
||||||
|
2. **Analyze Git History**:
|
||||||
|
* Run `git log` to find the most recent commit to a `.dart` file within that project's directory made by a human (i.e., not a bot).
|
||||||
|
* **Command**:
|
||||||
|
```bash
|
||||||
|
git log -1 --author='^(?!.*bot).*$' --pretty="format:%ad" --date=short -- ./**/*.dart
|
||||||
|
```
|
||||||
|
*(Note: This command should be run from within the project's directory).*
|
||||||
|
* **Handle No Commits**: If the command returns no output, note "No human commits found" for the log. If there are no commits at all, note that as well.
|
||||||
|
|
||||||
|
3. **Read and Assess Code**:
|
||||||
|
* Read all `.dart` files within the project directory (recursively).
|
||||||
|
* Compare the code against the rules and patterns defined in `.prompts/llm.md`.
|
||||||
|
* The assessment must focus on:
|
||||||
|
* Code organization and structure.
|
||||||
|
* Adherence to naming conventions.
|
||||||
|
* Proper use of Flutter and Dart patterns (e.g., `const` constructors, collection literals).
|
||||||
|
* State management best practices.
|
||||||
|
* Clarity, readability, and documentation.
|
||||||
|
|
||||||
|
4. **Log Findings**:
|
||||||
|
* Append a new entry to the log file using the following Markdown template. Ensure all fields are filled out.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
**Sample**: `path/to/sample`
|
||||||
|
**Last Human Commit**: `YYYY-MM-DD` or "No human commits found"
|
||||||
|
**Assessment Date**: `YYYY-MM-DD`
|
||||||
|
|
||||||
|
**Summary of Style Guide Adherence**:
|
||||||
|
A brief, one-paragraph summary of how well the project adheres to the style guide. Mention its strengths and weaknesses in general terms.
|
||||||
|
|
||||||
|
**Opportunities for Improvement**:
|
||||||
|
- A concrete, actionable bullet point describing a specific area for improvement.
|
||||||
|
- Another actionable bullet point.
|
||||||
|
- A third bullet point, if applicable. Focus on providing clear, specific, and helpful recommendations.
|
||||||
|
---
|
||||||
|
```
|
@ -0,0 +1,691 @@
|
|||||||
|
You are an expert Dart and Flutter developer on the Flutter team at Google. Your code must adhere to this style guide.
|
||||||
|
|
||||||
|
## Core Philosophy
|
||||||
|
|
||||||
|
- **Follow Effective Dart guidelines.**
|
||||||
|
- **Optimize for readability**: Write code that is easy to understand and maintain
|
||||||
|
- **Write detailed documentation**: Every public API should be well-documented
|
||||||
|
- **Keep one source of truth**: Avoid duplicating state across your application
|
||||||
|
- **Design APIs from the developer's perspective**: Consider how the API will be used
|
||||||
|
- **Create useful error messages**: Error messages should guide developers toward solutions
|
||||||
|
- **Write tests first**: When fixing bugs, write failing tests first, then fix the bug
|
||||||
|
- **Avoid workarounds**: Take time to fix problems correctly rather than implementing temporary solutions
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
### Identifier Types (Official Dart Guidelines)
|
||||||
|
|
||||||
|
#### UpperCamelCase
|
||||||
|
- **Classes**: `MyWidget`, `UserRepository`, `HttpClient`
|
||||||
|
- **Enum types**: `ButtonType`, `AnimationState`, `ConnectionState`
|
||||||
|
- **Typedefs**: `EventCallback`, `ValidatorFunction`
|
||||||
|
- **Type parameters**: `<T>`, `<K, V>`, `<TModel>`
|
||||||
|
- **Extensions**: `StringExtension`, `MyFancyList`, `SmartIterable`
|
||||||
|
|
||||||
|
#### lowerCamelCase
|
||||||
|
- **Variables**: `userName`, `isLoading`, `itemCount`
|
||||||
|
- **Parameters**: `onPressed`, `itemBuilder`, `scrollDirection`
|
||||||
|
- **Class members**: `_privateField`, `publicMethod`
|
||||||
|
- **Top-level functions**: `buildWidget`, `validateInput`
|
||||||
|
- **Constants**: `defaultPadding`, `maxRetries`, `pi` (prefer over SCREAMING_CAPS)
|
||||||
|
|
||||||
|
#### lowercase_with_underscores
|
||||||
|
- **Packages**: `my_package`, `http_client`
|
||||||
|
- **Directories**: `lib/widgets/custom`, `test/unit_tests`
|
||||||
|
- **Source files**: `user_profile_widget.dart`, `file_system.dart`
|
||||||
|
- **Import prefixes**: `import 'dart:math' as math;`, `import 'package:foo/foo.dart' as foo_lib;`
|
||||||
|
|
||||||
|
### Flutter-Specific Guidelines
|
||||||
|
- **Global constants**: Begin with prefix "k": `kDefaultTimeout`, `kMaxItems`
|
||||||
|
- **Avoid abbreviations**: Use `button` instead of `btn`, `number` instead of `num`
|
||||||
|
- **Acronyms**: Capitalize acronyms longer than two letters like regular words: `HttpClient` not `HTTPClient`
|
||||||
|
- **Unused parameters**: Use wildcards (`_`) for unused callback parameters
|
||||||
|
- **Private identifiers**: Use leading underscores only for truly private members
|
||||||
|
- **Avoid Hungarian notation**: Don't use prefix letters like `strName` or `intCount`
|
||||||
|
|
||||||
|
## Code Organization and Structure
|
||||||
|
|
||||||
|
- **Define related classes in the same library.**
|
||||||
|
- **For large libraries, group smaller libraries by exporting them in a top-level library.**
|
||||||
|
- **Group related libraries in the same folder.**
|
||||||
|
|
||||||
|
### Import Ordering (Strict Dart Convention)
|
||||||
|
```dart
|
||||||
|
// 1. Dart core libraries (alphabetically)
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
// 2. Flutter and package imports (alphabetically)
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
// 3. Relative imports (alphabetically)
|
||||||
|
import '../models/user.dart';
|
||||||
|
import '../widgets/custom_button.dart';
|
||||||
|
import 'user_repository.dart';
|
||||||
|
|
||||||
|
// 4. Exports (if any, in separate section)
|
||||||
|
export 'src/my_library.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Class Member Ordering (Flutter Team Convention)
|
||||||
|
```dart
|
||||||
|
class MyWidget extends StatefulWidget {
|
||||||
|
// 1. Constructors first
|
||||||
|
const MyWidget({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
this.isEnabled = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Public constants
|
||||||
|
static const double kDefaultHeight = 48.0;
|
||||||
|
|
||||||
|
// 3. Public fields
|
||||||
|
final String title;
|
||||||
|
final bool isEnabled;
|
||||||
|
|
||||||
|
// 4. Private constants
|
||||||
|
static const double _defaultPadding = 16.0;
|
||||||
|
|
||||||
|
// 5. Private fields
|
||||||
|
String? _cachedValue;
|
||||||
|
|
||||||
|
// 6. Getters and setters
|
||||||
|
bool get isDisabled => !isEnabled;
|
||||||
|
|
||||||
|
// 7. Public methods
|
||||||
|
@override
|
||||||
|
State<MyWidget> createState() => _MyWidgetState();
|
||||||
|
|
||||||
|
// 8. Private methods
|
||||||
|
void _updateCache() {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formatting and Style Rules
|
||||||
|
|
||||||
|
### Line Length and Basic Formatting
|
||||||
|
- **Always use `dart format`** for automatic code formatting
|
||||||
|
- **Prefer lines 80 characters or fewer** for better readability
|
||||||
|
- **Maximum 100 characters for comments** (Flutter team preference)
|
||||||
|
- **Always use curly braces** for all flow control statements
|
||||||
|
- **Don't add trailing comments**
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Good - always use braces
|
||||||
|
if (condition) {
|
||||||
|
print('hello');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad - missing braces
|
||||||
|
if (condition) print('hello');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function and Method Formatting
|
||||||
|
```dart
|
||||||
|
// Use "=>" for short functions and getters
|
||||||
|
String get displayName => '$firstName $lastName';
|
||||||
|
int get age => DateTime.now().year - birthYear;
|
||||||
|
|
||||||
|
// Use braces for longer functions
|
||||||
|
String formatUserName(String first, String last) {
|
||||||
|
if (first.isEmpty && last.isEmpty) {
|
||||||
|
return 'Anonymous';
|
||||||
|
}
|
||||||
|
return '$first $last'.trim();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dart-Specific Formatting Rules
|
||||||
|
- **Prefer `+=` over `++`** for increment operations: `counter += 1;`
|
||||||
|
- **Use collection literals** when possible: `<int>[]` instead of `List<int>()`
|
||||||
|
- **Adjacent string literals** for concatenation:
|
||||||
|
```dart
|
||||||
|
var longMessage = 'This is a very long message '
|
||||||
|
'that spans multiple lines.';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Annotations and Safety
|
||||||
|
|
||||||
|
### Type Annotations (Required by Flutter Team)
|
||||||
|
```dart
|
||||||
|
// DO annotate return types on function declarations
|
||||||
|
String formatName(String first, String last) {
|
||||||
|
return '$first $last';
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO annotate parameter types on function declarations
|
||||||
|
void updateUser(String id, Map<String, dynamic> data) {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO use explicit types for variables (avoid var/dynamic)
|
||||||
|
final List<User> users = [];
|
||||||
|
final Map<String, int> scores = {};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Null Safety Best Practices
|
||||||
|
```dart
|
||||||
|
// DON'T explicitly initialize variables to null
|
||||||
|
String? name; // Good
|
||||||
|
String? name = null; // Bad
|
||||||
|
|
||||||
|
// DO use proper null-aware operators
|
||||||
|
final displayName = user?.name ?? 'Unknown';
|
||||||
|
|
||||||
|
// DO use late for non-nullable fields initialized later
|
||||||
|
class MyWidget extends StatefulWidget {
|
||||||
|
late final AnimationController controller;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Future and Async Types
|
||||||
|
```dart
|
||||||
|
// DO use Future<void> for async functions that don't return values
|
||||||
|
Future<void> saveUser(User user) async {
|
||||||
|
await repository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO prefer async/await over raw futures
|
||||||
|
Future<List<User>> loadUsers() async {
|
||||||
|
final response = await http.get('/api/users');
|
||||||
|
return parseUsers(response.body);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation Standards
|
||||||
|
|
||||||
|
### Documentation Comments (Dart Standard)
|
||||||
|
```dart
|
||||||
|
/// A custom button widget that provides enhanced styling and behavior.
|
||||||
|
///
|
||||||
|
/// This widget wraps Flutter's [ElevatedButton] and adds additional
|
||||||
|
/// functionality like loading states and custom styling.
|
||||||
|
///
|
||||||
|
/// The [onPressed] callback is called when the button is tapped.
|
||||||
|
/// Set [isEnabled] to false to disable the button.
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// ```dart
|
||||||
|
/// CustomButton(
|
||||||
|
/// onPressed: () => print('Pressed'),
|
||||||
|
/// child: Text('Click me'),
|
||||||
|
/// isEnabled: true,
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
class CustomButton extends StatelessWidget {
|
||||||
|
/// Creates a custom button.
|
||||||
|
///
|
||||||
|
/// The [onPressed] and [child] parameters are required.
|
||||||
|
/// The [isEnabled] parameter defaults to true.
|
||||||
|
const CustomButton({
|
||||||
|
super.key,
|
||||||
|
required this.onPressed,
|
||||||
|
required this.child,
|
||||||
|
this.isEnabled = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Called when the button is pressed.
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
/// The widget to display inside the button.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Whether the button is enabled for interaction.
|
||||||
|
final bool isEnabled;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method Documentation Requirements
|
||||||
|
```dart
|
||||||
|
/// Validates the given email address format.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the [email] is valid according to RFC standards.
|
||||||
|
/// Returns `false` if the format is invalid.
|
||||||
|
///
|
||||||
|
/// Throws [ArgumentError] if [email] is null or empty.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// final isValid = validateEmail('user@example.com'); // true
|
||||||
|
/// ```
|
||||||
|
bool validateEmail(String email) {
|
||||||
|
if (email.isEmpty) {
|
||||||
|
throw ArgumentError('Email cannot be empty');
|
||||||
|
}
|
||||||
|
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flutter-Specific Patterns
|
||||||
|
|
||||||
|
- **Prefer composition over inheritance.**
|
||||||
|
- **Avoid large build() methods by creating smaller Widgets with a reusable API.**
|
||||||
|
- **Use small, private Widget classes instead of private helper methods that return a Widget.**
|
||||||
|
- **Use lazy lists wherever possible using ListView.builder.**
|
||||||
|
|
||||||
|
### Widget Construction
|
||||||
|
```dart
|
||||||
|
class CustomCard extends StatelessWidget {
|
||||||
|
const CustomCard({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.content,
|
||||||
|
this.elevation = 2.0,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final Widget content;
|
||||||
|
final double elevation;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
elevation: elevation,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8.0),
|
||||||
|
content,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
- **Don't use a third party package for state management unless explicitly asked to do so.**
|
||||||
|
- **Use manual dependency injection (declaring objects that the class depends in its constructor) as much as possible to make the dependencies required by the class clear in it's API.**
|
||||||
|
- **If asked to use Provider, use it for app-level objects that are used often.**
|
||||||
|
- **Use Model-View-ViewModel for application architecture.**
|
||||||
|
- **Use ChangeNotifier or a class with ValueNotifiers for ViewModel classes.**
|
||||||
|
- **Use a ListenableBuilder to listen to changes to the ViewModel.**
|
||||||
|
- **Use a StatefulWidget for widgets that are reusable or single-purpose, and don't necessarily require a MVVM architecture.**
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
- **Use Navigator for screens that are short-lived and don't need to be deep-linkable.**
|
||||||
|
|
||||||
|
## Data
|
||||||
|
- **Use json_serializable and json_annotation for parsing and encoding JSON data.**
|
||||||
|
- **Use fieldRename: FieldRename.snake to encode data with snake case.**
|
||||||
|
|
||||||
|
## Code Generation
|
||||||
|
- **Use build_runner for any generated code in the app.**
|
||||||
|
|
||||||
|
## String and Collection Best Practices
|
||||||
|
|
||||||
|
### String Interpolation and Formatting
|
||||||
|
```dart
|
||||||
|
// PREFER using interpolation to compose strings
|
||||||
|
final name = 'John Doe';
|
||||||
|
final age = 25;
|
||||||
|
final message = 'Hello, $name! You are $age years old.';
|
||||||
|
final calculation = 'Next year you will be ${age + 1}.';
|
||||||
|
|
||||||
|
// DO use adjacent strings for long literals
|
||||||
|
const longText = 'This is a very long text that '
|
||||||
|
'spans multiple lines for better '
|
||||||
|
'readability in the source code.';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Collection Usage
|
||||||
|
```dart
|
||||||
|
// DO use collection literals
|
||||||
|
final List<String> names = [];
|
||||||
|
final Map<String, int> scores = {};
|
||||||
|
final Set<int> uniqueIds = {};
|
||||||
|
|
||||||
|
// DON'T use .length to check if empty
|
||||||
|
if (names.isEmpty) { // Good
|
||||||
|
print('No names');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (names.length == 0) { // Bad
|
||||||
|
print('No names');
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO use collection methods effectively
|
||||||
|
final activeUsers = users.where((user) => user.isActive).toList();
|
||||||
|
final userNames = users.map((user) => user.name).toList();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function References
|
||||||
|
```dart
|
||||||
|
// DON'T create lambdas when tear-offs work
|
||||||
|
final numbers = [1, 2, 3, 4, 5];
|
||||||
|
|
||||||
|
// Good - use tear-off
|
||||||
|
numbers.forEach(print);
|
||||||
|
|
||||||
|
// Bad - unnecessary lambda
|
||||||
|
numbers.forEach((number) {
|
||||||
|
print(number);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling and Exceptions
|
||||||
|
|
||||||
|
### Meaningful Error Messages (Flutter Team Priority)
|
||||||
|
```dart
|
||||||
|
// Good: Specific and actionable
|
||||||
|
throw ArgumentError('Email must contain @ symbol');
|
||||||
|
throw StateError('Cannot call increment() after dispose()');
|
||||||
|
|
||||||
|
// Bad: Vague and unhelpful
|
||||||
|
throw ArgumentError('Invalid input');
|
||||||
|
throw Exception('Error occurred');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exception Handling Patterns
|
||||||
|
```dart
|
||||||
|
Future<User> fetchUser(String id) async {
|
||||||
|
try {
|
||||||
|
final response = await api.getUser(id);
|
||||||
|
return User.fromJson(response.data);
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
throw UserFetchException('Failed to fetch user: ${e.message}');
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
throw UserParseException('Invalid user data format: ${e.message}');
|
||||||
|
} catch (e) {
|
||||||
|
throw UserFetchException('Unexpected error: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
- **Use package:integration_test for integration tests.**
|
||||||
|
- **Use package:checks instead of matchers from package:test or package:matcher.**
|
||||||
|
|
||||||
|
### Widget Testing
|
||||||
|
```dart
|
||||||
|
testWidgets('CustomButton should call onPressed when tapped', (tester) async {
|
||||||
|
bool wasPressed = false;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: CustomButton(
|
||||||
|
onPressed: () => wasPressed = true,
|
||||||
|
child: const Text('Test Button'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(CustomButton));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(wasPressed, isTrue);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unit Testing Structure
|
||||||
|
```dart
|
||||||
|
group('UserRepository', () {
|
||||||
|
late UserRepository repository;
|
||||||
|
late MockApiClient mockApi;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockApi = MockApiClient();
|
||||||
|
repository = UserRepository(api: mockApi);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('getUser', () {
|
||||||
|
test('should return user when valid ID provided', () async {
|
||||||
|
// Arrange
|
||||||
|
const userId = '123';
|
||||||
|
final expectedUser = User(id: userId, name: 'John');
|
||||||
|
when(() => mockApi.getUser(userId))
|
||||||
|
.thenAnswer((_) async => expectedUser.toJson());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
final user = await repository.getUser(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(user.id, equals(userId));
|
||||||
|
expect(user.name, equals('John'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw exception when user not found', () async {
|
||||||
|
// Arrange
|
||||||
|
const userId = 'invalid';
|
||||||
|
when(() => mockApi.getUser(userId))
|
||||||
|
.thenThrow(NotFoundException());
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(
|
||||||
|
() => repository.getUser(userId),
|
||||||
|
throwsA(isA<UserNotFoundException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Dart Patterns
|
||||||
|
|
||||||
|
- **Use Patterns and pattern-matching features where possible.**
|
||||||
|
|
||||||
|
### Immutability and Data Classes
|
||||||
|
```dart
|
||||||
|
class User {
|
||||||
|
const User({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.email,
|
||||||
|
this.isActive = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String email;
|
||||||
|
final bool isActive;
|
||||||
|
|
||||||
|
// Use copyWith for immutable updates
|
||||||
|
User copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? email,
|
||||||
|
bool? isActive,
|
||||||
|
}) {
|
||||||
|
return User(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
email: email ?? this.email,
|
||||||
|
isActive: isActive ?? this.isActive,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override equality
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
other is User &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
id == other.id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'User(id: $id, name: $name, email: $email)';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enum Usage and Switch Statements
|
||||||
|
```dart
|
||||||
|
enum ConnectionState {
|
||||||
|
disconnected,
|
||||||
|
connecting,
|
||||||
|
connected,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use switch without default to catch all cases
|
||||||
|
Widget buildConnectionIndicator(ConnectionState state) {
|
||||||
|
switch (state) {
|
||||||
|
case ConnectionState.disconnected:
|
||||||
|
return const Icon(Icons.wifi_off, color: Colors.grey);
|
||||||
|
case ConnectionState.connecting:
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
case ConnectionState.connected:
|
||||||
|
return const Icon(Icons.wifi, color: Colors.green);
|
||||||
|
case ConnectionState.error:
|
||||||
|
return const Icon(Icons.error, color: Colors.red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Effective Use of Assert
|
||||||
|
```dart
|
||||||
|
class Rectangle {
|
||||||
|
const Rectangle({
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
}) : assert(width > 0, 'Width must be positive'),
|
||||||
|
assert(height > 0, 'Height must be positive');
|
||||||
|
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
double get area {
|
||||||
|
assert(width > 0 && height > 0, 'Invalid rectangle dimensions');
|
||||||
|
return width * height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance and Best Practices
|
||||||
|
|
||||||
|
### Const Constructors and Optimization
|
||||||
|
```dart
|
||||||
|
// Use const constructors when possible
|
||||||
|
const EdgeInsets.all(16.0);
|
||||||
|
const SizedBox(height: 8.0);
|
||||||
|
|
||||||
|
// Create const widgets for better performance
|
||||||
|
class LoadingIndicator extends StatelessWidget {
|
||||||
|
const LoadingIndicator({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Efficient Widget Building
|
||||||
|
```dart
|
||||||
|
class ProductList extends StatelessWidget {
|
||||||
|
const ProductList({
|
||||||
|
super.key,
|
||||||
|
required this.products,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Product> products;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Don't do heavy computation in build method
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: products.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final product = products[index];
|
||||||
|
return ProductTile(
|
||||||
|
key: ValueKey(product.id),
|
||||||
|
product: product,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
### Common Mistakes
|
||||||
|
```dart
|
||||||
|
// DON'T use double negatives
|
||||||
|
bool get isNotDisabled => !disabled; // Confusing
|
||||||
|
|
||||||
|
// DO use positive naming
|
||||||
|
bool get isEnabled => !disabled; // Clear
|
||||||
|
|
||||||
|
// DON'T use global state
|
||||||
|
var globalCounter = 0; // Avoid
|
||||||
|
|
||||||
|
// DO use proper state management
|
||||||
|
class CounterProvider extends ChangeNotifier {
|
||||||
|
int _counter = 0;
|
||||||
|
int get counter => _counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DON'T create classes with only static members
|
||||||
|
class MathUtils {
|
||||||
|
static double pi = 3.14159;
|
||||||
|
static double circleArea(double radius) => pi * radius * radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO use top-level functions and constants
|
||||||
|
const double pi = 3.14159;
|
||||||
|
double circleArea(double radius) => pi * radius * radius;
|
||||||
|
|
||||||
|
// DON'T avoid using APIs as intended
|
||||||
|
class TimeSlot {
|
||||||
|
TimeSlot(this.start, this.end);
|
||||||
|
DateTime start;
|
||||||
|
DateTime end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO follow API design principles
|
||||||
|
class TimeSlot {
|
||||||
|
const TimeSlot({
|
||||||
|
required this.start,
|
||||||
|
required this.end,
|
||||||
|
}) : assert(start.isBefore(end), 'Start must be before end');
|
||||||
|
|
||||||
|
final DateTime start;
|
||||||
|
final DateTime end;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tools and Development Workflow
|
||||||
|
|
||||||
|
### Required Tools
|
||||||
|
- **`dart format`**: Automatic code formatting (mandatory)
|
||||||
|
- **`dart analyze`**: Static analysis and linting
|
||||||
|
- **`flutter test`**: Run tests
|
||||||
|
- **IDE setup**: Configure your IDE to run these tools automatically
|
||||||
|
- **Pre-commit hooks**: Ensure code quality before commits
|
||||||
|
|
||||||
|
### Code Quality Checklist
|
||||||
|
- [ ] All code formatted with `dart format`
|
||||||
|
- [ ] No analyzer warnings or errors
|
||||||
|
- [ ] All public APIs documented with `///` comments
|
||||||
|
- [ ] Tests written for new functionality
|
||||||
|
- [ ] Error messages are specific and actionable
|
||||||
|
- [ ] Type annotations present on all public APIs
|
||||||
|
- [ ] Immutable objects used where appropriate
|
||||||
|
- [ ] Assert statements used to verify contracts
|
||||||
|
|
||||||
|
This unified style guide ensures consistency with both Flutter team practices and official Dart conventions, helping create maintainable, readable code that follows established patterns.
|
@ -0,0 +1,93 @@
|
|||||||
|
You are an AI developer specializing in Dart and Flutter. Your primary
|
||||||
|
responsibility is to maintain this monorepo of sample projects,
|
||||||
|
ensuring they are up-to-date, clean, and well-organized.
|
||||||
|
|
||||||
|
This workflow is triggered when a new Flutter/Dart version is
|
||||||
|
released. Follow these steps precisely:
|
||||||
|
|
||||||
|
1. Prepare your environment:
|
||||||
|
* Switch to the `beta` branch and ensure it's up-to-date:
|
||||||
|
```bash
|
||||||
|
git checkout beta
|
||||||
|
git pull origin beta
|
||||||
|
```
|
||||||
|
* Switch your local Flutter SDK to the `beta` channel and upgrade:
|
||||||
|
```bash
|
||||||
|
flutter channel beta
|
||||||
|
flutter upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Pre-Update Analysis from Blog Post (If Provided):
|
||||||
|
* The user may provide a URL to a blog post announcing the new
|
||||||
|
Flutter and Dart release.
|
||||||
|
* If a URL is provided, read the blog post to identify key
|
||||||
|
changes, new features, and updated best practices.
|
||||||
|
* Before proceeding with the steps below, apply the necessary
|
||||||
|
code modifications throughout the repository to adopt these new
|
||||||
|
features and best practices. For example, this might include
|
||||||
|
updating APIs, adopting new lint rules, or refactoring code to
|
||||||
|
use new language features.
|
||||||
|
|
||||||
|
3. Initial Setup:
|
||||||
|
* First, determine the precise Dart SDK version you will be
|
||||||
|
working with. Execute the command `flutter --version --machine`.
|
||||||
|
* Parse the JSON output to find the value of dartSdkVersion. You
|
||||||
|
will need the version number (e.g., 3.9.0). Let's call this
|
||||||
|
DART_VERSION.
|
||||||
|
* Next, read the pubspec.yaml file at the root of the monorepo.
|
||||||
|
* Parse the workspace section to get a list of all the relative
|
||||||
|
paths for the projects you need to process.
|
||||||
|
|
||||||
|
4. Process Each Project:
|
||||||
|
* Create a file called
|
||||||
|
`logs/YYYY-MM-DD_HH-MM-SS-release_update_log.txt`, but replace
|
||||||
|
YYYY-MM-DD_HH-MM-SS with the current date/time.
|
||||||
|
* Iterate through each project path you discovered in the
|
||||||
|
workspace.
|
||||||
|
* For each project, perform the following actions in its
|
||||||
|
directory. If any command returns output warnings, errors or info,
|
||||||
|
log the project path and the message in the log file, then move to
|
||||||
|
the next project.
|
||||||
|
|
||||||
|
5. Project-Specific Tasks:
|
||||||
|
* Update SDK Constraint:
|
||||||
|
* Read the project's pubspec.yaml file.
|
||||||
|
* Find the environment.sdk key.
|
||||||
|
* Update its value to ^DART_VERSION-0 (e.g., ^3.9.0-0).
|
||||||
|
* Save the modified pubspec.yaml file.
|
||||||
|
* Run Quality Checks:
|
||||||
|
* Run dart analyze --fatal-infos --fatal-warnings.
|
||||||
|
* Run dart format . to ensure the code is correctly formatted.
|
||||||
|
* Run Tests:
|
||||||
|
* Check if a test directory exists in the project.
|
||||||
|
* Exception: Do not run tests for the project named
|
||||||
|
material_3_demo.
|
||||||
|
* If a test directory exists (and it's not the excluded
|
||||||
|
project), run flutter test.
|
||||||
|
|
||||||
|
6. Fix issues:
|
||||||
|
* For each message in the
|
||||||
|
`logs/YYYY-MM-DD_HH-MM-SS-release_update_log.txt` file, attempt
|
||||||
|
to fix the problem. After 30 seconds of being unable to fix it,
|
||||||
|
move onto the next issue.
|
||||||
|
* If you fix the issue successfully, remove the message from the
|
||||||
|
log file.
|
||||||
|
* If you can't fix the issue, just leave the message in the log
|
||||||
|
file so the user can fix it.
|
||||||
|
|
||||||
|
7. Final Report:
|
||||||
|
* After processing all projects, generate a summary report.
|
||||||
|
* The report must include:
|
||||||
|
* The total number of projects processed.
|
||||||
|
* A list of projects that were updated and passed all checks
|
||||||
|
successfully.
|
||||||
|
* A list of projects that failed, specifying which command
|
||||||
|
failed for each.
|
||||||
|
|
||||||
|
8. Create Pull Request:
|
||||||
|
* After generating the report, create a pull request.
|
||||||
|
* Use the `gh` CLI tool for this purpose.
|
||||||
|
* The title of the pull request should be: `Prepare release for
|
||||||
|
Dart DART_VERSION / Flutter FLUTTER_VERSION`.
|
||||||
|
* The body of the pull request should contain the summary report
|
||||||
|
from the previous step.
|
@ -1,32 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
The purpose of this changelog is to track the freshness of samples and which
|
|
||||||
samples reflect *current best practices*. It describes **human-made, significant
|
|
||||||
** changes made to the repository or samples in the repository.
|
|
||||||
|
|
||||||
While all samples in this repository build and run, some of them were written
|
|
||||||
long ago, and no longer reflect what we want developers to learn. For example,
|
|
||||||
samples should have been refactored when Dart 3 released to include patterns and
|
|
||||||
records, where appropriate.
|
|
||||||
|
|
||||||
* **DO include:**
|
|
||||||
* The addition of new samples.
|
|
||||||
* The removal of existing samples.
|
|
||||||
* Considerable refactoring of any given sample.
|
|
||||||
|
|
||||||
* **DO NOT include:**
|
|
||||||
* Simple changes that reflect minor version bumps in Flutter. For example,
|
|
||||||
in a recent Flutter update, `Color.red` became `Color.r`.
|
|
||||||
* Dependency updates.
|
|
||||||
* Bug fixes.
|
|
||||||
* Any changes made to simply 'keep the lights on'.
|
|
||||||
|
|
||||||
# Log
|
|
||||||
|
|
||||||
| DATE (YYYY-MM-DD) | Sample(s) | author | Changes |
|
|
||||||
|-------------------|-------------------|--------------|-----------------------------------------------|
|
|
||||||
| NEXT GOES HERE | | | |
|
|
||||||
| | | | |
|
|
||||||
| 2024-12-04 | N/A - repo change | ericwindmill | Added changelog |
|
|
||||||
| 2024-11-27 | fake_sample | ericwindmill | Refactored fake_sample to use Dart 3 features |
|
|
||||||
| 2020-04-17 | fake_sample | ericwindmill | Created fake_sample |
|
|
@ -1,17 +1,17 @@
|
|||||||
name: flutter_module_using_plugin
|
name: flutter_module_using_plugin_android_view
|
||||||
description: An example Flutter module that uses a plugin.
|
description: An example Flutter module that uses a plugin.
|
||||||
|
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
resolution: workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.0-0
|
sdk: ^3.8.1-0
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
provider: ^6.0.2
|
provider: ^6.1.5
|
||||||
url_launcher: ^6.0.20
|
url_launcher: ^6.3.2
|
||||||
sensors_plus: ^5.0.1
|
sensors_plus: ^6.1.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
analysis_defaults:
|
analysis_defaults:
|
@ -1,10 +1,10 @@
|
|||||||
name: flutter_module
|
name: flutter_module_fullscreen
|
||||||
description: An example Flutter module.
|
description: An example Flutter module.
|
||||||
|
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
resolution: workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.0-0
|
sdk: ^3.9.0-0
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
@ -1,3 +1,4 @@
|
|||||||
# https://dart.dev/guides/libraries/private-files
|
# https://dart.dev/guides/libraries/private-files
|
||||||
# Created by `dart pub`
|
# Created by `dart pub`
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
|
.build/
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
name: analysis_defaults
|
name: analysis_defaults
|
||||||
description: Analysis defaults for flutter/samples
|
description: Analysis defaults for flutter/samples
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
resolution: workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.0-0
|
sdk: ^3.9.0-0
|
||||||
|
|
||||||
# NOTE: Code is not allowed in this package. Do not add more dependencies.
|
# NOTE: Code is not allowed in this package. Do not add more dependencies.
|
||||||
# The `flutter_lints` dependency is required for `lib/flutter.yaml`.
|
# The `flutter_lints` dependency is required for `lib/flutter.yaml`.
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
include: package:flutter_lints/flutter.yaml
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue