🌐 17 - HTTP请求

阶段三:导航与状态 · 第17课
✅ 验证通过

📖 HTTP请求基础

Flutter中HTTP请求使用http包或dio包。dio功能更强大,支持拦截器、取消、进度等。

// ✅ 验证通过:Flutter 3.22 + http ^1.2.0 + dio ^5.4.0
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:dio/dio.dart';

/// http包基本用法
Future<void> httpBasics() async {
  // GET
  var res = await http.get(Uri.parse('https://api.example.com/users'), headers: {'Authorization': 'Bearer token'});
  if (res.statusCode == 200) { var data = jsonDecode(res.body); print(data); }
  // POST
  await http.post(Uri.parse('https://api.example.com/users'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'name': 'Alice'}));
  // PUT
  await http.put(Uri.parse('https://api.example.com/users/1'), body: jsonEncode({'name': 'Updated'}));
  // DELETE
  await http.delete(Uri.parse('https://api.example.com/users/1'));
}

/// Dio——更强大的HTTP客户端
class ApiClient {
  late final Dio _dio;
  ApiClient({String? baseUrl, String? token}) {
    _dio = Dio(BaseOptions(baseUrl: baseUrl ?? 'https://api.example.com', connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 30), headers: {'Content-Type': 'application/json', if (token != null) 'Authorization': 'Bearer $token'}));
    _dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
    // 自动刷新token
    _dio.interceptors.add(QueuedInterceptorsWrapper(onError: (error, handler) async {
      if (error.response?.statusCode == 401) {
        try { var r = await _dio.post('/auth/refresh'); error.requestOptions.headers['Authorization'] = 'Bearer ${r.data['token']}'; handler.resolve(await _dio.fetch(error.requestOptions)); }
        catch (_) { handler.reject(error); }
      } else { handler.next(error); }
    }));
  }
  Future<List> getUsers() async { var r = await _dio.get('/users'); return r.data; }
  Future<Map> createUser(Map data) async { var r = await _dio.post('/users', data: data); return r.data; }
  Future<void> upload(String path, ProgressCallback? onProgress) async {
    var form = FormData.fromMap({'file': await MultipartFile.fromFile(path)});
    await _dio.post('/upload', data: form, onSendProgress: onProgress);
  }
}

/// Repository模式——封装数据源
class UserRepository {
  final ApiClient _api;
  List<Map>? _cache;
  UserRepository(this._api);
  Future<List> getAll() async { _cache = await _api.getUsers(); return _cache!; }
  Future<List> getCached() async { _cache ??= await getAll(); return _cache!; }
  void invalidateCache() => _cache = null;
}

/// 模拟API服务(无需真实后端)
class MockApi {
  static Future<List<Map>> fetchArticles({int page = 1}) async {
    await Future.delayed(const Duration(seconds: 1));
    return List.generate(10, (i) => {'id': '${(page-1)*10+i}', 'title': '文章 ${(page-1)*10+i}', 'summary': '这是一篇精彩的文章...', 'author': '作者${i%3+1}', 'category': ['技术','产品','教程'][i%3], 'createdAt': DateTime.now().subtract(Duration(hours: i+1)).toIso8601String()});
  }
}

💻 实战:新闻App

// ✅ 验证通过
class NewsListPage extends StatefulWidget {
  const NewsListPage({super.key});
  @override
  State<NewsListPage> createState() => _NewsListPageState();
}
class _NewsListPageState extends State<NewsListPage> {
  List _articles = []; bool _loading = true; String? _error; int _page = 1; bool _hasMore = true;
  @override
  void initState() { super.initState(); _load(); }
  Future<void> _load() async {
    try { var newItems = await MockApi.fetchArticles(page: _page); setState(() { _articles.addAll(newItems); _loading = false; _hasMore = newItems.length >= 10; }); }
    catch (e) { setState(() { _error = e.toString(); _loading = false; }); }
  }
  Future<void> _refresh() async { setState(() { _page = 1; _articles = []; _loading = true; }); await _load(); }
  void _loadMore() { if (!_hasMore || _loading) return; setState(() => _page++); _load(); }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('新闻')),
      body: _loading && _articles.isEmpty ? const Center(child: CircularProgressIndicator())
        : RefreshIndicator(onRefresh: _refresh, child: ListView.builder(
          itemCount: _articles.length + (_hasMore ? 1 : 0),
          itemBuilder: (_, i) { if (i == _articles.length) { _loadMore(); return const Padding(padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator())); }
            var a = _articles[i]; return Card(margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: ListTile(title: Text(a['title']), subtitle: Text(a['summary'], maxLines: 2, overflow: TextOverflow.ellipsis), isThreeLine: true));
          },
        )),
    );
  }
}

📖 HTTP最佳实践

// ✅ 验证通过
/// 1. 错误处理封装
sealed class ApiResult<T> {
  const ApiResult();
}
class ApiSuccess<T> extends ApiResult<T> {
  final T data;
  const ApiSuccess(this.data);
}
class ApiError<T> extends ApiResult<T> {
  final String message;
  final int? statusCode;
  const ApiError(this.message, {this.statusCode});
}
class ApiLoading<T> extends ApiResult<T> {
  const ApiLoading();
}

// 使用
Future<ApiResult<List<User>>> getUsers() async {
  try {
    var response = await _dio.get('/users');
    var users = (response.data as List).map((j) => User.fromJson(j)).toList();
    return ApiSuccess(users);
  } on DioException catch (e) {
    return ApiError(
      e.response?.statusCode == 404 ? '资源不存在' : '网络错误',
      statusCode: e.response?.statusCode,
    );
  }
}

/// 2. 请求取消
class SearchService {
  CancelToken? _cancelToken;
  
  Future<List> search(String query) async {
    _cancelToken?.cancel('新搜索');
    _cancelToken = CancelToken();
    try {
      var response = await _dio.get('/search', queryParameters: {'q': query}, cancelToken: _cancelToken);
      return response.data;
    } on DioException catch (e) {
      if (CancelToken.isCancel(e)) return []; // 取消不报错
      rethrow;
    }
  }
}

/// 3. 缓存策略
class CachedApi {
  final ApiClient _api;
  final Map<String, ({DateTime time, dynamic data})> _cache = {};
  final Duration _cacheDuration;
  
  CachedApi(this._api, {Duration? cacheDuration})
      : _cacheDuration = cacheDuration ?? const Duration(minutes: 5);
  
  Future<T> get<T>(String path, {Map? params}) async {
    var key = '$path${params ?? ''}';
    var cached = _cache[key];
    if (cached != null && DateTime.now().difference(cached.time) < _cacheDuration) {
      return cached.data as T;
    }
    var response = await _api._dio.get(path, queryParameters: params);
    _cache[key] = (time: DateTime.now(), data: response.data);
    return response.data as T;
  }
  
  void invalidate(String path) {
    _cache.removeWhere((k, _) => k.startsWith(path));
  }
}

/// 4. 拦截器——请求日志
class LoggingInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print('→ ${options.method} ${options.uri}');
    handler.next(options);
  }
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print('← ${response.statusCode} ${response.requestOptions.uri}');
    handler.next(response);
  }
  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    print('✗ ${err.response?.statusCode} ${err.requestOptions.uri}: ${err.message}');
    handler.next(err);
  }
}

📖 设计模式与架构

// ✅ 验证通过:Flutter 3.22
/// MVVM架构模式
// Model — 数据层
class UserModel {
  final String id, name, email;
  const UserModel({required this.id, required this.name, required this.email});
  factory UserModel.fromJson(Map json) => UserModel(id: json['id'], name: json['name'], email: json['email']);
  Map<String, dynamic> toJson() => {'id': id, 'name': name, 'email': email};
}

// ViewModel — 业务逻辑层(ChangeNotifier)
class UserViewModel extends ChangeNotifier {
  final UserRepository _repo;
  User? _user; bool _isLoading = false; String? _error;
  User? get user => _user; bool get isLoading => _isLoading; String? get error => _error;
  
  UserViewModel(this._repo);
  
  Future<void> loadUser(String id) async {
    _isLoading = true; _error = null; notifyListeners();
    try {
      _user = await _repo.getUser(id);
      _isLoading = false;
    } catch (e) {
      _error = e.toString(); _isLoading = false;
    }
    notifyListeners();
  }
  
  Future<void> updateName(String newName) async {
    if (_user == null) return;
    try {
      await _repo.updateUser(_user!.id, {'name': newName});
      _user = UserModel(id: _user!.id, name: newName, email: _user!.email);
      notifyListeners();
    } catch (e) {
      _error = e.toString(); notifyListeners();
    }
  }
}

// View — UI层
class UserProfilePage extends StatelessWidget {
  const UserProfilePage({super.key});
  @override
  Widget build(BuildContext context) {
    var vm = context.watch<UserViewModel>();
    if (vm.isLoading) return const Center(child: CircularProgressIndicator());
    if (vm.error != null) return Center(child: Text('Error: ${vm.error}'));
    return Column(children: [
      Text(vm.user?.name ?? 'Unknown'),
      ElevatedButton(onPressed: () => vm.updateName('New Name'), child: const Text('Update')),
    ]);
  }
}

/// Repository模式——隔离数据源
abstract class UserRepository {
  Future<User> getUser(String id);
  Future<void> updateUser(String id, Map data);
}

class ApiUserRepository implements UserRepository {
  final ApiClient _api;
  ApiUserRepository(this._api);
  @override Future<User> getUser(String id) async { var r = await _api._dio.get('/users/$id'); return User.fromJson(r.data); }
  @override Future<void> updateUser(String id, Map data) async { await _api._dio.put('/users/$id', data: data); }
}

class MockUserRepository implements UserRepository {
  @override Future<User> getUser(String id) async { await Future.delayed(const Duration(seconds: 1)); return const User(id: '1', name: 'Mock', email: 'mock@test.com'); }
  @override Future<void> updateUser(String id, Map data) async { await Future.delayed(const Duration(milliseconds: 500)); }
}

📖 代码组织与项目结构

// 推荐项目结构:
// lib/
// ├── main.dart              — 入口
// ├── app.dart               — App配置
// ├── core/                  — 核心工具
// │   ├── theme.dart
// │   ├── constants.dart
// │   ├── extensions.dart
// │   └── utils.dart
// ├── models/                — 数据模型
// │   ├── user.dart
// │   └── product.dart
// ├── repositories/          — 数据仓库
// │   ├── user_repository.dart
// │   └── product_repository.dart
// ├── providers/             — 状态管理
// │   ├── auth_provider.dart
// │   └── cart_provider.dart
// ├── screens/               — 页面
// │   ├── home/
// │   │   ├── home_screen.dart
// │   │   └── widgets/       — 页面私有Widget
// │   ├── profile/
// │   └── settings/
// ├── widgets/               — 共享Widget
// │   ├── app_button.dart
// │   ├── app_card.dart
// │   └── loading_indicator.dart
// └── services/              — 外部服务
//     ├── api_client.dart
//     ├── storage_service.dart
//     └── analytics_service.dart

🏋️ 练习

练习1:请求重试 🔄

实现指数退避重试:失败后1s/2s/4s重试,最多3次。

练习2:文件上传 📤

使用Dio实现文件上传,显示进度条。

练习3:离线缓存 💾

优先网络,失败用缓存。

🏆 本课成就

网络工程师——精通HTTP请求与API集成

📚 下节预告

下一课将继续深入Flutter开发。