Flutter艺术探索-Flutter网络请求基础:http包使用指南

Flutter 网络请求基础:用好官方 http 包

引言

在移动应用开发中,网络请求是连接客户端与服务器的核心环节。对于 Flutter 开发者而言,官方提供的 http 包是一个绕不开的基础工具。它轻量、稳定且由官方维护,非常适合初学者上手以及用于中小型项目。

http 包本质上是对 Dart 原生 HttpClient 的一层封装,提供了诸如 GET、POST 等 HTTP 方法的简洁 API。虽然功能上不如一些第三方库丰富,但它胜在简单可靠,能帮你理清网络请求的基本脉络,是掌握 Flutter 网络通信的扎实起点。

技术分析

http 包是如何工作的?

http 包的设计遵循了清晰的分层思想,这让它在不同平台上都能有一致的行为:

  1. 传输层 :在原生平台(Android/iOS)使用 dart:io 库的 HttpClient,在 Web 平台则使用 dart:htmlHttpRequest。这层处理了最底层的网络通信。
  2. 适配层 :通过 BaseClient 这个抽象类,它统一了不同平台底层 API 的差异,为上层的调用提供了一个稳定的接口。
  3. 应用层 :也就是我们直接打交道的部分,提供了像 get()post() 这样的顶级函数,以及可以实例化、支持更多配置的 Client 类。

你可以这样理解它的架构:dart:io/dart:htmlHttpClientBaseClient → 我们使用的 http 包 API。

核心类与接口一览

1. Client 类

Client 类是 http 包的核心,它实现了 BaseClient 接口。直接使用 http.get() 这样的静态函数很方便,但在需要复用连接、管理资源或添加统一拦截逻辑时,创建 Client 实例会是更好的选择。

BaseClient 接口定义了我们熟悉的所有 HTTP 方法:

dart 复制代码
abstract class BaseClient {
  Future<Response> head(Uri url, {Map<String, String>? headers});
  Future<Response> get(Uri url, {Map<String, String>? headers});
  // ... 其他 post, put, patch, delete 方法
  Future<StreamedResponse> send(BaseRequest request);
  void close();
}
2. Request 与 Response 对象
  • Request:包含了要发起请求的所有信息(URL、方法、头部、体)。
  • Response:服务器返回的响应,包含状态码、头部和响应体。
  • StreamedResponse:用于处理流式的响应数据,比如下载大文件。

聊聊其他网络库:如何选择?

http 包并非唯一选择,了解它能帮你更好地决策:

特性 http 包 dio http_client
官方维护 ✅ 是 ❌ 社区 ✅ 是
API 简洁度 ⭐⭐⭐⭐⭐ 极简 ⭐⭐⭐⭐ 丰富 ⭐⭐⭐ 偏底层
拦截器 需手动实现 ✅ 内置强大支持 需手动实现
文件上传 基础支持(MultipartRequest ✅ 高级易用支持 基础支持
学习曲线 平缓,极易上手 中等,功能多 陡峭,更接近原生

简单来说,http 包适合学习、轻量级应用或作为理解底层原理的起点 。如果你的项目需要复杂的拦截、取消请求、文件上传/下载进度等高级功能,那么 dio 会是更高效的选择。

动手实现

第一步:配置环境

pubspec.yaml 文件中添加依赖:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0 # 核心网络库
  # 如果需要处理复杂 JSON,可以加上序列化支持
  json_annotation: ^4.8.1

dev_dependencies:
  build_runner: ^2.4.4
  json_serializable: ^6.7.0

第二步:搭建网络层基类

一个好的习惯是将网络请求逻辑封装起来。下面是一个基础的网络服务类,它处理了 URL 拼接、头部管理、错误处理和基本的 GET/POST 请求:

dart 复制代码
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';

/// 自定义的网络异常,方便区分处理
class NetworkException implements Exception {
  final String message;
  final int statusCode;
  NetworkException(this.message, this.statusCode);
  @override
  String toString() => 'NetworkException: $message (状态码: $statusCode)';
}

/// 网络服务基类
abstract class BaseHttpService {
  static const String _baseUrl = 'https://api.example.com';
  static const Duration _defaultTimeout = Duration(seconds: 30);

  final http.Client _client;

  BaseHttpService({http.Client? client}) : _client = client ?? http.Client();

  /// 构建完整的请求 URL
  Uri _buildUrl(String endpoint, {Map<String, dynamic>? queryParameters}) {
    return Uri.parse('$_baseUrl/$endpoint').replace(
      queryParameters: queryParameters,
    );
  }

  /// 构建公共请求头,例如添加认证 Token
  Map<String, String> _buildHeaders({Map<String, String>? extraHeaders, String? contentType}) {
    final headers = <String, String>{
      'Accept': 'application/json',
      'Content-Type': contentType ?? 'application/json',
    };
    // 示例:添加认证 Token
    final token = _getAuthToken();
    if (token != null) {
      headers['Authorization'] = 'Bearer $token';
    }
    if (extraHeaders != null) {
      headers.addAll(extraHeaders);
    }
    return headers;
  }

  /// 获取认证 Token,子类可重写此方法
  String? _getAuthToken() => null;

  /// 统一处理响应:成功则解析 JSON,失败则抛出异常
  Future<dynamic> _handleResponse(http.Response response) async {
    if (response.statusCode >= 200 && response.statusCode < 300) {
      return response.body.isEmpty ? null : jsonDecode(response.body);
    } else {
      throw NetworkException('请求失败: ${response.reasonPhrase}', response.statusCode);
    }
  }

  /// GET 请求
  Future<dynamic> get(String endpoint, {Map<String, dynamic>? queryParams, Map<String, String>? headers}) async {
    try {
      final url = _buildUrl(endpoint, queryParameters: queryParams);
      final response = await _client
          .get(url, headers: _buildHeaders(extraHeaders: headers))
          .timeout(_defaultTimeout);
      return await _handleResponse(response);
    } on http.ClientException catch (e) {
      throw NetworkException('网络连接错误: ${e.message}', 0);
    } on TimeoutException catch (_) {
      throw NetworkException('请求超时', 408);
    }
  }

  /// POST 请求
  Future<dynamic> post(String endpoint,
      {Map<String, dynamic>? body, Map<String, String>? headers, String? contentType}) async {
    try {
      final url = _buildUrl(endpoint);
      final response = await _client
          .post(url,
              headers: _buildHeaders(extraHeaders: headers, contentType: contentType),
              body: body != null ? jsonEncode(body) : null)
          .timeout(_defaultTimeout);
      return await _handleResponse(response);
    } on FormatException catch (e) {
      throw NetworkException('数据格式错误: ${e.message}', 400);
    } catch (e) {
      rethrow;
    }
  }

  /// 记得在应用退出时关闭 Client,释放资源
  void dispose() {
    _client.close();
  }
}

第三步:实现具体的业务 API

基于上面的基类,我们可以创建针对特定业务(比如用户管理)的服务类:

dart 复制代码
/// 用户数据模型
class User {
  final int id;
  final String name;
  final String email;
  User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(id: json['id'] as int, name: json['name'] as String, email: json['email'] as String);
  }
  Map<String, dynamic> toJson() => {'id': id, 'name': name, 'email': email};
}

/// 用户相关的网络请求
class UserService extends BaseHttpService {
  static const String _usersEndpoint = 'users';

  /// 获取用户列表
  Future<List<User>> getUsers({int page = 1, int limit = 10}) async {
    try {
      final response = await get(_usersEndpoint,
          queryParams: {'page': page.toString(), 'limit': limit.toString()});
      if (response is List) {
        return response.map((json) => User.fromJson(json)).toList();
      }
      return [];
    } catch (e) {
      debugPrint('获取用户列表失败: $e');
      rethrow; // 将异常抛给 UI 层处理
    }
  }

  /// 创建新用户
  Future<User> createUser(User user) async {
    try {
      final response = await post(_usersEndpoint, body: user.toJson());
      return User.fromJson(response);
    } catch (e) {
      debugPrint('创建用户失败: $e');
      rethrow;
    }
  }

  @override
  String? _getAuthToken() {
    // 实际情况中,从安全存储(如 flutter_secure_storage)中获取
    // return storage.get('auth_token');
    return 'your_auth_token_here';
  }
}

第四步:在 UI 中调用并展示

最后,在 Flutter 页面中,我们使用 UserService 来获取数据并更新界面:

dart 复制代码
import 'package:flutter/material.dart';
import 'services/user_service.dart';

class UserListScreen extends StatefulWidget {
  const UserListScreen({super.key});
  @override
  State<UserListScreen> createState() => _UserListScreenState();
}

class _UserListScreenState extends State<UserListScreen> {
  final UserService _userService = UserService();
  List<User> _users = [];
  bool _isLoading = false;
  String? _errorMessage;

  @override
  void initState() {
    super.initState();
    _loadUsers();
  }

  Future<void> _loadUsers() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });
    try {
      final users = await _userService.getUsers();
      setState(() => _users = users);
    } on NetworkException catch (e) {
      setState(() => _errorMessage = e.message);
      _showErrorSnackbar(e.message);
    } catch (e) {
      setState(() => _errorMessage = '未知错误');
    } finally {
      setState(() => _isLoading = false);
    }
  }

  void _showErrorSnackbar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message), backgroundColor: Colors.red));
  }

  Future<void> _refreshData() async => _loadUsers();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('用户列表'), actions: [
        IconButton(icon: const Icon(Icons.refresh), onPressed: _refreshData),
      ]),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    if (_isLoading && _users.isEmpty) return const Center(child: CircularProgressIndicator());
    if (_errorMessage != null && _users.isEmpty) {
      return Center(
        child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
          Text(_errorMessage!),
          const SizedBox(height: 16),
          ElevatedButton(onPressed: _loadUsers, child: const Text('重试')),
        ]),
      );
    }
    return RefreshIndicator(
      onRefresh: _refreshData,
      child: ListView.builder(
        itemCount: _users.length,
        itemBuilder: (context, index) {
          final user = _users[index];
          return ListTile(
            leading: CircleAvatar(child: Text(user.name[0])),
            title: Text(user.name),
            subtitle: Text(user.email),
            onTap: () {/* 跳转详情 */},
          );
        },
      ),
    );
  }

  @override
  void dispose() {
    _userService.dispose(); // 重要:释放网络客户端
    super.dispose();
  }
}

如何优化得更专业?

基础功能跑通后,我们可以考虑一些优化策略,让网络层更健壮、高效。

1. 管理 Client 的生命周期:连接池

http.Client 内部有连接池机制。我们应该在应用级别共享一个 Client 实例,并在应用退出时关闭它,而不是每个请求都新建一个。

dart 复制代码
class HttpServiceManager {
  static final HttpServiceManager _instance = HttpServiceManager._internal();
  late http.Client _sharedClient;
  factory HttpServiceManager() => _instance;
  HttpServiceManager._internal() {
    _sharedClient = http.Client(); // 全局唯一的 Client
  }
  http.Client get client => _sharedClient;
  void dispose() => _sharedClient.close();
}
// 使用:UserService(client: HttpServiceManager().client)

2. 实现请求重试机制

网络不稳定时,自动重试能提升用户体验。下面是一个简单的指数退避重试 Client:

dart 复制代码
class RetryClient extends http.BaseClient {
  final http.Client _innerClient;
  final int _maxRetries;
  final Duration _initialDelay;
  RetryClient({required http.Client innerClient, int maxRetries = 3, Duration initialDelay = const Duration(milliseconds: 500)})
      : _innerClient = innerClient,
        _maxRetries = maxRetries,
        _initialDelay = initialDelay;

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    int attempt = 0;
    Duration delay = _initialDelay;
    while (true) {
      attempt++;
      try {
        return await _innerClient.send(request);
      } catch (error) {
        // 只在网络错误、超时等情况下重试
        bool shouldRetry = (error is SocketException || error is TimeoutException || error is http.ClientException) && attempt < _maxRetries;
        if (!shouldRetry) rethrow;
        debugPrint('请求失败,进行第$attempt次重试(延迟 ${delay.inMilliseconds}ms)');
        await Future.delayed(delay);
        delay *= 2; // 指数退避
      }
    }
  }
  @override
  void close() => _innerClient.close();
}

3. 添加简单的内存缓存

对于不常变的数据,缓存可以极大提升加载速度并节省流量:

dart 复制代码
class CachedHttpClient extends http.BaseClient {
  final http.Client _innerClient;
  final Map<String, (DateTime, dynamic)> _cache = {};
  final Duration _defaultCacheDuration;
  CachedHttpClient({required http.Client innerClient, Duration defaultCacheDuration = const Duration(minutes: 5)})
      : _innerClient = innerClient,
        _defaultCacheDuration = defaultCacheDuration;

  @override
  Future<http.Response> get(Uri url, {Map<String, String>? headers}) async {
    final cacheKey = 'GET:${url.toString()}';
    // 检查缓存是否存在且未过期
    if (_cache.containsKey(cacheKey)) {
      final (expiryTime, cachedResponse) = _cache[cacheKey]!;
      if (expiryTime.isAfter(DateTime.now())) {
        return cachedResponse as http.Response;
      } else {
        _cache.remove(cacheKey);
      }
    }
    // 执行请求并缓存成功的结果
    final response = await _innerClient.get(url, headers: headers);
    if (response.statusCode == 200) {
      _cache[cacheKey] = (DateTime.now().add(_defaultCacheDuration), response);
    }
    return response;
  }
  // ... 需要实现其他 send 等方法
}

一些实用的开发建议

调试技巧:记录所有网络请求

在开发阶段,一个能打印详细日志的 Client 非常有用:

dart 复制代码
class LoggingClient extends http.BaseClient {
  final http.Client _innerClient;
  LoggingClient(this._innerClient);
  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    final startTime = DateTime.now();
    debugPrint('🚀 [HTTP 请求] ${request.method} ${request.url}');
    debugPrint('   头信息: ${request.headers}');
    try {
      final response = await _innerClient.send(request);
      final duration = DateTime.now().difference(startTime);
      debugPrint('✅ [HTTP 响应] 状态码: ${response.statusCode}, 耗时: ${duration.inMilliseconds}ms');
      return response;
    } catch (e) {
      debugPrint('❌ [HTTP 错误]: $e');
      rethrow;
    }
  }
  @override
  void close() => _innerClient.close();
}

安全与最佳实践

  1. 务必使用 HTTPS :生产环境的 API 地址必须是 https:// 开头。
  2. 妥善管理敏感信息 :如 API Token,应使用 flutter_secure_storage 等库存储,切勿硬编码或打印到日志。
  3. 提供友好的用户错误提示:将网络异常状态码(如 401、500)转换为用户能理解的消息。
  4. 配置平台权限 :别忘了在 AndroidManifest.xmlInfo.plist 中添加网络权限。

总结

通过本文,我们从原理到实践,完整地梳理了如何在 Flutter 中使用官方的 http 包。你应该已经掌握了如何:

  • 使用 http 包进行基本的 GET/POST 请求。
  • 设计一个分层、易维护的网络服务层。
  • 实现健壮的错误处理和用户体验优化。
  • 通过连接池、重试、缓存等策略提升网络性能。

http 包是你 Flutter 网络编程之旅的一个坚实起点。当你和你的项目一起成长,遇到需要更复杂功能(如拦截器、请求取消、更便捷的文件操作)时,你会自然地去探索像 dio 这样更强大的第三方库。但在此之前,充分理解并用好 http 包,将为你的开发打下坚实的基础。

相关推荐
小白阿龙3 小时前
鸿蒙+flutter 跨平台开发——基于日历视图的生理周期计算逻辑
flutter·华为·harmonyos·鸿蒙
kirk_wang3 小时前
Flutter艺术探索-Flutter包管理:pubspec.yaml配置详解
flutter·移动开发·flutter教程·移动开发教程
猛扇赵四那边好嘴.3 小时前
Flutter 框架跨平台鸿蒙开发 - 脑筋急转弯应用开发教程
flutter·华为·harmonyos
朽木成才4 小时前
Android+Flutter混合开发实战
android·flutter
猛扇赵四那边好嘴.4 小时前
Flutter 框架跨平台鸿蒙开发 - 药品信息查询应用开发教程
flutter·华为·harmonyos
AiFlutter5 小时前
六、表单元素(04):开关
flutter·低代码平台·aiflutter·aiflutter低代码·dart开发
猛扇赵四那边好嘴.5 小时前
Flutter 框架跨平台鸿蒙开发 - 问答社区应用开发教程
开发语言·javascript·flutter·华为·harmonyos
LawrenceLan5 小时前
Flutter 零基础入门(二十二):Text 文本组件与样式系统
开发语言·前端·flutter·dart
kirk_wang7 小时前
Flutter艺术探索-Flutter性能优化基础:const与const构造函数
flutter·移动开发·flutter教程·移动开发教程