This document outlines the development rules, guidelines, and best practices for the SetTimer Flutter project when using Cursor IDE.
- Project Overview
- Architecture Rules
- Code Style & Conventions
- File Organization
- State Management
- Error Handling
- Dependencies & Imports
- Platform-Specific Guidelines
- Testing Standards
- Performance Guidelines
- Commit & Version Control
- Documentation Requirements
SetTimer is a minimalist workout timer app for set-based workouts, HIIT, and interval training. The app follows clean architecture principles with MVC pattern and uses Provider for state management.
- Framework: Flutter 3.5.3+
- State Management: Provider pattern with ChangeNotifier
- Audio: flutter_ringtone_player, audioplayers
- Voice Coaching: flutter_tts
- Storage: sqflite, shared_preferences
- Analytics: fl_chart
- Wake Lock: wakelock_plus
lib/
├── controllers/ # Business logic and state management
├── models/ # Data models and entities
├── services/ # External services and utilities
├── views/ # UI screens and layouts
├── widgets/ # Reusable UI components
└── main.dart # App entry point
- Controllers MUST extend
ChangeNotifierand handle business logic only - Models MUST be immutable with
copyWith()methods - Services MUST handle external APIs, storage, and platform features
- Views MUST be StatelessWidget that consume Provider state
- Widgets MUST be reusable components with clear props
- Views → Controllers → Services → Models
- Never import Views in Controllers or Services
- Never import Controllers in Services
// Classes: PascalCase
class TimerController extends ChangeNotifier {}
// Variables and functions: camelCase
int remainingSeconds = 30;
void startTimer() {}
// Constants: SCREAMING_SNAKE_CASE
static const int MAX_SETS = 20;
// Private members: _prefixed
String _privateMethod() {}
// Files: snake_case.dart
timer_controller.dart
audio_service.dart- Line Length: Maximum 120 characters
- Indentation: 2 spaces (no tabs)
- Trailing Commas: Always use for multi-line arguments
- Imports: Group and sort alphabetically
// ✅ Good
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text('Hello World'),
ElevatedButton(
onPressed: onPressed,
child: Text('Button'),
),
],
),
);
}/// Starts the workout timer with current settings.
///
/// This method handles:
/// - Session tracking initialization
/// - Audio feedback
/// - Background mode enabling
/// - Screen wake lock
///
/// Throws [TimerException] if timer is already running.
void startTimer() async {
// Implementation
}- Each service handles ONE specific domain
- Services MUST be stateless and injectable
- Use dependency injection pattern
// ✅ Good - Single responsibility
class AudioService {
Future<void> initialize() async {}
void playSetStart() {}
void playSetEnd() {}
}
// ❌ Bad - Multiple responsibilities
class UtilityService {
void playSound() {}
void saveData() {}
void sendAnalytics() {}
}- One model per file
- Include
copyWith(),toJson(),fromJson()methods - Use
@immutableannotation
@immutable
class TimerModel {
final int totalSets;
final int setDurationSeconds;
final TimerState state;
const TimerModel({
required this.totalSets,
required this.setDurationSeconds,
required this.state,
});
TimerModel copyWith({
int? totalSets,
int? setDurationSeconds,
TimerState? state,
}) {
return TimerModel(
totalSets: totalSets ?? this.totalSets,
setDurationSeconds: setDurationSeconds ?? this.setDurationSeconds,
state: state ?? this.state,
);
}
}- Controllers extend
ChangeNotifier - Use
Consumer<T>for reactive UI updates - Use
context.read<T>()for one-time actions - NEVER call
notifyListeners()in getters
// ✅ Good - Reactive UI
Consumer<TimerController>(
builder: (context, controller, child) {
return Text('${controller.timer.remainingSeconds}');
},
),
// ✅ Good - One-time action
ElevatedButton(
onPressed: () => context.read<TimerController>().startTimer(),
child: Text('Start'),
),
// ❌ Bad - Using watch for actions
ElevatedButton(
onPressed: () => context.watch<TimerController>().startTimer(),
child: Text('Start'),
),- NEVER mutate state directly
- Always use
copyWith()for state updates - Call
notifyListeners()after state changes
// ✅ Good
void updateSettings(int newDuration) {
_timer = _timer.copyWith(setDurationSeconds: newDuration);
notifyListeners();
}
// ❌ Bad
void updateSettings(int newDuration) {
_timer.setDurationSeconds = newDuration; // Direct mutation
}- Use specific exception types
- Handle async operations with try-catch
- Log errors with context
- Never suppress exceptions silently
// ✅ Good
Future<void> initializeAudio() async {
try {
await audioService.initialize();
print('✅ Audio service initialized successfully');
} on AudioException catch (e) {
print('🔊 Audio initialization failed: ${e.message}');
throw TimerException('Failed to initialize audio: ${e.message}');
} catch (e) {
print('⚠️ Unexpected error during audio init: $e');
rethrow;
}
}
// ❌ Bad
Future<void> initializeAudio() async {
try {
await audioService.initialize();
} catch (e) {
// Silent failure
}
}// Use structured logging with emojis for clarity
print('🏁 Timer started - Sets: ${timer.totalSets}, Duration: ${timer.setDurationSeconds}s');
print('⏸️ Timer paused at ${timer.remainingSeconds}s remaining');
print('⚠️ Warning: Battery optimization may affect background timers');
print('❌ Error: Failed to save workout session - ${e.toString()}');
print('✅ Workout completed successfully in ${duration.inMinutes}m ${duration.inSeconds % 60}s');// 1. Dart SDK imports
import 'dart:async';
import 'dart:io';
// 2. Flutter framework imports
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// 3. Third-party package imports
import 'package:provider/provider.dart';
import 'package:sqflite/sqflite.dart';
// 4. Local imports (relative paths)
import '../models/timer_model.dart';
import '../services/audio_service.dart';- NEVER add dependencies without discussion
- Keep dependencies minimal and focused
- Use specific version constraints in
pubspec.yaml - Document why each dependency is needed
# ✅ Good - Specific versions with reasoning
dependencies:
# State management for reactive UI
provider: ^6.1.1
# Audio playback for workout feedback
audioplayers: ^5.2.1
# Voice coaching functionality
flutter_tts: ^4.0.2
# ❌ Bad - Loose constraints
dependencies:
provider: any
audioplayers: ^5.0.0- Handle background audio sessions properly
- Request microphone permissions for TTS
- Test on multiple iOS versions (12.0+)
- Consider iOS-specific UI guidelines
- Handle wake locks carefully
- Consider battery optimization warnings
- Test on various Android versions (API 21+)
- Handle different screen densities
// ✅ Good - Platform-specific implementations
class AudioService {
Future<void> initialize() async {
if (Platform.isIOS) {
await _initializeIOSAudioSession();
} else if (Platform.isAndroid) {
await _initializeAndroidAudioFocus();
}
}
}test/
├── unit/
│ ├── controllers/
│ ├── models/
│ └── services/
├── widget/
│ ├── views/
│ └── widgets/
└── integration/
└── app_test.dart
- Aim for 80%+ code coverage
- Test business logic thoroughly
- Mock external dependencies
- Write descriptive test names
// ✅ Good test structure
group('TimerController', () {
late TimerController controller;
late MockAudioService mockAudioService;
setUp(() {
mockAudioService = MockAudioService();
controller = TimerController(audioService: mockAudioService);
});
group('startTimer', () {
test('should start timer when in idle state', () {
// Arrange
expect(controller.timer.state, TimerState.idle);
// Act
controller.startTimer();
// Assert
expect(controller.timer.state, TimerState.running);
verify(mockAudioService.playSetStart()).called(1);
});
test('should throw exception when timer is already running', () {
// Arrange
controller.startTimer();
// Act & Assert
expect(() => controller.startTimer(), throwsA(isA<TimerException>()));
});
});
});- Use
constconstructors wherever possible - Implement
shouldRebuildfor expensive widgets - Use
ListView.builderfor long lists - Minimize widget rebuilds with proper
Consumerplacement
// ✅ Good - Const constructor
const TimerDisplay({
super.key,
required this.remainingSeconds,
});
// ✅ Good - Selective rebuilds
Consumer<TimerController>(
builder: (context, controller, child) {
return Text('${controller.timer.remainingSeconds}');
},
child: const ExpensiveWidget(), // Won't rebuild
),- Dispose controllers, timers, and streams
- Cancel subscriptions in dispose methods
- Use weak references for callbacks
@override
void dispose() {
_countdownTimer?.cancel();
_audioService.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}<type>(<scope>): <description>
<optional body>
<optional footer>
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, semicolons, etc.)refactor: Code refactoringtest: Adding or updating testschore: Maintenance tasks
feat(timer): add voice coaching support
fix(audio): resolve iOS background audio issue
docs(readme): update installation instructions
refactor(models): simplify timer state managementfeature/voice-coaching
bugfix/ios-audio-session
hotfix/critical-timer-bug- Document all public APIs
- Include usage examples for complex functions
- Document platform-specific behavior
- Keep comments up-to-date with code changes
- Update feature lists when adding functionality
- Include new screenshots for UI changes
- Update installation steps for new dependencies
- Maintain accurate architecture documentation
- Document breaking changes
- Include migration guides
- List new features and bug fixes
- Reference issue numbers
- Provide context about SetTimer's architecture when asking for help
- Mention specific patterns used (Provider, MVC)
- Reference existing code structures for consistency
- Ask for platform-specific implementations when needed
- Use existing models as templates for new models
- Follow established patterns for controllers and services
- Maintain consistent error handling patterns
- Ensure generated code follows our naming conventions
- Use IDE refactoring tools for renaming
- Verify all references are updated
- Run tests after major refactoring
- Update documentation for API changes
These rules ensure consistency, maintainability, and quality across the SetTimer codebase. When in doubt:
- Follow existing patterns in the codebase
- Ask for clarification rather than guessing
- Test thoroughly on both platforms
- Document your changes appropriately
Remember: SetTimer is a minimalist app focused on workout timing. Every feature and change should align with this core principle of simplicity and effectiveness.
Happy coding! 🏋️♂️