Skip to content

Instantly share code, notes, and snippets.

@saturngod
Created June 18, 2025 16:02
Show Gist options
  • Save saturngod/40a63459f93abe17d7f6a84937147e44 to your computer and use it in GitHub Desktop.
Save saturngod/40a63459f93abe17d7f6a84937147e44 to your computer and use it in GitHub Desktop.
---
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