This is the initial commit for the CODEX_ADK project, a full-stack AI agent management platform featuring: BACKEND (ASP.NET Core 8.0): - CQRS architecture with 6 commands and 7 queries - 16 API endpoints (all working and tested) - PostgreSQL database with 5 entities - AES-256 encryption for API keys - FluentValidation on all commands - Rate limiting and CORS configured - OpenAPI/Swagger documentation - Docker Compose setup (PostgreSQL + Ollama) FRONTEND (Flutter 3.x): - Dark theme with Svrnty branding - Collapsible sidebar navigation - CQRS API client with Result<T> error handling - Type-safe endpoints from OpenAPI schema - Multi-platform support (Web, iOS, Android, macOS, Linux, Windows) DOCUMENTATION: - Comprehensive API reference - Architecture documentation - Development guidelines for Claude Code - API integration guides - context-claude.md project overview Status: Backend ready (Grade A-), Frontend integration pending 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
962 lines
22 KiB
Markdown
962 lines
22 KiB
Markdown
# 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
|