This style guide ensures consistency across the hexaGenApp codebase by outlining formatting rules, naming conventions, and Dart/Flutter-specific practices.
- General Principles
- File Organization
- Naming Conventions
- Code Formatting
- Dart Language Conventions
- Flutter Widget Conventions
- Comments and Documentation
- Error Handling
- State Management
- Testing
- Write clear, readable, maintainable code
- Prefer simplicity over cleverness
- Follow DRY (Don't Repeat Yourself) principle
- Use meaningful names that convey intent
- Keep functions small and focused (single responsibility)
- Follow existing patterns in the codebase
- Match the style of surrounding code
- Use the same approach for similar problems
- Avoid premature optimization
- Profile before optimizing
- Consider memory usage in mobile contexts
Every file must include SPDX license headers:
// SPDX-FileCopyrightText: 2025 hexaTune LLC
// SPDX-License-Identifier: MITOrganize imports in the following order with blank lines between groups:
- Dart SDK imports
- Flutter framework imports
- Third-party package imports
- Local project imports (using relative paths from
lib/)
// SPDX-FileCopyrightText: 2025 hexaTune LLC
// SPDX-License-Identifier: MIT
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_midi_command/flutter_midi_command.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:hexagenapp/src/core/device/device.dart';
import 'package:hexagenapp/src/core/at/at.dart';
import 'package:hexagenapp/l10n/app_localizations.dart';Files should follow this general structure:
- License header
- Imports
- Constants (top-level or class-level)
- Type definitions (enums, typedefs)
- Classes/functions
- Private helpers at the bottom
- Use descriptive names that convey purpose
- Avoid abbreviations unless widely recognized (e.g.,
Hz,MIDI) - Use English for all identifiers
- Classes:
DeviceService,HexaTuneDeviceManager - Enums:
ItemStatus,LogCategory - Type aliases:
ItemStatusCallback - Extensions:
StringExtension
- Variables:
currentDevice,deviceVersion - Functions/methods:
initialize(),connectToDevice() - Parameters:
onItemCountChanged,deviceId - Non-constant fields:
_isInitialized,_deviceManager
- Package names:
hexagenapp - Directories:
core,pages,device - File names:
device_service.dart,log_service.dart
- Constants:
_maxItems,_maxId,_maxNotifications - Enum values use lowerCamelCase:
ItemStatus.pending
Prefix with underscore for private fields, methods, and classes:
class DeviceService {
// Private fields
MidiDevice? _currentDevice;
int _nextId = 1;
// Private methods
int _generateId() { ... }
void _trackCommand() { ... }
// Public interface
bool get isConnected => ...;
Future<void> initialize() async { ... }
}Use positive, question-like names:
bool isConnected; // ✅ Good
bool notDisconnected; // ❌ Bad
bool hasUnreadNotifications; // ✅ Good
bool noNotifications; // ❌ Bad
bool _waitingForResponse; // ✅ Good
bool _notReady; // ❌ BadUse plural names:
final List<NotificationItem> _notifications = [];
final Map<int, Timer> _commandTimers = {};
final Set<String> deviceIds = {};Always run dart format . before committing. The project follows official Dart formatting conventions.
- Maximum 80 characters per line (Dart standard)
- Break long lines at logical points
- Use 2 spaces (not tabs)
- Flutter/Dart standard indentation
Use trailing commas for better formatting and diffs:
// ✅ Good - Allows formatter to work optimally
Widget build(BuildContext context) {
return Column(
children: [
Text('Hello'),
Button('Click me'),
],
);
}
// ❌ Bad - No trailing comma, single line
Widget build(BuildContext context) {
return Column(children: [Text('Hello'), Button('Click me')]);
}Always use braces for control flow, even single-line statements:
// ✅ Good
if (isConnected) {
disconnect();
}
// ❌ Bad
if (isConnected) disconnect();- Space after control keywords:
if (condition),for (item in items) - No space for function calls:
initialize(),connect(device) - Space around operators:
a + b,x == y,value ?? defaultValue
Use explicit types for public APIs, optional for local variables:
// ✅ Good - Public API with explicit types
Future<void> initialize() async { ... }
String? get deviceVersion => _deviceVersion;
// ✅ Good - Local variable with inference
final device = await scanForDevice();
var count = items.length;
// ❌ Bad - Missing return type
connectDevice() async { ... }Embrace null safety features:
// Use nullable types
String? _deviceVersion;
// Use null-aware operators
final version = _deviceVersion ?? 'Unknown';
final length = items?.length ?? 0;
// Use null assertion only when certain
final device = currentDevice!; // Only if guaranteed non-null
// Prefer null checks
if (_currentDevice != null) {
final device = _currentDevice!;
useDevice(device);
}Prefer async/await over raw Future chaining:
// ✅ Good
Future<void> initialize() async {
await loadDevices();
await connectToDevice();
await fetchVersion();
}
// ❌ Bad
Future<void> initialize() {
return loadDevices()
.then((_) => connectToDevice())
.then((_) => fetchVersion());
}Use const for compile-time constants:
// ✅ Good
const Duration timeout = Duration(seconds: 5);
const int maxItems = 64;
const Widget placeholder = Text('Loading...');
// Use static const for class constants
class GenerationPage extends StatefulWidget {
static const int _maxItems = 64;
static const double _minHz = 0;
static const double _maxHz = 20_000_000;
}Use collection literals and spread operators:
// ✅ Good
final items = <String>[];
final map = <String, int>{};
final combined = [...list1, ...list2];
// ❌ Bad
final items = List<String>();
final map = Map<String, int>();Use enhanced enums when possible (Dart 2.17+):
enum ItemStatus {
pending,
processing,
completed,
error,
}
enum LogCategory {
app,
navigation,
device,
midi,
ui,
network,
storage,
}Follow this structure for widget classes:
class MyWidget extends StatefulWidget {
// 1. Constants
static const int maxValue = 100;
// 2. Final fields (constructor parameters)
final String title;
final VoidCallback? onPressed;
// 3. Constructor
const MyWidget({
super.key,
required this.title,
this.onPressed,
});
// 4. createState
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// 1. Private fields
int _counter = 0;
// 2. Lifecycle methods
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
// 3. Build method
@override
Widget build(BuildContext context) {
return Container();
}
// 4. Helper methods
void _incrementCounter() {
setState(() => _counter++);
}
}Use keys for widgets in lists or when identity matters:
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
key: ValueKey(items[index].id), // ✅ Good
title: Text(items[index].name),
);
},
)Pass BuildContext explicitly, don't store in fields:
// ✅ Good
void _showDialog(BuildContext context) {
showDialog(context: context, builder: ...);
}
// ❌ Bad
late BuildContext _context;
void initState() {
_context = context; // Context can become invalid
}Extract complex widget trees into separate methods or widgets:
// ✅ Good - Small build method
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppBar(),
body: _buildBody(),
);
}
Widget _buildAppBar() {
return AppBar(title: Text('Title'));
}
// Or extract to a separate widget for better performance
class MyAppBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBar(title: Text('Title'));
}
}Use /// for public APIs:
/// Initializes the device service.
///
/// This should be called once during app startup.
/// Returns a [Future] that completes when initialization is done.
Future<void> initialize() async { ... }
/// The currently connected MIDI device, or null if disconnected.
MidiDevice? get currentDevice => _currentDevice;Use // for implementation notes:
// Generate next sequential ID (1-9999, wraps around)
int _generateId() {
final id = _nextId;
_nextId = _nextId % _maxId + 1; // Wrap around at max
return id;
}- Write complete sentences with proper punctuation
- Explain why, not what (code shows what)
- Keep comments up to date with code changes
- Remove commented-out code (use version control)
// ✅ Good - Explains why
// Use a timer to prevent blocking the UI thread
await Future.delayed(Duration(milliseconds: 100));
// ❌ Bad - States the obvious
// Set counter to zero
_counter = 0;Format consistently for easy searching:
// TODO(username): Add retry logic for failed connections
// FIXME: Memory leak when disposing multiple times
// HACK: Workaround for flutter_midi_command issue #123Use appropriate exception types:
// Use built-in exceptions
throw ArgumentError('Device ID cannot be null');
throw StateError('Service not initialized');
throw FormatException('Invalid AT command format');
// Define custom exceptions for domain-specific errors
class DeviceNotConnectedException implements Exception {
final String message;
DeviceNotConnectedException(this.message);
@override
String toString() => 'DeviceNotConnectedException: $message';
}Handle exceptions at appropriate levels:
// ✅ Good - Handle specific errors
try {
await connectToDevice(device);
} on DeviceNotConnectedException catch (e) {
logger.error('Failed to connect: $e', category: LogCategory.device);
showErrorDialog(context, e.message);
} catch (e) {
logger.critical('Unexpected error: $e', category: LogCategory.app);
rethrow;
}Use nullable types or Result types for expected failures:
// ✅ Good - Nullable for expected absence
String? findDeviceById(String id) { ... }
// ✅ Good - Result type for success/failure
Result<Device, Error> connectDevice(String id) { ... }Follow singleton pattern for services:
class DeviceService extends ChangeNotifier {
// Singleton instance
static final DeviceService _instance = DeviceService._internal();
factory DeviceService() => _instance;
DeviceService._internal();
// Private state
bool _isConnected = false;
// Public getters
bool get isConnected => _isConnected;
// Methods that modify state call notifyListeners
void connect() {
_isConnected = true;
notifyListeners();
}
}Keep setState calls minimal and focused:
// ✅ Good - Only update what changed
void _incrementCounter() {
setState(() {
_counter++;
});
}
// ❌ Bad - Heavy computation in setState
void _updateData() {
setState(() {
_data = processLargeDataset(); // Move outside setState
_filtered = filterData(_data);
});
}
// ✅ Better
Future<void> _updateData() async {
final processed = await processLargeDataset();
final filtered = filterData(processed);
setState(() {
_data = processed;
_filtered = filtered;
});
}Match source file names with _test.dart suffix:
lib/src/core/device/device.dart
test/core/device/device_test.dart
Use group and descriptive test names:
void main() {
group('DeviceService', () {
test('should initialize successfully', () {
// Arrange
final service = DeviceService();
// Act
service.initialize();
// Assert
expect(service.isInitialized, isTrue);
});
test('should generate sequential IDs', () {
final service = DeviceService();
final id1 = service.generateId();
final id2 = service.generateId();
expect(id2, equals(id1 + 1));
});
});
}The project uses flutter_lints with custom rules in analysis_options.yaml.
Run analysis before committing:
flutter analyzeprefer_const_constructors- Use const when possibleprefer_final_fields- Mark fields final if not reassignedavoid_print- Use logging instead of print()use_key_in_widget_constructors- Add key parameter to widgets
Before committing code:
- ✅ Run
dart format . - ✅ Run
flutter analyze(no errors) - ✅ Run
flutter test(all tests pass) - ✅ Add SPDX license headers to new files
- ✅ Update documentation if needed
- ✅ Follow commit message conventions
- ✅ Remove debug code and console logs
Consistent style improves collaboration and makes onboarding easier. Stick to this guide for clean, professional code.
For style-related questions:
- Email: info@hexatune.com
- Issues: GitHub Issues
Built by hexaTune LLC · GitHub: hTuneSys/hexaGenApp · License: MIT