1.Dio HTTPクライアント
dependencies:
dio: ^5.4.0
json_annotation: ^4.8.0
retrofit: ^4.1.0
dev_dependencies:
json_serializable: ^6.7.0
retrofit_generator: ^8.1.0
build_runner: ^2.4.0
class ApiClient {
late final Dio _dio;
ApiClient({required String baseUrl, String? token}) {
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
headers: {'Content-Type': 'application/json'},
));
_dio.interceptors.addAll([
LogInterceptor(requestBody: true, responseBody: true),
InterceptorsWrapper(
onRequest: (options, handler) {
if (token != null) options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
// Refresh token logic
}
handler.next(error);
},
),
]);
}
}
2. JSON シリアル化
import 'package:json_annotation/json_annotation.dart';
part 'product.g.dart';
@JsonSerializable()
class Product {
final String id;
final String name;
final double price;
@JsonKey(name: 'created_at')
final DateTime createdAt;
@JsonKey(name: 'image_url')
final String? imageUrl;
const Product({
required this.id,
required this.name,
required this.price,
required this.createdAt,
this.imageUrl,
});
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
}
3. リポジトリパターン
class ProductRepository {
final Dio _dio;
ProductRepository(this._dio);
Future<List<Product>> getProducts({int page = 1, int limit = 20}) async {
try {
final response = await _dio.get('/products', queryParameters: {
'page': page,
'limit': limit,
});
return (response.data['data'] as List)
.map((json) => Product.fromJson(json))
.toList();
} on DioException catch (e) {
throw ApiException.fromDioError(e);
}
}
}
class ApiException implements Exception {
final String message;
final int? statusCode;
ApiException(this.message, {this.statusCode});
factory ApiException.fromDioError(DioException e) {
return switch (e.type) {
DioExceptionType.connectionTimeout => ApiException('Kết nối timeout'),
DioExceptionType.receiveTimeout => ApiException('Server phản hồi chậm'),
_ => ApiException(e.response?.data?['message'] ?? 'Lỗi không xác định',
statusCode: e.response?.statusCode),
};
}
}
4. Riverpod による無限スクロール
class ProductListNotifier extends StateNotifier<AsyncValue<List<Product>>> {
final ProductRepository _repo;
int _page = 1;
bool _hasMore = true;
ProductListNotifier(this._repo) : super(const AsyncLoading()) {
loadMore();
}
Future<void> loadMore() async {
if (!_hasMore) return;
try {
final products = await _repo.getProducts(page: _page);
_hasMore = products.length == 20;
_page++;
state = AsyncData([
...state.valueOrNull ?? [],
...products,
]);
} catch (e, st) {
state = AsyncError(e, st);
}
}
}
// UI — NotificationListener
NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification.metrics.pixels >= notification.metrics.maxScrollExtent - 200) {
ref.read(productListProvider.notifier).loadMore();
}
return false;
},
child: ListView.builder(
itemCount: products.length,
itemBuilder: (_, i) => ProductCard(product: products[i]),
),
);
次の記事: ローカルストレージとオフラインファースト。