Code Style¶
Coding standards and conventions for Fluvie
Follow these guidelines to maintain consistency across the codebase.
Table of Contents¶
Dart Style¶
Formatting¶
Use the Dart formatter with 80-character line length:
Configure your IDE to format on save.
analysis_options.yaml¶
The project uses strict analysis rules:
include: package:flutter_lints/flutter.yaml
linter:
rules:
- prefer_const_constructors
- prefer_const_declarations
- prefer_final_fields
- prefer_final_locals
- avoid_print
- avoid_unnecessary_containers
- prefer_single_quotes
Imports¶
Order imports in groups:
// Dart core
import 'dart:async';
import 'dart:math';
// Flutter
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
// External packages
import 'package:json_annotation/json_annotation.dart';
// Internal packages
import 'package:fluvie/fluvie.dart';
// Relative imports
import '../utils/interpolate.dart';
import 'time_consumer.dart';
Prefer const¶
Use const wherever possible:
// Good
const EdgeInsets.all(16)
const TextStyle(fontSize: 24)
const SizedBox(height: 8)
// In constructors
class MyWidget extends StatelessWidget {
const MyWidget({super.key}); // const constructor
}
Naming Conventions¶
Classes¶
| Type | Convention | Example |
|---|---|---|
| Widgets | PascalCase | TimeConsumer, VideoComposition |
| Data classes | PascalCase | RenderConfig, AudioConfig |
| Enums | PascalCase | TemplateCategory, RenderQuality |
| Mixins | PascalCase with "Mixin" suffix | TemplateMixin |
| Extensions | PascalCase with context | IterableExtension |
Files¶
| Type | Convention | Example |
|---|---|---|
| Widgets | snake_case | time_consumer.dart |
| Tests | snake_case with _test suffix |
time_consumer_test.dart |
| Generated | snake_case with .g.dart |
render_config.g.dart |
Variables and Functions¶
// Variables - camelCase
final frameNumber = 42;
final totalDuration = Duration(seconds: 5);
// Private - underscore prefix
final _internalState = {};
void _processFrame() {}
// Constants - camelCase or SCREAMING_SNAKE_CASE for app-wide
const defaultFps = 30;
const MAX_FRAME_CACHE_SIZE = 1000;
// Functions - camelCase, verb-first
void renderFrame() {}
Future<void> processVideo() async {}
bool isValidFrame(int frame) => frame >= 0;
Widget Parameters¶
class MyWidget extends StatelessWidget {
// Required parameters first
final String title;
final int startFrame;
// Optional parameters with defaults
final int durationInFrames;
final Curve curve;
// Callbacks
final VoidCallback? onComplete;
// Child/children last
final Widget child;
const MyWidget({
super.key,
required this.title,
required this.startFrame,
this.durationInFrames = 30,
this.curve = Curves.easeOut,
this.onComplete,
required this.child,
});
}
Documentation¶
Class Documentation¶
/// A widget that provides the current frame number to its descendants.
///
/// [TimeConsumer] rebuilds its child whenever the frame changes, making it
/// the primary way to create frame-based animations in Fluvie.
///
/// ## Example
///
/// ```dart
/// TimeConsumer(
/// builder: (context, frame, child) {
/// return Text('Frame: $frame');
/// },
/// )
/// ```
///
/// See also:
/// * [RenderModeProvider], which provides the frame notifier
/// * [Fade], a widget that uses TimeConsumer internally
class TimeConsumer extends StatelessWidget {
Property Documentation¶
/// The frame at which the animation starts.
///
/// This is relative to the scene's start frame. A [startFrame] of 30
/// means the animation begins one second into a 30fps scene.
final int startFrame;
/// The duration of the animation in frames.
///
/// At 30fps, a [durationInFrames] of 60 equals 2 seconds.
/// Defaults to 30 frames (1 second at 30fps).
final int durationInFrames;
Method Documentation¶
/// Calculates the opacity at the given [progress].
///
/// The [progress] value ranges from 0.0 (start) to 1.0 (end).
///
/// Returns a value between 0.0 (invisible) and 1.0 (fully visible).
///
/// Throws [ArgumentError] if [progress] is outside the 0.0-1.0 range.
double opacityAt(double progress) {
if (progress < 0 || progress > 1) {
throw ArgumentError.value(progress, 'progress', 'Must be between 0 and 1');
}
return _curve.transform(progress);
}
When to Document¶
Always document: - Public classes - Public methods - Public properties - Complex logic
Skip documentation for: - Obvious getters/setters - Private implementation details - Self-documenting code
// Skip - obvious
int get length => _length;
// Document - non-obvious behavior
/// Returns the effective length, accounting for trim settings.
///
/// This may be less than [originalLength] if the video is trimmed.
int get effectiveLength => _length - _trimStart - _trimEnd;
Widget Guidelines¶
Prefer Composition¶
// Good - composed from smaller widgets
class StatCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedProp(
animation: PropAnimation.fadeIn(),
child: Container(
child: Column(
children: [
_buildTitle(),
_buildValue(),
],
),
),
);
}
}
// Avoid - monolithic widgets with everything inline
Use const Constructors¶
class MyWidget extends StatelessWidget {
final String text;
// const constructor
const MyWidget({
super.key,
required this.text,
});
@override
Widget build(BuildContext context) {
// Use const for static children
return const Column(
children: [
SizedBox(height: 8),
Divider(),
],
);
}
}
Keep build() Clean¶
// Good - extracted methods
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildHeader(),
_buildContent(),
_buildFooter(),
],
);
}
Widget _buildHeader() { ... }
Widget _buildContent() { ... }
Widget _buildFooter() { ... }
// Avoid - everything in build()
@override
Widget build(BuildContext context) {
return Column(
children: [
// 50 lines of header code...
// 100 lines of content code...
// 30 lines of footer code...
],
);
}
Avoid Expensive Operations in build()¶
// Bad - calculates every rebuild
@override
Widget build(BuildContext context) {
final expensiveData = computeExpensiveData(); // Avoid!
return Text(expensiveData.toString());
}
// Good - compute in initState or use memoization
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final String _cachedData;
@override
void initState() {
super.initState();
_cachedData = computeExpensiveData();
}
@override
Widget build(BuildContext context) {
return Text(_cachedData);
}
}
Testing Standards¶
Test File Structure¶
void main() {
// Group related tests
group('ClassName', () {
// Setup shared across tests
late SomeClass instance;
setUp(() {
instance = SomeClass();
});
tearDown(() {
instance.dispose();
});
// Test specific behavior
group('methodName', () {
test('does X when Y', () {
// Arrange
final input = 'test';
// Act
final result = instance.methodName(input);
// Assert
expect(result, equals(expected));
});
test('throws when invalid input', () {
expect(
() => instance.methodName(null),
throwsA(isA<ArgumentError>()),
);
});
});
});
}
Widget Test Structure¶
testWidgets('description of what is being tested', (tester) async {
// Arrange - set up widget
await tester.pumpWidget(
MaterialApp(
home: MyWidget(),
),
);
// Act - interact with widget
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// Assert - verify result
expect(find.text('Expected'), findsOneWidget);
});
Git Conventions¶
Branch Names¶
feature/add-particle-effects
fix/audio-sync-issue
docs/update-tutorial
refactor/simplify-render-pipeline
Commit Messages¶
Follow conventional commits:
Types:
- feat: New feature
- fix: Bug fix
- docs: Documentation
- style: Formatting
- refactor: Code restructure
- test: Add/update tests
- chore: Maintenance
Examples:
feat(templates): add StackClimb ranking template
Implements a new ranking template that reveals items by stacking
and sliding cards off screen.
Closes #123
fix(audio): correct sync anchor timing calculation
The sync offset was being applied in the wrong direction,
causing audio to play late instead of early.
docs(widgets): add TimeConsumer documentation
- Add comprehensive guide with examples
- Include performance tips
- Link to related widgets
Pull Request Guidelines¶
- Title: Use conventional commit format
- Description: Explain what and why
- Testing: Describe how to test
- Screenshots: Include for visual changes
- Checklist: Mark completed items
Related¶
- Development Setup - Environment setup
- Testing - Testing guidelines
- Contributing - Contribution overview