9.5 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Lichess Mobile is a Flutter-based mobile application (iOS/Android) for lichess.org. The app uses:
- Flutter: Cross-platform UI framework (Flutter 3.38.0+, Dart 3.10.0+)
- Riverpod: State management with providers
- Freezed: Immutable data classes
- Code generation: For data classes, JSON serialization, and localization
- Firebase: Crashlytics and messaging
- Stockfish: Chess engine integration via
multistockfishpackage
Development Setup
Initial Setup
# Install dependencies
flutter pub get
# Generate code (required before first run)
dart run build_runner build
# For continuous development
dart run build_runner watch &
flutter analyze --watch &
Running the App
# Run on all devices (uses lichess.dev server by default)
flutter run -d all
# Run with custom server
flutter run \
--dart-define=LICHESS_HOST=localhost:8080 \
--dart-define=LICHESS_WS_HOST=localhost:8080
Note: Do not include scheme (https:// or ws://) in host values.
For Android with local lila-docker
# Map ports
adb reverse tcp:8080 tcp:8080
# Run app
flutter run --dart-define=LICHESS_HOST=localhost:8080 --dart-define=LICHESS_WS_HOST=localhost:8080
Testing & Quality
Run Tests
# All tests
flutter test
# Single test file
flutter test test/model/engine/engine_test.dart
# Specific test
flutter test test/model/engine/engine_test.dart --name "test name"
Code Quality Checks
# Static analysis
flutter analyze
# Riverpod linting
dart run custom_lint
# Format check (files to format)
dart format --output=none --set-exit-if-changed $(find lib/src -name "*.dart" -not \( -name "*.*freezed.dart" -o -name "*.*g.dart" -o -name "*lichess_icons.dart" \) )
dart format --output=none --set-exit-if-changed $(find test -name "*.dart" -not \( -name "*.*freezed.dart" -o -name "*.*g.dart" \) )
# Format all code
dart format lib/src test
Translations (i18n)
CRITICAL: Never manually edit lib/l10n/app_*.arb files - they are generated.
Adding New Translations
- Edit
translation/source/mobile.xmlfor mobile-specific strings - Generate ARB files and Dart code:
./scripts/gen-arb.mjs
flutter gen-l10n
Mobile-specific translations get a mobile prefix (e.g., "foo" becomes mobileFoo in Dart).
Architecture
Directory Structure
lib/src/
├── model/ # Business logic, state management (Riverpod providers)
│ ├── account/
│ ├── analysis/
│ ├── auth/
│ ├── challenge/
│ ├── common/ # Shared models and utilities
│ ├── engine/ # Stockfish integration
│ ├── game/
│ ├── puzzle/
│ ├── settings/
│ └── ...
├── view/ # UI screens and pages
│ ├── account/
│ ├── analysis/
│ ├── game/
│ ├── home/
│ ├── play/
│ ├── puzzle/
│ ├── settings/
│ └── ...
├── widgets/ # Reusable UI components
├── network/ # HTTP client, WebSocket, connectivity
├── utils/ # Helper functions and utilities
├── styles/ # Theme, colors, icons
├── db/ # Local database (sqflite)
├── app.dart # Main app widget
├── binding.dart # Plugin/API abstraction layer
└── constants.dart # App-wide constants
Key Architectural Patterns
State Management: Riverpod providers throughout lib/src/model/. Controllers, repositories, and services are implemented as providers. State is immutable and managed with Freezed data classes.
Binding Layer: LichessBinding (in binding.dart) provides a testable abstraction for plugins and external APIs:
- SharedPreferences
- Firebase (messaging, crashlytics)
- Stockfish factory
Use AppLichessBinding.ensureInitialized() in production, TestLichessBinding in tests.
Network Layer:
- HTTP:
lib/src/network/http.dart- Platform-specific clients (Cronet for Android, Cupertino for iOS) with authentication, caching, and retry logic - WebSocket:
lib/src/network/socket.dart- Handles ping/pong, message acks, auto-reconnection, event versioning - Helper:
lichessUri(path, queryParams)for HTTP,lichessWSUri(path, queryParams)for WebSocket
Services: Long-running background services initialized in app.dart:
AccountService,NotificationService,MessageService,ChallengeService,CorrespondenceService- Start in
_AppState.initState()
Navigation: Uses Flutter's Navigator with custom route resolution via app_links.dart for deep linking.
Dart Event Loop Execution Model
Understanding Dart's event loop is critical for this codebase due to heavy async operations (network requests, WebSocket communication, Stockfish engine interaction).
Event Loop Basics
Dart is single-threaded and uses an event loop with two queues:
-
Microtask Queue (higher priority)
- Executed before the event queue
- Scheduled with
scheduleMicrotask()or viaFuturecompletions - Used internally by
Future.then(),async/await
-
Event Queue (lower priority)
- I/O events, timers, user interactions
- Scheduled with
Future(),Timer,Streamevents - UI rendering happens between event queue items
Execution order:
1. Execute current synchronous code
2. Process ALL microtasks (until microtask queue is empty)
3. Process ONE event from event queue
4. Repeat from step 2
Async/Await Behavior
// This does NOT block the event loop
Future<void> fetchData() async {
final response = await http.get(uri); // Yields to event loop
// Resumes here when response completes
processData(response);
}
When await is encountered:
- Current function execution pauses
- Control returns to event loop
- Function resumes as a microtask when Future completes
Common Patterns in This Codebase
Riverpod AsyncNotifier: State updates are async but don't block UI:
class MyController extends AsyncNotifier<Data> {
@override
Future<Data> build() async {
// Fetches async, UI shows loading state
return await repository.getData();
}
}
WebSocket Message Handling (see lib/src/network/socket.dart):
- Messages arrive as events
- Processed in event queue
- Microtasks schedule state updates
Stockfish Engine Communication:
- Engine runs in isolate (separate event loop)
- Communication via
SendPort/ReceivePort(event queue)
Important Gotchas
Microtask Queue Starvation: Never create infinite microtask loops - they block the event queue and freeze the UI:
// BAD - Starves event queue
void badLoop() {
scheduleMicrotask(() {
doWork();
badLoop(); // Immediately schedules another microtask
});
}
// GOOD - Allows event queue processing
void goodLoop() {
Future(() { // Uses event queue
doWork();
goodLoop();
});
}
Future vs Future.microtask:
Future(callback)→ event queueFuture.microtask(callback)→ microtask queue- Prefer event queue for non-critical work
Stream Subscriptions: Always cancel to prevent memory leaks:
// In StatefulWidget or Riverpod
final subscription = stream.listen(onData);
@override
void dispose() {
subscription.cancel(); // Critical!
super.dispose();
}
Testing with fake_async: Use FakeAsync for tests involving timers and microtasks to control time progression.
Coding Standards
Immutability (Required)
All data structures must be immutable (all fields final or late final):
- Use Freezed for data classes
- Use fast_immutable_collections for collections in public APIs
- Standard Dart collections (
List,Map) are forbidden in public APIs but allowed in local scopes
Strong Typing
Prefer strong types over primitives (e.g., Duration instead of int).
Functional Style
Prefer functional constructs over imperative:
// Good
return [
if (check) Text('conditional'),
for (el in items) Text(el.name),
];
// Bad
final widgets = <Widget>[];
if (check) widgets.add(Text('conditional'));
for (el in items) widgets.add(Text(el.name));
Widget Guidelines
- Avoid functions returning widgets (use
StatelessWidgetfor reusables) - Don't create private widgets used only once - inline them
- Write reusable widgets as classes even if single-screen scope
Analysis
- Strict mode enabled:
strict-casts,strict-inference,strict-raw-types - Use single quotes for strings
- Always use package imports (no relative imports)
- Page width: 100 characters
- Generated files (
*.g.dart,*.freezed.dart) are excluded from analysis
Code Generation
This project heavily uses code generation. Always run dart run build_runner build (or watch) after:
- Modifying Freezed classes
- Adding JSON serialization
- Changing models with code generation annotations
Generated files are NOT committed to git.
Common Gotchas
- Don't edit generated files: Anything ending in
.g.dart,.freezed.dart, or inlib/l10n/ - Translations: Start with hardcoded English text for new features. Add translations after the feature is stable and in use
- Error messages: Don't translate non-critical error messages (e.g., "could not load XY")
- Brand names: Don't translate names like "Puzzle Storm" or "Puzzle Streak"
- FVM users: Remember to prefix commands with
fvm(e.g.,fvm flutter test)
Debugging
# Start DevTools for logging
dart devtools
# Then run app and follow printed link
flutter run