Chuyển đến nội dung chính

Bài 9: HTTP & REST API Integration

Dio HTTP client, interceptors, retry. JSON serialization với json_serializable/freezed. Repository pattern, error handling. Pagination, infinite scroll.

💻 Lập trình — Bài 9 Bài 9: HTTP & REST API Integration

Flutter & Dart: Từ Cơ bản đến Nâng cao

Phần 3: Networking & Data

xdev.asia

1. Dio HTTP Client

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 Serialization

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. Repository Pattern

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. Infinite Scroll với 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]),
  ),
);

Bài tiếp theo: Local Storage & Offline First.