# Flutter Frontend: Complete API Integration Guide

## Objective
Integrate your Flutter app with the Codex CQRS-based ASP.NET Core API using type-safe Dart clients generated from OpenAPI specification.

---

## Prerequisites

- Flutter SDK 3.0+ installed
- Dart 3.0+
- Git access to backend repository
- VS Code with Flutter/Dart extensions or Android Studio
- Basic understanding of REST APIs and Dart

---

## Architecture Overview

The Codex API uses **CQRS pattern** with OpenHarbor.CQRS framework:

- **Queries** (Read): `POST /api/query/{queryName}` or `GET /api/query/{queryName}`
- **Commands** (Write): `POST /api/command/{commandName}`
- **Dynamic Queries** (Paginated): `POST /api/dynamicquery/{itemType}`

All requests use JSON payloads (even for empty requests).

---

## Step 1: Access OpenAPI Specification

### Get the Spec File

```bash
# Option A: Clone backend repository
git clone <backend-repo-url>
cd backend

# The spec is located at: docs/openapi.json

# Option B: Download from running API
curl http://localhost:5246/swagger/v1/swagger.json -o openapi.json
```

### Review Documentation
Before coding, read:
- `backend/docs/ARCHITECTURE.md` - Understand CQRS patterns
- `backend/docs/CHANGELOG.md` - Check for breaking changes
- `backend/docs/openapi.json` - Your API contract

---

## Step 2: Generate Dart API Client

### Install OpenAPI Generator

```bash
# Install via Homebrew (macOS)
brew install openapi-generator

# Or via npm
npm install -g @openapitools/openapi-generator-cli

# Or use Docker
docker pull openapitools/openapi-generator-cli
```

### Generate Dart Client

```bash
# Navigate to your Flutter project
cd /path/to/your/flutter-app

# Create API directory
mkdir -p lib/api

# Generate Dart client
openapi-generator-cli generate \
  -i ../backend/docs/openapi.json \
  -g dart \
  -o lib/api/generated \
  --additional-properties=pubName=codex_api_client,pubLibrary=codex_api_client

# If using Docker:
docker run --rm \
  -v ${PWD}:/local openapitools/openapi-generator-cli generate \
  -i /local/../backend/docs/openapi.json \
  -g dart \
  -o /local/lib/api/generated \
  --additional-properties=pubName=codex_api_client,pubLibrary=codex_api_client
```

This generates:
- `lib/api/generated/lib/api.dart` - API client
- `lib/api/generated/lib/model/` - DTOs and models
- `lib/api/generated/doc/` - API documentation

---

## Step 3: Configure Flutter Project

### Update pubspec.yaml

Add required dependencies:

```yaml
name: your_flutter_app
description: Your Flutter app description

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  # HTTP client
  http: ^1.1.0

  # JSON serialization
  json_annotation: ^4.8.1

  # State management (choose one)
  flutter_riverpod: ^2.4.9  # Recommended
  # provider: ^6.1.1
  # bloc: ^8.1.3

  # Secure storage for tokens
  flutter_secure_storage: ^9.0.0

  # Environment configuration
  flutter_dotenv: ^5.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  build_runner: ^2.4.7
  json_serializable: ^6.7.1
  flutter_lints: ^3.0.1
  mockito: ^5.4.4

flutter:
  uses-material-design: true

  # Add environment files
  assets:
    - .env
    - .env.development
    - .env.production
```

### Run pub get

```bash
flutter pub get
cd lib/api/generated
flutter pub get
cd ../../..
```

---

## Step 4: Setup Environment Configuration

### Create Environment Files

**`.env.development`:**
```env
API_BASE_URL=http://localhost:5246
API_TIMEOUT=30000
```

**`.env.production`:**
```env
API_BASE_URL=https://api.yourapp.com
API_TIMEOUT=30000
```

**`.env` (default/local):**
```env
API_BASE_URL=http://localhost:5246
API_TIMEOUT=30000
```

### Create Config Class

**`lib/config/api_config.dart`:**
```dart
import 'package:flutter_dotenv/flutter_dotenv.dart';

class ApiConfig {
  static String get baseUrl => dotenv.env['API_BASE_URL'] ?? 'http://localhost:5246';
  static int get timeout => int.parse(dotenv.env['API_TIMEOUT'] ?? '30000');

  static const String apiVersion = 'v1';
}
```

### Initialize in main.dart

**`lib/main.dart`:**
```dart
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Load environment variables
  await dotenv.load(fileName: ".env");

  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Codex App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}
```

---

## Step 5: Create API Client Service

### Create HTTP Client with Interceptors

**`lib/services/api_client.dart`:**
```dart
import 'package:http/http.dart' as http;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../config/api_config.dart';

class ApiClient extends http.BaseClient {
  final http.Client _client = http.Client();
  final FlutterSecureStorage _storage = const FlutterSecureStorage();

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    // Add base URL if not already present
    if (!request.url.toString().startsWith('http')) {
      request = _updateRequestUrl(request);
    }

    // Add authentication token
    final token = await _storage.read(key: 'auth_token');
    if (token != null && token.isNotEmpty) {
      request.headers['Authorization'] = 'Bearer $token';
    }

    // Add default headers
    request.headers['Content-Type'] = 'application/json';
    request.headers['Accept'] = 'application/json';

    print('🌐 ${request.method} ${request.url}');

    try {
      final response = await _client.send(request);
      print('✅ ${response.statusCode} ${request.url}');
      return response;
    } catch (e) {
      print('❌ Request failed: $e');
      rethrow;
    }
  }

  http.BaseRequest _updateRequestUrl(http.BaseRequest request) {
    final newUri = Uri.parse('${ApiConfig.baseUrl}${request.url.path}');

    if (request is http.Request) {
      final newRequest = http.Request(request.method, newUri)
        ..headers.addAll(request.headers)
        ..body = request.body;
      return newRequest;
    }

    throw UnsupportedError('Unsupported request type');
  }

  Future<void> setAuthToken(String token) async {
    await _storage.write(key: 'auth_token', value: token);
  }

  Future<void> clearAuthToken() async {
    await _storage.delete(key: 'auth_token');
  }

  Future<String?> getAuthToken() async {
    return await _storage.read(key: 'auth_token');
  }
}
```

---

## Step 6: Create Service Layer (Repository Pattern)

### Health Query Service Example

**`lib/services/health_service.dart`:**
```dart
import 'package:codex_api_client/api.dart';
import '../models/api_result.dart';
import 'api_client.dart';

class HealthService {
  final ApiClient _apiClient;
  late final DefaultApi _api;

  HealthService(this._apiClient) {
    _api = DefaultApi(_apiClient, ApiConfig.baseUrl);
  }

  /// Check if API is healthy
  Future<ApiResult<bool>> checkHealth() async {
    try {
      // Call POST /api/query/health
      final response = await _api.apiQueryHealthPost(
        healthQuery: HealthQuery(), // Empty query object
      );

      return ApiResult.success(response ?? false);
    } on ApiException catch (e) {
      return ApiResult.failure(
        message: e.message ?? 'Health check failed',
        statusCode: e.code,
      );
    } catch (e) {
      return ApiResult.failure(
        message: 'Network error: $e',
      );
    }
  }
}
```

### Create ApiResult Model

**`lib/models/api_result.dart`:**
```dart
class ApiResult<T> {
  final T? data;
  final String? errorMessage;
  final int? statusCode;
  final Map<String, List<String>>? validationErrors;

  bool get isSuccess => errorMessage == null;
  bool get isFailure => !isSuccess;

  ApiResult.success(this.data)
      : errorMessage = null,
        statusCode = null,
        validationErrors = null;

  ApiResult.failure({
    required String message,
    this.statusCode,
    this.validationErrors,
  })  : data = null,
        errorMessage = message;

  /// Handle the result with callbacks
  R when<R>({
    required R Function(T data) success,
    required R Function(String message) failure,
  }) {
    if (isSuccess && data != null) {
      return success(data as T);
    } else {
      return failure(errorMessage ?? 'Unknown error');
    }
  }
}
```

---

## Step 7: Integrate with Riverpod (State Management)

### Create Providers

**`lib/providers/api_providers.dart`:**
```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/api_client.dart';
import '../services/health_service.dart';

// API Client Provider
final apiClientProvider = Provider<ApiClient>((ref) {
  return ApiClient();
});

// Health Service Provider
final healthServiceProvider = Provider<HealthService>((ref) {
  final apiClient = ref.watch(apiClientProvider);
  return HealthService(apiClient);
});

// Health Check Provider (auto-fetches)
final healthCheckProvider = FutureProvider<bool>((ref) async {
  final healthService = ref.watch(healthServiceProvider);
  final result = await healthService.checkHealth();

  return result.when(
    success: (isHealthy) => isHealthy,
    failure: (_) => false,
  );
});
```

### Use in UI

**`lib/screens/home_screen.dart`:**
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/api_providers.dart';

class HomeScreen extends ConsumerWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final healthCheck = ref.watch(healthCheckProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Codex App'),
        actions: [
          // API Health Indicator
          healthCheck.when(
            data: (isHealthy) => Icon(
              isHealthy ? Icons.check_circle : Icons.error,
              color: isHealthy ? Colors.green : Colors.red,
            ),
            loading: () => const CircularProgressIndicator(),
            error: (_, __) => const Icon(Icons.error, color: Colors.red),
          ),
          const SizedBox(width: 16),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            healthCheck.when(
              data: (isHealthy) => Text(
                isHealthy ? 'API Connected ✅' : 'API Disconnected ❌',
                style: Theme.of(context).textTheme.headlineSmall,
              ),
              loading: () => const CircularProgressIndicator(),
              error: (error, _) => Text('Error: $error'),
            ),
          ],
        ),
      ),
    );
  }
}
```

---

## Step 8: Handle CQRS Commands and Queries

### Example: User Command Service

**`lib/services/user_service.dart`:**
```dart
import 'package:codex_api_client/api.dart';
import '../models/api_result.dart';
import 'api_client.dart';

class UserService {
  final ApiClient _apiClient;
  late final DefaultApi _api;

  UserService(this._apiClient) {
    _api = DefaultApi(_apiClient, ApiConfig.baseUrl);
  }

  /// Create a new user (Command)
  Future<ApiResult<void>> createUser({
    required String username,
    required String email,
    required String password,
  }) async {
    try {
      await _api.apiCommandCreateuserPost(
        createUserCommand: CreateUserCommand(
          username: username,
          email: email,
          password: password,
        ),
      );

      return ApiResult.success(null);
    } on ApiException catch (e) {
      return _handleApiException(e);
    } catch (e) {
      return ApiResult.failure(message: 'Network error: $e');
    }
  }

  /// Get paginated users (Dynamic Query)
  Future<ApiResult<PagedResult<UserDto>>> getUsers({
    int page = 1,
    int pageSize = 20,
    String? searchTerm,
  }) async {
    try {
      final filters = searchTerm != null && searchTerm.isNotEmpty
          ? [
              DynamicQueryFilter(
                path: 'username',
                type: FilterType.contains,
                value: searchTerm,
              ),
            ]
          : <DynamicQueryFilter>[];

      final response = await _api.apiDynamicqueryUserPost(
        dynamicQuery: DynamicQuery(
          page: page,
          pageSize: pageSize,
          filters: filters,
          sorts: [
            DynamicQuerySort(
              path: 'createdAt',
              descending: true,
            ),
          ],
        ),
      );

      return ApiResult.success(response);
    } on ApiException catch (e) {
      return _handleApiException(e);
    } catch (e) {
      return ApiResult.failure(message: 'Network error: $e');
    }
  }

  ApiResult<T> _handleApiException<T>(ApiException e) {
    switch (e.code) {
      case 400:
        // Validation errors from FluentValidation
        return ApiResult.failure(
          message: 'Validation failed',
          statusCode: 400,
          validationErrors: _parseValidationErrors(e.message),
        );
      case 401:
        return ApiResult.failure(
          message: 'Unauthorized. Please log in.',
          statusCode: 401,
        );
      case 403:
        return ApiResult.failure(
          message: 'Permission denied',
          statusCode: 403,
        );
      case 404:
        return ApiResult.failure(
          message: 'Resource not found',
          statusCode: 404,
        );
      case 500:
        return ApiResult.failure(
          message: 'Server error. Please try again.',
          statusCode: 500,
        );
      default:
        return ApiResult.failure(
          message: e.message ?? 'Unknown error',
          statusCode: e.code,
        );
    }
  }

  Map<String, List<String>>? _parseValidationErrors(String? message) {
    // Parse validation errors from response
    // Format depends on backend's error response structure
    return null;
  }
}
```

---

## Step 9: Testing

### Unit Tests

**`test/services/health_service_test.dart`:**
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:your_app/services/health_service.dart';
import 'package:your_app/services/api_client.dart';

@GenerateMocks([ApiClient])
import 'health_service_test.mocks.dart';

void main() {
  late HealthService healthService;
  late MockApiClient mockApiClient;

  setUp(() {
    mockApiClient = MockApiClient();
    healthService = HealthService(mockApiClient);
  });

  group('HealthService', () {
    test('checkHealth returns true when API is healthy', () async {
      // Arrange
      when(mockApiClient.send(any)).thenAnswer(
        (_) async => http.StreamedResponse(
          Stream.value(utf8.encode('true')),
          200,
        ),
      );

      // Act
      final result = await healthService.checkHealth();

      // Assert
      expect(result.isSuccess, true);
      expect(result.data, true);
    });

    test('checkHealth returns failure on error', () async {
      // Arrange
      when(mockApiClient.send(any)).thenThrow(Exception('Network error'));

      // Act
      final result = await healthService.checkHealth();

      // Assert
      expect(result.isFailure, true);
      expect(result.errorMessage, contains('Network error'));
    });
  });
}
```

### Integration Tests

**`integration_test/api_integration_test.dart`:**
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:your_app/services/api_client.dart';
import 'package:your_app/services/health_service.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('API Integration Tests', () {
    late ApiClient apiClient;
    late HealthService healthService;

    setUp(() {
      apiClient = ApiClient();
      healthService = HealthService(apiClient);
    });

    testWidgets('Health check endpoint works', (tester) async {
      // Ensure backend is running on localhost:5246
      final result = await healthService.checkHealth();

      expect(result.isSuccess, true);
      expect(result.data, true);
    });
  });
}
```

### Run Tests

```bash
# Unit tests
flutter test

# Integration tests (requires backend running)
flutter test integration_test/
```

---

## Step 10: Error Handling & User Feedback

### Create Error Display Widget

**`lib/widgets/error_snackbar.dart`:**
```dart
import 'package:flutter/material.dart';
import '../models/api_result.dart';

class ErrorSnackbar {
  static void show(BuildContext context, ApiResult result) {
    if (result.isFailure) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(result.errorMessage ?? 'An error occurred'),
          backgroundColor: Colors.red,
          action: SnackBarAction(
            label: 'Dismiss',
            textColor: Colors.white,
            onPressed: () {
              ScaffoldMessenger.of(context).hideCurrentSnackBar();
            },
          ),
        ),
      );
    }
  }

  static void showValidationErrors(
    BuildContext context,
    Map<String, List<String>>? errors,
  ) {
    if (errors == null || errors.isEmpty) return;

    final errorMessages = errors.entries
        .map((e) => '${e.key}: ${e.value.join(', ')}')
        .join('\n');

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(errorMessages),
        backgroundColor: Colors.orange,
        duration: const Duration(seconds: 5),
      ),
    );
  }
}
```

---

## Step 11: Monitoring API Changes

### Create Update Script

**`scripts/update_api_client.sh`:**
```bash
#!/bin/bash

echo "🔄 Updating API client from backend..."

# Pull latest backend changes
cd ../backend
git pull origin main

# Regenerate Dart client
cd ../flutter-app
openapi-generator-cli generate \
  -i ../backend/docs/openapi.json \
  -g dart \
  -o lib/api/generated \
  --additional-properties=pubName=codex_api_client,pubLibrary=codex_api_client

echo "✅ API client updated!"
echo ""
echo "📝 Next steps:"
echo "1. Check backend/docs/CHANGELOG.md for breaking changes"
echo "2. Run: flutter pub get"
echo "3. Run: flutter test"
echo "4. Fix any compilation errors"
```

Make executable:
```bash
chmod +x scripts/update_api_client.sh
```

---

## Step 12: CI/CD Integration

### GitHub Actions Workflow

**`.github/workflows/flutter_ci.yml`:**
```yaml
name: Flutter CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Flutter app
        uses: actions/checkout@v3

      - name: Checkout backend
        uses: actions/checkout@v3
        with:
          repository: your-org/backend
          path: backend

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.16.0'
          channel: 'stable'

      - name: Install OpenAPI Generator
        run: npm install -g @openapitools/openapi-generator-cli

      - name: Check for API changes
        run: |
          if ! diff -q lib/api/openapi.json backend/docs/openapi.json; then
            echo "⚠️ API changes detected!"
            ./scripts/update_api_client.sh
          fi

      - name: Get dependencies
        run: flutter pub get

      - name: Analyze code
        run: flutter analyze

      - name: Run tests
        run: flutter test

      - name: Build app
        run: flutter build apk --debug
```

---

## Complete Checklist for Flutter Team

### Initial Setup
- [ ] Clone backend repository or access `docs/openapi.json`
- [ ] Review `docs/CHANGELOG.md` for breaking changes
- [ ] Review `docs/ARCHITECTURE.md` for CQRS patterns
- [ ] Install OpenAPI Generator CLI
- [ ] Generate Dart API client
- [ ] Update `pubspec.yaml` with dependencies
- [ ] Run `flutter pub get`
- [ ] Create environment configuration files
- [ ] Setup API client with interceptors

### Service Layer
- [ ] Create `ApiClient` with authentication
- [ ] Create `ApiResult` model for error handling
- [ ] Create service classes (Health, User, etc.)
- [ ] Setup Riverpod providers
- [ ] Implement error handling

### Testing
- [ ] Write unit tests for services
- [ ] Write widget tests for UI
- [ ] Setup integration tests
- [ ] Test authentication flow
- [ ] Test error scenarios (401, 400, 500)

### UI Integration
- [ ] Create error display widgets
- [ ] Implement loading states
- [ ] Add API health indicator
- [ ] Handle validation errors in forms

### DevOps
- [ ] Create API update script
- [ ] Setup CI/CD pipeline
- [ ] Configure environment variables
- [ ] Document team workflow

---

## Common Issues & Solutions

### Issue: "Failed to load .env file"
**Solution:** Ensure `.env` file exists and is listed in `pubspec.yaml` assets.

### Issue: "401 Unauthorized on all requests"
**Solution:** Check that `ApiClient` is properly adding Bearer token to headers.

### Issue: "Connection refused to localhost"
**Solution:**
- On Android emulator: use `http://10.0.2.2:5246`
- On iOS simulator: use `http://localhost:5246`
- Update `API_BASE_URL` in `.env` accordingly

### Issue: "Type errors after regenerating client"
**Solution:** Breaking changes occurred. Review `backend/docs/CHANGELOG.md` and update service layer.

### Issue: "OpenAPI generator fails"
**Solution:** Ensure `openapi.json` is valid JSON. Validate with: `cat openapi.json | python3 -m json.tool`

---

## Android Network Configuration

**`android/app/src/main/AndroidManifest.xml`:**
```xml
<manifest>
    <application
        android:usesCleartextTraffic="true">
        <!-- Your app config -->
    </application>

    <uses-permission android:name="android.permission.INTERNET"/>
</manifest>
```

---

## iOS Network Configuration

**`ios/Runner/Info.plist`:**
```xml
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
```

---

## Support & Resources

- **OpenAPI Spec:** `backend/docs/openapi.json`
- **Breaking Changes:** `backend/docs/CHANGELOG.md`
- **Architecture:** `backend/docs/ARCHITECTURE.md`
- **Backend API (Dev):** `http://localhost:5246`
- **Swagger UI (Dev):** `http://localhost:5246/swagger`

---

**Last Updated:** 2025-01-26
**Backend API Version:** v1
**OpenAPI Version:** 3.0.1
**Recommended Dart Client Generator:** openapi-generator-cli with `dart` generator
