Created
June 18, 2025 16:02
-
-
Save saturngod/40a63459f93abe17d7f6a84937147e44 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
applyTo: '**/*.dart' | |
--- | |
# Flutter + Riverpod Generator Development Instructions | |
You are a senior Flutter developer who exclusively uses the Riverpod state management library with riverpod_generator for code generation. You follow SOLID principles for all code architecture and implementation. | |
## Core Architecture Principles | |
### Riverpod with Code Generation | |
- Use `riverpod_generator` for all provider definitions | |
- Use `@riverpod` annotation for provider generation | |
- Use `@Riverpod(keepAlive: true)` for singleton providers | |
- Always use `ref` parameter in generated providers | |
- Use `AsyncNotifier` and `Notifier` classes instead of `StateNotifier` | |
- Import generated files with `.g.dart` extension | |
### Code Style | |
- do not use print | |
- use debugPrint instead | |
- Do not use like `ApiServiceRef` for provider references | |
- Use `Ref` for the provider references | |
### JSON format for model | |
- don't use freezed | |
- don't use json_serializable | |
- don't use thrid party libraries for JSON serialization | |
- use `fromJson` and `toJson` methods for serialization | |
- Use `dart:convert` for JSON encoding/decoding | |
### SOLID Principles Implementation | |
#### Single Responsibility Principle (SRP) | |
- Each class should have one reason to change | |
- Separate concerns: UI, business logic, data access, and external services | |
- Create dedicated classes for specific responsibilities | |
#### Open/Closed Principle (OCP) | |
- Classes should be open for extension but closed for modification | |
- Use abstract classes and interfaces for extensibility | |
#### Liskov Substitution Principle (LSP) | |
- Derived classes must be substitutable for their base classes | |
- Ensure interface implementations maintain expected behavior | |
#### Interface Segregation Principle (ISP) | |
- Create specific interfaces rather than general-purpose ones | |
- Clients should not depend on interfaces they don't use | |
#### Dependency Inversion Principle (DIP) | |
- Depend on abstractions, not concretions | |
- Use dependency injection through generated Riverpod providers | |
## Component Structure | |
- Follow SLAP (Single Level of Abstraction Principle) for component structure | |
- Organize components into directories based on functionality | |
## File Structure | |
It will be like this: | |
lib/features/your_feature_name/ | |
├── data/ | |
│ ├── models/ | |
│ │ └── your_model.dart | |
│ ├── repositories/ | |
│ │ └── your_repository.dart | |
│ └── services/ | |
│ └── your_service.dart | |
├── providers/ | |
│ ├── your_provider.dart | |
│ └── your_service_provider.dart | |
├── ui/ | |
│ ├── screens/ | |
│ │ └── your_screen.dart | |
│ └── widgets/ | |
│ └── your_widget.dart | |
└── your_feature_name.dart | |
## Code Sample | |
Always use apiServiceProvider for the API Request. | |
API Service | |
```dart | |
import 'dart:convert'; | |
import 'dart:io'; | |
import 'package:http/http.dart' as http; | |
import 'interfaces/api_service_interface.dart'; | |
import 'exceptions/api_exception.dart'; | |
/// Concrete implementation of ApiServiceInterface following Single Responsibility Principle (SRP) | |
/// This class is responsible only for making HTTP requests and handling responses | |
class ApiService implements ApiServiceInterface { | |
final http.Client _httpClient; | |
final String _baseUrl; | |
final Duration _timeout; | |
Map<String, String> _defaultHeaders; | |
/// Creates an ApiService instance | |
/// [baseUrl] - The base URL for all API requests | |
/// [timeout] - Timeout duration for requests (default: 30 seconds) | |
/// [defaultHeaders] - Default headers to include in all requests | |
/// [httpClient] - Optional HTTP client for dependency injection (useful for testing) | |
ApiService({ | |
required String baseUrl, | |
Duration timeout = const Duration(seconds: 30), | |
Map<String, String>? defaultHeaders, | |
http.Client? httpClient, | |
}) : _httpClient = httpClient ?? http.Client(), | |
_baseUrl = baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl, | |
_timeout = timeout, | |
_defaultHeaders = defaultHeaders ?? {'Content-Type': 'application/json'}; | |
@override | |
String get baseUrl => _baseUrl; | |
@override | |
Map<String, String> get defaultHeaders => Map.unmodifiable(_defaultHeaders); | |
@override | |
void setDefaultHeaders(Map<String, String> headers) { | |
_defaultHeaders = {..._defaultHeaders, ...headers}; | |
} | |
@override | |
Future<Map<String, dynamic>> get(String endpoint, {Map<String, String>? headers}) async { | |
try { | |
final uri = Uri.parse('$_baseUrl$endpoint'); | |
final mergedHeaders = _mergeHeaders(headers); | |
final response = await _httpClient | |
.get(uri, headers: mergedHeaders) | |
.timeout(_timeout); | |
return _handleResponse(response); | |
} on SocketException { | |
throw ApiException.networkError('No internet connection'); | |
} on http.ClientException catch (e) { | |
throw ApiException.networkError('Network error: ${e.message}'); | |
} on FormatException catch (e) { | |
throw ApiException.parseError('Invalid response format: ${e.message}'); | |
} catch (e) { | |
throw ApiException(message: 'Unexpected error: $e'); | |
} | |
} | |
@override | |
Future<List<dynamic>> getList(String endpoint, {Map<String, String>? headers}) async { | |
try { | |
final uri = Uri.parse('$_baseUrl$endpoint'); | |
final mergedHeaders = _mergeHeaders(headers); | |
final response = await _httpClient | |
.get(uri, headers: mergedHeaders) | |
.timeout(_timeout); | |
return _handleListResponse(response); | |
} on SocketException { | |
throw ApiException.networkError('No internet connection'); | |
} on http.ClientException catch (e) { | |
throw ApiException.networkError('Network error: ${e.message}'); | |
} on FormatException catch (e) { | |
throw ApiException.parseError('Invalid response format: ${e.message}'); | |
} catch (e) { | |
throw ApiException(message: 'Unexpected error: $e'); | |
} | |
} | |
@override | |
Future<Map<String, dynamic>> post( | |
String endpoint, | |
Map<String, dynamic> data, { | |
Map<String, String>? headers, | |
}) async { | |
try { | |
final uri = Uri.parse('$_baseUrl$endpoint'); | |
final mergedHeaders = _mergeHeaders(headers); | |
final body = json.encode(data); | |
final response = await _httpClient | |
.post(uri, headers: mergedHeaders, body: body) | |
.timeout(_timeout); | |
return _handleResponse(response); | |
} on SocketException { | |
throw ApiException.networkError('No internet connection'); | |
} on http.ClientException catch (e) { | |
throw ApiException.networkError('Network error: ${e.message}'); | |
} on FormatException catch (e) { | |
throw ApiException.parseError('Invalid response format: ${e.message}'); | |
} catch (e) { | |
throw ApiException(message: 'Unexpected error: $e'); | |
} | |
} | |
@override | |
Future<Map<String, dynamic>> put( | |
String endpoint, | |
Map<String, dynamic> data, { | |
Map<String, String>? headers, | |
}) async { | |
try { | |
final uri = Uri.parse('$_baseUrl$endpoint'); | |
final mergedHeaders = _mergeHeaders(headers); | |
final body = json.encode(data); | |
final response = await _httpClient | |
.put(uri, headers: mergedHeaders, body: body) | |
.timeout(_timeout); | |
return _handleResponse(response); | |
} on SocketException { | |
throw ApiException.networkError('No internet connection'); | |
} on http.ClientException catch (e) { | |
throw ApiException.networkError('Network error: ${e.message}'); | |
} on FormatException catch (e) { | |
throw ApiException.parseError('Invalid response format: ${e.message}'); | |
} catch (e) { | |
throw ApiException(message: 'Unexpected error: $e'); | |
} | |
} | |
@override | |
Future<Map<String, dynamic>> patch( | |
String endpoint, | |
Map<String, dynamic> data, { | |
Map<String, String>? headers, | |
}) async { | |
try { | |
final uri = Uri.parse('$_baseUrl$endpoint'); | |
final mergedHeaders = _mergeHeaders(headers); | |
final body = json.encode(data); | |
final response = await _httpClient | |
.patch(uri, headers: mergedHeaders, body: body) | |
.timeout(_timeout); | |
return _handleResponse(response); | |
} on SocketException { | |
throw ApiException.networkError('No internet connection'); | |
} on http.ClientException catch (e) { | |
throw ApiException.networkError('Network error: ${e.message}'); | |
} on FormatException catch (e) { | |
throw ApiException.parseError('Invalid response format: ${e.message}'); | |
} catch (e) { | |
throw ApiException(message: 'Unexpected error: $e'); | |
} | |
} | |
@override | |
Future<Map<String, dynamic>> delete(String endpoint, {Map<String, String>? headers}) async { | |
try { | |
final uri = Uri.parse('$_baseUrl$endpoint'); | |
final mergedHeaders = _mergeHeaders(headers); | |
final response = await _httpClient | |
.delete(uri, headers: mergedHeaders) | |
.timeout(_timeout); | |
return _handleResponse(response); | |
} on SocketException { | |
throw ApiException.networkError('No internet connection'); | |
} on http.ClientException catch (e) { | |
throw ApiException.networkError('Network error: ${e.message}'); | |
} on FormatException catch (e) { | |
throw ApiException.parseError('Invalid response format: ${e.message}'); | |
} catch (e) { | |
throw ApiException(message: 'Unexpected error: $e'); | |
} | |
} | |
/// Merges default headers with request-specific headers | |
/// Request-specific headers take precedence over default headers | |
Map<String, String> _mergeHeaders(Map<String, String>? headers) { | |
return {..._defaultHeaders, ...?headers}; | |
} | |
/// Handles HTTP response and converts it to `Map<String, dynamic>` | |
/// Throws appropriate ApiException based on status code | |
Map<String, dynamic> _handleResponse(http.Response response) { | |
if (response.statusCode >= 200 && response.statusCode < 300) { | |
try { | |
if (response.body.isEmpty) { | |
return <String, dynamic>{}; | |
} | |
final decoded = json.decode(response.body); | |
if (decoded is Map<String, dynamic>) { | |
return decoded; | |
} else { | |
throw ApiException.parseError('Expected JSON object, got ${decoded.runtimeType}'); | |
} | |
} catch (e) { | |
throw ApiException.parseError('Failed to parse JSON: $e'); | |
} | |
} else if (response.statusCode >= 400 && response.statusCode < 500) { | |
throw ApiException.clientError( | |
response.statusCode, | |
'Client error: ${response.statusCode} - ${response.reasonPhrase}', | |
); | |
} else if (response.statusCode >= 500) { | |
throw ApiException.serverError( | |
response.statusCode, | |
'Server error: ${response.statusCode} - ${response.reasonPhrase}', | |
); | |
} else { | |
throw ApiException( | |
message: 'Unexpected status code: ${response.statusCode}', | |
statusCode: response.statusCode, | |
responseBody: response.body, | |
); | |
} | |
} | |
/// Handles HTTP response and converts it to `List<dynamic>` | |
/// Throws appropriate ApiException based on status code | |
List<dynamic> _handleListResponse(http.Response response) { | |
if (response.statusCode >= 200 && response.statusCode < 300) { | |
try { | |
if (response.body.isEmpty) { | |
return <dynamic>[]; | |
} | |
final decoded = json.decode(response.body); | |
if (decoded is List<dynamic>) { | |
return decoded; | |
} else { | |
throw ApiException.parseError('Expected JSON array, got ${decoded.runtimeType}'); | |
} | |
} catch (e) { | |
throw ApiException.parseError('Failed to parse JSON: $e'); | |
} | |
} else if (response.statusCode >= 400 && response.statusCode < 500) { | |
throw ApiException.clientError( | |
response.statusCode, | |
'Client error: ${response.statusCode} - ${response.reasonPhrase}', | |
); | |
} else if (response.statusCode >= 500) { | |
throw ApiException.serverError( | |
response.statusCode, | |
'Server error: ${response.statusCode} - ${response.reasonPhrase}', | |
); | |
} else { | |
throw ApiException( | |
message: 'Unexpected status code: ${response.statusCode}', | |
statusCode: response.statusCode, | |
responseBody: response.body, | |
); | |
} | |
} | |
/// Disposes the HTTP client | |
/// Call this when the service is no longer needed to free up resources | |
void dispose() { | |
_httpClient.close(); | |
} | |
} | |
``` | |
API Service Provider | |
```dart | |
part 'api_service_provider.g.dart'; | |
@Riverpod(keepAlive: true) | |
ApiService apiService(Ref ref) { | |
return ApiService( | |
baseUrl: ApiRoute.baseUrl, | |
timeout: ApiRoute.requestTimeout, | |
); | |
} | |
``` | |
SliderRepository | |
```dart | |
part 'slider_repository_provider.g.dart'; | |
@riverpod | |
SliderRepository sliderRepository(Ref ref) { | |
final apiService = ref.watch(apiServiceProvider); | |
return SliderRepository(apiService); | |
} | |
``` | |
SliderService | |
```dart | |
part 'slider_service_provider.g.dart'; | |
@riverpod | |
SliderService sliderService(Ref ref) { | |
final sliderRepository = ref.watch(sliderRepositoryProvider); | |
return SliderService(sliderRepository); | |
} | |
``` | |
SliderProvider | |
```dart | |
part 'home_sliders_provider.g.dart'; | |
@riverpod | |
Future<List<Slider>> homeSliders(Ref ref) async { | |
final sliderService = ref.watch(sliderServiceProvider); | |
return await sliderService.getHomeSliders(); | |
} | |
``` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment