This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Hermes - A Flutter application demonstrating GitHub user search with infinite scroll pagination, built using clean architecture and the Flux pattern.
This is a Flutter monorepo organized into separate packages, managed with Melos:
hermes/
├── lib/ # Main app entry point
│ ├── main.dart # App initialization
│ └── router/ # App routing
├── packages/
│ ├── core/ # Core functionality
│ │ ├── flux/ # Flux architecture (Dispatcher, EventStore, ActionCreator)
│ │ └── env/ # Environment configuration
│ ├── data/ # Data layer
│ │ ├── graphql/ # GraphQL queries
│ │ ├── graphql_client.dart # GraphQL client setup
│ │ └── links/ # GraphQL links (auth, error handling)
│ ├── ui/ # Shared UI components
│ │ └── theme/ # App theme (colors, typography)
│ └── feature/
│ └── home/ # GitHub search feature
│ ├── action/ # Flux actions and action creators
│ ├── store/ # Flux stores for state management
│ ├── viewmodel/ # ViewModels (Input/Output interface)
│ ├── view/ # UI pages
│ ├── model/ # Data models
│ ├── repository/ # Data access layer
│ └── i18n/ # Internationalization
The project uses Melos 7.3.0 for monorepo management. The workspace is configured in the root pubspec.yaml under the workspace: and melos: sections.
IMPORTANT: Always use Melos commands for managing the monorepo. Melos is installed globally and must be in your PATH.
Ensure Melos is installed and in your PATH:
# Install globally
dart pub global activate melos
# Add to PATH (add to ~/.zshrc or ~/.bashrc)
export PATH="$PATH:$HOME/.pub-cache/bin"# List all packages in workspace
melos list
# Bootstrap workspace (link all packages)
melos bootstrap
# Get dependencies for all packages
melos run get
# Clean all packages
melos run clean# Run build_runner on all packages that use it
melos run build_runner --no-select
# Watch mode for continuous code generation
melos run build_runner:watch --no-selectNote: Always use the --no-select flag with build_runner commands to avoid interactive prompts.
# Run flutter analyze on all packages
melos run analyze
# Format all Dart files in workspace
melos run formatFrom the root directory:
flutter runFor specific build modes:
flutter run --profile # Profile mode
flutter run --release # Release modeRun tests using Melos (runs tests in all packages):
melos exec -- flutter testOr run tests in a specific package:
cd packages/feature/home
flutter testBuild for different platforms:
flutter build apk # Android APK
flutter build appbundle # Android App Bundle
flutter build ios # iOS
flutter build macos # macOS
flutter build web # WebThis project uses the Flux architecture pattern for unidirectional data flow:
User Input → ViewModel → ActionCreator → Dispatcher → Store → State Update → ViewModel → UI
Key Components:
-
Actions (
action/)- Immutable data classes extending
EventAction - Represent user intents (e.g.,
SearchUsersAction,LoadMoreUsersAction) - Example:
class SearchUsersAction extends EventAction { final String username; SearchUsersAction(this.username); }
- Immutable data classes extending
-
Action Creators (
action/)- Create and dispatch actions to the
Dispatcher - Example:
class GitHubSearchActionCreator extends ActionCreator { void searchUsers(String username) { dispatch(SearchUsersAction(username)); } }
- Create and dispatch actions to the
-
Stores (
store/)- Extend
EventStoreand manage feature state - Listen to actions from
DispatcherviaonAction() - Use Freezed for immutable state
- Example:
class GitHubSearchStore extends EventStore<GitHubSearchState> { @override void onAction(EventAction action) { switch (action) { case SearchUsersAction(): _searchUsers(action); break; } } }
- Extend
-
ViewModels (
viewmodel/)- Implement Input/Output interface pattern
- Watch store state and provide it to UI
- Delegate user actions to action creators
- Example:
abstract interface class _Input { void onSearchUsers(String username); } abstract interface class _Output { GitHubSearchState get state; } class GitHubSearchViewmodel implements _Input, _Output { final GitHubSearchState _searchState; final GitHubSearchActionCreator _actionCreator; @override void onSearchUsers(String username) { _actionCreator.searchUsers(username); } }
-
Views (
view/)- UI components using
StatefulHookConsumerWidget - Watch viewmodel for state updates
- Call viewmodel methods for user interactions
- UI components using
- Riverpod for dependency injection
- Freezed for immutable state classes
- Hooks for lifecycle management (useEffect, useScrollController, etc.)
The theme system is centralized in the packages/ui package:
packages/ui/lib/theme/theme.dart- Main theme definitions (hermesLightTheme,hermesDarkTheme)packages/ui/lib/theme/colors.dart- Color definitions includingseedColor
The main app imports and applies the theme in lib/main.dart:
import 'package:ui/theme/theme.dart';
MaterialApp(
theme: hermesLightTheme,
darkTheme: hermesDarkTheme,
...
)The home feature (packages/feature/home/) demonstrates the full Flux pattern:
Features:
- Search GitHub users by username using GraphQL API
- Display user profiles with avatar, bio, stats, and top repositories
- Infinite scroll pagination - Automatically loads more users when scrolling to bottom
- Error handling with user-friendly messages
- Null-safe parsing to handle missing data gracefully
Key Implementation Details:
-
Pagination
- Uses
LoadMoreUsersActionto trigger loading more results - Store tracks
currentQueryandendCursorfor pagination - View uses
useScrollControllerto detect scroll position - Loads more when within 200px of bottom
- Shows loading indicator at end of list
- Uses
-
Null Safety
- All required String fields have fallback values:
(data['field'] as String?) ?? '' - Empty avatarUrl shows person icon instead of trying to load empty URL
- Defensive parsing for all GraphQL responses
- All required String fields have fallback values:
-
i18n Support
- Uses
slangfor internationalization - Base locale:
ja(Japanese) - Translations in
lib/i18n/ja.i18n.json - Generated code in
lib/i18n/strings.g.dartandstrings_ja.g.dart
- Uses
The repository layer (repository/) abstracts data access:
@riverpod
Future<GitHubSearchRepository> gitHubSearchRepository(Ref ref) async {
final client = await ref.watch(graphqlClientProvider.future);
return GitHubSearchRepositoryImpl(client);
}IMPORTANT: When using async providers in Flux stores, always use ref.watch() not ref.read():
// ✅ CORRECT
final repository = await ref.watch(gitHubSearchRepositoryProvider.future);
// ❌ WRONG - causes disposal errors
final repository = await ref.read(gitHubSearchRepositoryProvider.future);The project uses environment variables for configuration:
File: packages/core/.env
GITHUB_BASE_URL=https://api.github.com/graphql
GITHUB_TOKEN=ghp_YourTokenHereLoading: Uses flutter_dotenv to load environment variables
Access: Via EnvironmentSettings model with Freezed
.env files to version control!
- riverpod_generator - Generates provider code (
@riverpod) - freezed - Generates immutable model classes (
@freezed) - slang_build_runner - Generates i18n translations from JSON
*.g.dart- Riverpod providers and JSON serialization*.freezed.dart- Freezed immutable classes**/i18n/strings*.g.dart- Internationalization
You MUST regenerate code when:
- After cloning the repository (first time)
- After pulling changes affecting generated files
- After modifying any file with
@freezedor@riverpodannotations - After modifying i18n JSON files
- When switching branches
melos run build_runner --no-select-
Follow Flux Pattern:
- Create actions in
action/ - Create action creator
- Create store with Freezed state
- Create viewmodel with Input/Output interface
- Create view using
StatefulHookConsumerWidget
- Create actions in
-
Use Freezed for State:
@freezed abstract class MyFeatureState with _$MyFeatureState { const factory MyFeatureState({ @Default(false) bool isLoading, @Default(null) String? error, }) = _MyFeatureState; }
-
Add i18n Keys:
- Add keys to
lib/i18n/ja.i18n.json - Run build_runner to generate
- Use
t.keyNamein UI
- Add keys to
-
Implement Error Handling:
- Catch exceptions in store methods
- Update state with error message
- Display user-friendly error in UI
-
Write Defensive Code:
- Use null-safe casting:
(data['field'] as Type?) ?? fallback - Handle empty states gracefully
- Validate user input
- Use null-safe casting:
Async Operations in Stores:
void _fetchData(FetchDataAction action) async {
state = state.copyWith(isLoading: true);
try {
final repository = await ref.watch(repositoryProvider.future);
final result = await repository.fetchData();
state = state.copyWith(data: result, isLoading: false);
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
}
}Scroll Detection for Pagination:
final scrollController = useScrollController();
useEffect(() {
void onScroll() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (viewModel.state.canLoadMore) {
viewModel.onLoadMore();
}
}
}
scrollController.addListener(onScroll);
return () => scrollController.removeListener(onScroll);
}, [scrollController, viewModel.state.canLoadMore]);- Flutter: 3.35.7 (stable channel)
- Dart: 3.9.2
- SDK constraint:
^3.9.2 - Melos: 7.3.0
- IMPORTANT: After cloning, run:
melos bootstrap melos run build_runner --no-select
- Use
melos run getinstead offlutter pub get - For build_runner commands, always add
--no-selectflag - Run
melos listto see all packages - Check available scripts with
melos run --list
- Cause: Using
ref.read()instead ofref.watch()with async providers in stores - Solution: Use
ref.watch(provider.future)in store methods
- Cause: GraphQL API returning null for required fields
- Solution: Use defensive casting:
(data['field'] as String?) ?? ''
- Solution:
melos clean melos bootstrap melos run build_runner --no-select