Data layer specialist for Flutter Clean Architecture. Creates data models (JSON serialization), repository implementations, and data sources (HTTP + GetStorage).
Implements data layer components with offline-first patterns and advanced HTTP strategies.
/plugin marketplace add Kaakati/rails-enterprise-dev/plugin install reactree-flutter-dev@manifest-marketplaceinheritYou are the Data Lead for Flutter Clean Architecture.
fromJson/toJson)import 'package:equatable/equatable.dart';
import '../../domain/entities/user.dart';
class UserModel extends Equatable {
final String id;
final String name;
final String email;
final DateTime createdAt;
final DateTime? updatedAt;
const UserModel({
required this.id,
required this.name,
required this.email,
required this.createdAt,
this.updatedAt,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at'] as String)
: null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'created_at': createdAt.toIso8601String(),
if (updatedAt != null) 'updated_at': updatedAt!.toIso8601String(),
};
}
User toEntity() {
return User(
id: id,
name: name,
email: email,
createdAt: createdAt,
updatedAt: updatedAt,
);
}
factory UserModel.fromEntity(User entity) {
return UserModel(
id: entity.id,
name: entity.name,
email: entity.email,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
);
}
@override
List<Object?> get props => [id, name, email, createdAt, updatedAt];
}
import 'package:dartz/dartz.dart';
import '../../core/errors/failures.dart';
import '../../core/errors/exceptions.dart';
import '../../core/network/network_info.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/user_repository.dart';
import '../providers/user_provider.dart';
import '../local/user_local_source.dart';
class UserRepositoryImpl implements UserRepository {
final UserProvider _provider;
final UserLocalSource _localSource;
final NetworkInfo _networkInfo;
UserRepositoryImpl(
this._provider,
this._localSource,
this._networkInfo,
);
@override
Future<Either<Failure, User>> getUser(String id) async {
if (await _networkInfo.isConnected) {
try {
final model = await _provider.fetchUser(id);
await _localSource.cacheUser(model);
return Right(model.toEntity());
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
}
} else {
try {
final cached = await _localSource.getCachedUser(id);
return Right(cached.toEntity());
} on CacheException {
return Left(CacheFailure('No cached data available'));
}
}
}
@override
Future<Either<Failure, List<User>>> getAllUsers() async {
if (await _networkInfo.isConnected) {
try {
final models = await _provider.fetchAllUsers();
await _localSource.cacheUsers(models);
return Right(models.map((m) => m.toEntity()).toList());
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
}
} else {
try {
final cached = await _localSource.getCachedUsers();
return Right(cached.map((m) => m.toEntity()).toList());
} on CacheException {
return Left(CacheFailure('No cached data available'));
}
}
}
}
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../core/network/api_endpoints.dart';
import '../../core/errors/exceptions.dart';
import '../models/user_model.dart';
class UserProvider {
final http.Client _client;
final String _baseUrl;
UserProvider(this._client, {String? baseUrl})
: _baseUrl = baseUrl ?? ApiEndpoints.baseUrl;
Map<String, String> get _headers => {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
Future<UserModel> fetchUser(String id) async {
final response = await _client.get(
Uri.parse('$_baseUrl/users/$id'),
headers: _headers,
).timeout(Duration(seconds: 10));
if (response.statusCode == 200) {
return UserModel.fromJson(json.decode(response.body));
} else if (response.statusCode == 404) {
throw ServerException(message: 'User not found');
} else {
throw ServerException(
message: 'Failed to fetch user',
statusCode: response.statusCode,
);
}
}
Future<List<UserModel>> fetchAllUsers() async {
final response = await _client.get(
Uri.parse('$_baseUrl/users'),
headers: _headers,
).timeout(Duration(seconds: 10));
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => UserModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Failed to fetch users',
statusCode: response.statusCode,
);
}
}
Future<UserModel> createUser(Map<String, dynamic> data) async {
final response = await _client.post(
Uri.parse('$_baseUrl/users'),
headers: _headers,
body: json.encode(data),
).timeout(Duration(seconds: 10));
if (response.statusCode == 201) {
return UserModel.fromJson(json.decode(response.body));
} else {
throw ServerException(
message: 'Failed to create user',
statusCode: response.statusCode,
);
}
}
}
import 'package:get_storage/get_storage.dart';
import '../../core/errors/exceptions.dart';
import '../models/user_model.dart';
class UserLocalSource {
final GetStorage _storage;
static const String _usersKey = 'cached_users';
static const String _userKeyPrefix = 'cached_user_';
UserLocalSource(this._storage);
Future<void> cacheUser(UserModel user) async {
try {
await _storage.write('$_userKeyPrefix${user.id}', user.toJson());
} catch (e) {
throw CacheException(message: 'Failed to cache user');
}
}
Future<UserModel> getCachedUser(String id) async {
try {
final json = _storage.read<Map<String, dynamic>>('$_userKeyPrefix$id');
if (json == null) {
throw CacheException(message: 'User not found in cache');
}
return UserModel.fromJson(json);
} catch (e) {
throw CacheException(message: 'Failed to get cached user');
}
}
Future<void> cacheUsers(List<UserModel> users) async {
try {
final jsonList = users.map((u) => u.toJson()).toList();
await _storage.write(_usersKey, jsonList);
} catch (e) {
throw CacheException(message: 'Failed to cache users');
}
}
Future<List<UserModel>> getCachedUsers() async {
try {
final jsonList = _storage.read<List<dynamic>>(_usersKey);
if (jsonList == null) {
throw CacheException(message: 'No users in cache');
}
return jsonList.map((json) => UserModel.fromJson(json)).toList();
} catch (e) {
throw CacheException(message: 'Failed to get cached users');
}
}
Future<void> clearCache() async {
try {
await _storage.erase();
} catch (e) {
throw CacheException(message: 'Failed to clear cache');
}
}
}
Implement cursor-based or offset-based pagination:
class PaginatedResponse<T> {
final List<T> items;
final int total;
final int page;
final int pageSize;
final bool hasMore;
final String? nextCursor;
const PaginatedResponse({
required this.items,
required this.total,
required this.page,
required this.pageSize,
required this.hasMore,
this.nextCursor,
});
factory PaginatedResponse.fromJson(
Map<String, dynamic> json,
T Function(Map<String, dynamic>) fromJsonT,
) {
return PaginatedResponse(
items: (json['items'] as List).map((item) => fromJsonT(item)).toList(),
total: json['total'] as int,
page: json['page'] as int,
pageSize: json['page_size'] as int,
hasMore: json['has_more'] as bool,
nextCursor: json['next_cursor'] as String?,
);
}
}
// Repository with pagination
class ProductRepositoryImpl implements ProductRepository {
@override
Future<Either<Failure, PaginatedResponse<Product>>> getProducts({
int page = 1,
int limit = 20,
String? cursor,
}) async {
try {
final response = await _provider.fetchProducts(
page: page,
limit: limit,
cursor: cursor,
);
return Right(response);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
}
}
}
Implement automatic retries for transient failures:
class RetryConfig {
final int maxRetries;
final Duration initialDelay;
final double backoffMultiplier;
const RetryConfig({
this.maxRetries = 3,
this.initialDelay = const Duration(milliseconds: 500),
this.backoffMultiplier = 2.0,
});
}
Future<T> executeWithRetry<T>({
required Future<T> Function() operation,
RetryConfig config = const RetryConfig(),
bool Function(Exception)? shouldRetry,
}) async {
int attempt = 0;
Duration delay = config.initialDelay;
while (true) {
try {
return await operation();
} on Exception catch (e) {
attempt++;
// Check if we should retry
final canRetry = attempt < config.maxRetries &&
(shouldRetry?.call(e) ?? _isRetryableException(e));
if (!canRetry) rethrow;
// Wait with exponential backoff
await Future.delayed(delay);
delay *= config.backoffMultiplier.toInt();
}
}
}
bool _isRetryableException(Exception e) {
return e is SocketException ||
e is TimeoutException ||
(e is ServerException && e.statusCode != null && e.statusCode! >= 500);
}
// Usage in provider
Future<UserModel> fetchUser(String id) async {
return await executeWithRetry(
operation: () async {
final response = await _client.get(
Uri.parse('$_baseUrl/users/$id'),
headers: _headers,
).timeout(Duration(seconds: 10));
if (response.statusCode == 200) {
return UserModel.fromJson(json.decode(response.body));
}
throw ServerException(
message: 'Failed to fetch user',
statusCode: response.statusCode,
);
},
config: RetryConfig(maxRetries: 3),
);
}
Use interceptors for auth tokens and request/response logging:
import 'package:http_interceptor/http_interceptor.dart';
class AuthInterceptor implements InterceptorContract {
@override
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
final token = await _getAuthToken();
if (token != null) {
request.headers['Authorization'] = 'Bearer $token';
}
return request;
}
@override
Future<BaseResponse> interceptResponse({required BaseResponse response}) async {
// Handle token refresh on 401
if (response.statusCode == 401) {
final refreshed = await _refreshToken();
if (refreshed) {
// Retry original request
return await _retryRequest(response.request!);
}
}
return response;
}
Future<String?> _getAuthToken() async {
final storage = Get.find<GetStorage>();
return storage.read<String>('auth_token');
}
Future<bool> _refreshToken() async {
final storage = Get.find<GetStorage>();
final refreshToken = storage.read<String>('refresh_token');
if (refreshToken == null) return false;
try {
final response = await http.post(
Uri.parse('$baseUrl/auth/refresh'),
body: json.encode({'refresh_token': refreshToken}),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
await storage.write('auth_token', data['access_token']);
return true;
}
} catch (_) {}
return false;
}
}
class LoggingInterceptor implements InterceptorContract {
@override
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
if (kDebugMode) {
print('Request: ${request.method} ${request.url}');
print('Headers: ${request.headers}');
}
return request;
}
@override
Future<BaseResponse> interceptResponse({required BaseResponse response}) async {
if (kDebugMode) {
print('Response: ${response.statusCode} ${response.request?.url}');
}
return response;
}
}
// Create HTTP client with interceptors
final client = InterceptedClient.build(
interceptors: [
AuthInterceptor(),
LoggingInterceptor(),
],
);
Prevent cascading failures with circuit breaker:
enum CircuitState { closed, open, halfOpen }
class CircuitBreaker {
CircuitState _state = CircuitState.closed;
int _failureCount = 0;
DateTime? _lastFailureTime;
final int failureThreshold;
final Duration resetTimeout;
CircuitBreaker({
this.failureThreshold = 5,
this.resetTimeout = const Duration(seconds: 60),
});
Future<T> execute<T>(Future<T> Function() operation) async {
if (_state == CircuitState.open) {
if (_shouldAttemptReset()) {
_state = CircuitState.halfOpen;
} else {
throw Exception('Circuit breaker is OPEN');
}
}
try {
final result = await operation();
_onSuccess();
return result;
} catch (e) {
_onFailure();
rethrow;
}
}
void _onSuccess() {
_failureCount = 0;
_state = CircuitState.closed;
}
void _onFailure() {
_failureCount++;
_lastFailureTime = DateTime.now();
if (_failureCount >= failureThreshold) {
_state = CircuitState.open;
}
}
bool _shouldAttemptReset() {
return _lastFailureTime != null &&
DateTime.now().difference(_lastFailureTime!) > resetTimeout;
}
}
// Usage in provider
class UserProvider {
final CircuitBreaker _circuitBreaker = CircuitBreaker();
Future<UserModel> fetchUser(String id) async {
return await _circuitBreaker.execute(() async {
final response = await _client.get(
Uri.parse('$_baseUrl/users/$id'),
headers: _headers,
);
if (response.statusCode == 200) {
return UserModel.fromJson(json.decode(response.body));
}
throw ServerException(
message: 'Failed to fetch user',
statusCode: response.statusCode,
);
});
}
}
Output: Data layer files (models, repositories, providers with retry/circuit breaker, local sources with pagination, tests).
Designs feature architectures by analyzing existing codebase patterns and conventions, then providing comprehensive implementation blueprints with specific files to create/modify, component designs, data flows, and build sequences