This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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
# 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 &# 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:8080Note: Do not include scheme (https:// or ws://) in host values.
# 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# 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"# 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 testCRITICAL: Never manually edit lib/l10n/app_*.arb files - they are generated.
- Edit
translation/source/mobile.xmlfor mobile-specific strings - Generate ARB files and Dart code:
./scripts/gen-arb.mjs
flutter gen-l10nMobile-specific translations get a mobile prefix (e.g., "foo" becomes mobileFoo in Dart).
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
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.
Understanding Dart's event loop is critical for this codebase due to heavy async operations (network requests, WebSocket communication, Stockfish engine interaction).
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
// 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
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)
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.
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
Prefer strong types over primitives (e.g., Duration instead of int).
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));- 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
- 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
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.
- 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)
# Start DevTools for logging
dart devtools
# Then run app and follow printed link
flutter run