Skip to content

Latest commit

 

History

History
442 lines (345 loc) · 12.1 KB

File metadata and controls

442 lines (345 loc) · 12.1 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Hermes - A Flutter application demonstrating GitHub user search with infinite scroll pagination, built using clean architecture and the Flux pattern.

Project Structure

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

Melos Workspace

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.

Development Commands

Melos Commands (Recommended)

IMPORTANT: Always use Melos commands for managing the monorepo. Melos is installed globally and must be in your PATH.

Setup

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"

Monorepo Management

# 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

Code Generation

# 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-select

Note: Always use the --no-select flag with build_runner commands to avoid interactive prompts.

Code Quality

# Run flutter analyze on all packages
melos run analyze

# Format all Dart files in workspace
melos run format

Running the Application

From the root directory:

flutter run

For specific build modes:

flutter run --profile  # Profile mode
flutter run --release  # Release mode

Testing

Run tests using Melos (runs tests in all packages):

melos exec -- flutter test

Or run tests in a specific package:

cd packages/feature/home
flutter test

Building

Build 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          # Web

Architecture

Flux Pattern (Primary)

This project uses the Flux architecture pattern for unidirectional data flow:

User Input → ViewModel → ActionCreator → Dispatcher → Store → State Update → ViewModel → UI

Key Components:

  1. 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);
      }
  2. Action Creators (action/)

    • Create and dispatch actions to the Dispatcher
    • Example:
      class GitHubSearchActionCreator extends ActionCreator {
        void searchUsers(String username) {
          dispatch(SearchUsersAction(username));
        }
      }
  3. Stores (store/)

    • Extend EventStore and manage feature state
    • Listen to actions from Dispatcher via onAction()
    • Use Freezed for immutable state
    • Example:
      class GitHubSearchStore extends EventStore<GitHubSearchState> {
        @override
        void onAction(EventAction action) {
          switch (action) {
            case SearchUsersAction():
              _searchUsers(action);
              break;
          }
        }
      }
  4. 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);
        }
      }
  5. Views (view/)

    • UI components using StatefulHookConsumerWidget
    • Watch viewmodel for state updates
    • Call viewmodel methods for user interactions

State Management

  • Riverpod for dependency injection
  • Freezed for immutable state classes
  • Hooks for lifecycle management (useEffect, useScrollController, etc.)

Theme System

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 including seedColor

The main app imports and applies the theme in lib/main.dart:

import 'package:ui/theme/theme.dart';

MaterialApp(
  theme: hermesLightTheme,
  darkTheme: hermesDarkTheme,
  ...
)

Features

GitHub User Search

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:

  1. Pagination

    • Uses LoadMoreUsersAction to trigger loading more results
    • Store tracks currentQuery and endCursor for pagination
    • View uses useScrollController to detect scroll position
    • Loads more when within 200px of bottom
    • Shows loading indicator at end of list
  2. 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
  3. i18n Support

    • Uses slang for internationalization
    • Base locale: ja (Japanese)
    • Translations in lib/i18n/ja.i18n.json
    • Generated code in lib/i18n/strings.g.dart and strings_ja.g.dart

Repository Pattern

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);

Environment Configuration

The project uses environment variables for configuration:

File: packages/core/.env

GITHUB_BASE_URL=https://api.github.com/graphql
GITHUB_TOKEN=ghp_YourTokenHere

Loading: Uses flutter_dotenv to load environment variables Access: Via EnvironmentSettings model with Freezed

⚠️ Never commit .env files to version control!

Code Generation

Generators Used

  1. riverpod_generator - Generates provider code (@riverpod)
  2. freezed - Generates immutable model classes (@freezed)
  3. slang_build_runner - Generates i18n translations from JSON

Generated Files (NOT committed to git)

  • *.g.dart - Riverpod providers and JSON serialization
  • *.freezed.dart - Freezed immutable classes
  • **/i18n/strings*.g.dart - Internationalization

When to Regenerate

You MUST regenerate code when:

  1. After cloning the repository (first time)
  2. After pulling changes affecting generated files
  3. After modifying any file with @freezed or @riverpod annotations
  4. After modifying i18n JSON files
  5. When switching branches

How to Regenerate

melos run build_runner --no-select

Development Guidelines

When Creating New Features

  1. 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
  2. Use Freezed for State:

    @freezed
    abstract class MyFeatureState with _$MyFeatureState {
      const factory MyFeatureState({
        @Default(false) bool isLoading,
        @Default(null) String? error,
      }) = _MyFeatureState;
    }
  3. Add i18n Keys:

    • Add keys to lib/i18n/ja.i18n.json
    • Run build_runner to generate
    • Use t.keyName in UI
  4. Implement Error Handling:

    • Catch exceptions in store methods
    • Update state with error message
    • Display user-friendly error in UI
  5. Write Defensive Code:

    • Use null-safe casting: (data['field'] as Type?) ?? fallback
    • Handle empty states gracefully
    • Validate user input

Common Patterns

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]);

Environment

  • Flutter: 3.35.7 (stable channel)
  • Dart: 3.9.2
  • SDK constraint: ^3.9.2
  • Melos: 7.3.0

Tips for Using Melos

  • IMPORTANT: After cloning, run:
    melos bootstrap
    melos run build_runner --no-select
  • Use melos run get instead of flutter pub get
  • For build_runner commands, always add --no-select flag
  • Run melos list to see all packages
  • Check available scripts with melos run --list

Troubleshooting

"Cannot use the Ref after it has been disposed"

  • Cause: Using ref.read() instead of ref.watch() with async providers in stores
  • Solution: Use ref.watch(provider.future) in store methods

Null Type Cast Errors

  • Cause: GraphQL API returning null for required fields
  • Solution: Use defensive casting: (data['field'] as String?) ?? ''

Code Generation Fails

  • Solution:
    melos clean
    melos bootstrap
    melos run build_runner --no-select