系统化掌握Dart网络编程之Dio(一):筑基篇

407 阅读9分钟

前言

在移动应用开发中,网络请求如同应用的"生命线",但Flutter原生HttpClient简陋http库的局限性,让我们常陷入重复造轮子的困境。

  • 当你的应用需要处理文件上传下载多级拦截器全局配置时,是否还在手动拼接URL参数?
  • 当面对复杂的认证体系日志监控需求时,是否还在用print语句调试网络请求?

Dio作为Dart生态中最强大的网络请求库,以其高度可扩展的架构设计和丰富的功能,正在重塑异步编程范式。

本文将带你穿透API表层,系统构建Dio核心知识体系,解锁企业级应用的开发密码。

千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意

一、基本概念

dio 是一个强大的 HTTP 网络请求库,支持全局配置Restful APIFormData拦截器请求取消Cookie 管理文件上传/下载超时自定义适配器转换器等。

提供完整的网络请求生命周期管理能力。其本质是通过封装底层HTTP协议交互,构建可插拔的请求处理管道


二、核心价值

2.1、工程化能力:企业级开发标准

核心特性

  • BaseOptions(全局配置):通过统一管理基础URL超时阈值请求头等参数,避免重复配置
final dio = Dio(BaseOptions(
  baseUrl: "https://api.example.com/v2",
  connectTimeout: Duration(seconds: 8),
  headers: {"Content-Type": "application/json"}
));
  • Interceptors(拦截器链):支持请求/响应/错误三级拦截,可实现统一身份认证请求重试等逻辑。
  • CancelToken(请求取消):精准控制请求生命周期,防止内存泄漏无效资源占用。

典型场景
当应用需要对接多个微服务时,可通过全局配置快速切换不同环境:

// 开发环境配置
dio.options.baseUrl = "http://dev.api.example.com";

// 生产环境配置(通过环境变量动态切换)
if (isProduction) {
  dio.options = _loadProdConfig();
}

对比原生HttpClient
原生库需手动拼接URL、重复设置HeaderDio通过全局配置降低代码冗余度达60%以上。


2.2、扩展性架构:模块化设计哲学

拦截器机制解析
Dio的拦截器采用责任链模式,允许以插件形式扩展功能:

dio.interceptors.addAll([
  // 日志记录
  LogInterceptor(
    requestBody: true,
    responseBody: true
  ),
  
  // Token自动刷新
  QueuedInterceptorsWrapper(
    onError: (error, handler) {
      if (error.response?.statusCode == 401) {
        _refreshToken().then((newToken) {
          // 更新请求头并重新发起请求
          error.requestOptions.headers["Authorization"] = newToken;
          handler.resolve(dio.fetch(error.requestOptions));
        });
      }
    }
  )
]);

解耦优势

  • 认证模块:自动处理JWT过期问题,业务代码零侵入。
  • 监控模块:通过拦截器收集请求耗时成功率等指标。
  • 缓存模块:在拦截器中实现请求级缓存策略

2.3、协议完整性:覆盖全场景需求

关键协议支持

协议能力实现方式代码示例
文件上传FormData封装dio.post("/upload", data: FormData.fromMap({"file": MultipartFile(...)}))
分块下载download方法+进度回调dio.download(url, savePath, onReceiveProgress: (count, total) {...})
数据转换Transformer定制解析逻辑dio.transformer = MyCustomTransformer()

技术细节

  • 大文件处理:通过Stream实现流式上传,避免内存溢出
  • 下载优化:支持断点续传(通过savePath参数自动管理)。
  • 协议扩展:可自定义HTTP/2WebSocket等协议适配器。

2.4、性能优化:超越原生的秘诀

底层优化策略

  • 连接池管理:复用TCP连接,减少三次握手开销。
  • 并发队列控制:通过httpClientAdapter配置最大连接数。
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
  client.maxConnectionsPerHost = 10; // 控制并发量
};
  • 请求优先级调度:结合CancelToken实现关键请求优先处理。

2.5、为什么这些特性如此重要?

Dio的价值不仅在于提供网络请求能力,更在于将网络层抽象为可维护、可观测、可扩展的系统工程

  • 1、维护性:通过配置与代码分离,降低迭代成本。
  • 2、可观测性:拦截器天然支持APM(应用性能监控)集成。
  • 3、鲁棒性:内置重试机制、超时控制等容错策略。
  • 4、性能基线:优化的底层实现确保业务扩展不会引发性能劣化。

这些特性使得Dio成为中大型Flutter项目的必然选择,而非简单的工具库。当你的应用日活超过10万,或需要处理跨国网络抖动时,Dio的设计哲学将展现出真正的威力。


三、基本用法

3.1、安装与初始化

步骤1:添加依赖

# pubspec.yaml
dependencies:
    dio: ^5.8.0+1 # 使用最新稳定版

步骤2:创建Dio实例

import 'package:dio/dio.dart';

// 全局单例(推荐)
final dio = Dio(BaseOptions(
  baseUrl: "https://api.example.com/api/v1",
  connectTimeout: Duration(seconds: 10),
  headers: {"Accept": "application/json"}
));

3.2、基础请求方法

GET请求

// 简单GET
final response = await dio.get("/user/123");

// 带查询参数
final response = await dio.get("/search", queryParameters: {
  "keyword": "flutter",
  "page": 1,
  "sort": "desc"
});

// 处理响应
if (response.statusCode == 200) {
  final data = response.data; // 自动解析JSON为Map/List
  print("用户数据:$data");
}

POST请求

// 提交JSON数据
final response = await dio.post(
  "/users",
  data: {
    "name": "Flutter开发者",
    "email": "dev@example.com"
  },
  options: Options(headers: {"X-Request-ID": "uuid"}),
);

// 提交Form表单
await dio.post(
  "/login",
  data: FormData.fromMap({
    "username": "admin",
    "password": "securePassword123"
  }),
);

3.3、文件上传与下载

文件上传

// 单文件上传
final formData = FormData.fromMap({
  "file": await MultipartFile.fromFile(
    "/path/to/file.jpg",
    filename: "avatar.jpg",
  ),
  "description": "用户头像",
});

final uploadResponse = await dio.post(
  "/upload",
  data: formData,
  onSendProgress: (sentBytes, totalBytes) {
    print("上传进度:${(sentBytes / totalBytes * 100).toStringAsFixed(1)}%");
  },
);

文件下载

// 下载到指定路径
await dio.download(
  "https://example.com/largefile.zip",
  "/storage/emulated/0/Download/file.zip",
  onReceiveProgress: (receivedBytes, totalBytes) {
    print("下载进度:${(receivedBytes / totalBytes * 100).toStringAsFixed(1)}%");
  },
  deleteOnError: true, // 下载失败时删除文件
);

3.4、拦截器实践

日志拦截器

dio.interceptors.add(LogInterceptor(
  request: true,      // 打印请求信息
  requestBody: true,  // 显示请求体
  responseBody: true, // 显示响应体
  error: true,        // 显示错误详情
));

认证拦截器

dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) async {
    // 自动添加Token
    final token = await _getCachedToken();
    options.headers["Authorization"] = "Bearer $token";
    handler.next(options);
  },
  onError: (error, handler) async {
    // Token过期自动刷新
    if (error.response?.statusCode == 401) {
      final newToken = await _refreshToken();
      error.requestOptions.headers["Authorization"] = "Bearer $newToken";
      handler.resolve(await dio.fetch(error.requestOptions));
    } else {
      handler.next(error);
    }
  },
));

3.5、错误处理

捕获Dio异常

try {
  await dio.get("/protected-resource");
} on DioException catch (e) {
  // 分类处理错误类型
  switch (e.type) {
    case DioExceptionType.connectionTimeout:
      print("连接超时");
    case DioExceptionType.badResponse:
      print("服务器错误:${e.response?.statusCode}");
    case DioExceptionType.cancel:
      print("请求被取消");
    default:
      print("未知错误:${e.message}");
  }
}

自定义错误处理

Future<T> safeApiCall<T>(Future<Response> Function() request) async {
  try {
    final response = await request();
    return response.data as T;
  } on DioException catch (e) {
    throw _parseError(e);
  }
}

// 统一错误解析
ApiError _parseError(DioException e) {
  if (e.response?.data is Map) {
    final code = e.response?.data["errorCode"] ?? "unknown";
    final message = e.response?.data["message"] ?? "未知错误";
    return ApiError(code: code, message: message);
  }
  return ApiError(code: "NETWORK_ERROR", message: e.message ?? "网络异常");
}

3.6、请求取消

取消令牌使用

// 在Widget的dispose方法中取消
class _MyPageState extends State<MyPage> {
  final CancelToken _cancelToken = CancelToken();

  @override
  void dispose() {
    _cancelToken.cancel("页面销毁");
    super.dispose();
  }

  Future<void> fetchData() async {
    await dio.get(
      "/long-running-request",
      cancelToken: _cancelToken,
    );
  }
}

// 手动取消
void cancelRequest() {
  _cancelToken.cancel("用户主动取消");
}

3.7、最佳实践

1、配置管理:使用BaseOptions集中管理不同环境(开发/生产)的配置。

// config.dart
abstract class EnvConfig {
  static final dev = BaseOptions(baseUrl: "http://dev.api.example.com");
  static final prod = BaseOptions(baseUrl: "https://api.example.com");
}

2、响应模型化:使用json_serializable自动转换响应数据为Model类。

@JsonSerializable()
class User {
  final String id;
  final String name;
  
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

3、统一错误处理:通过拦截器或高阶函数统一封装错误处理逻辑。

4、性能监控:在拦截器中记录请求耗时成功率等指标。

dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) {
    final startTime = DateTime.now().millisecondsSinceEpoch;
    options.extra["startTime"] = startTime;
    handler.next(options);
  },
  onResponse: (response, handler) {
    final duration = DateTime.now().millisecondsSinceEpoch - 
                    response.requestOptions.extra["startTime"];
    _monitor.recordSuccess(duration);
    handler.next(response);
  },
));

3.8、常见问题

Q1: 如何防止重复请求?

// 使用请求锁
bool _isFetching = false;

Future<void> fetchData() async {
  if (_isFetching) return;
  _isFetching = true;
  try {
    await dio.get("/data");
  } finally {
    _isFetching = false;
  }
}

// 或使用CancelToken取消前序请求
CancelToken? _lastToken;

Future<void> fetchData() async {
  _lastToken?.cancel();
  _lastToken = CancelToken();
  await dio.get("/data", cancelToken: _lastToken);
}

Q2: 如何处理非JSON响应?

// 修改响应解析方式
final response = await dio.get(
  "/plain-text",
  options: Options(responseType: ResponseType.plain),
);
print(response.data); // 直接获取字符串

// 或自定义Transformer
dio.transformer = _CustomTransformer();

四、网络请求库对比

库名称原生库 (dart:io/http)Diohttp (官方包)ChopperRetrofit
功能特点基础HTTP请求拦截器、全局配置轻量级,简单封装基于代码生成的REST客户端基于Dio的代码生成
支持GET/POST等基本方法FormData文件上传/下载支持常见HTTP方法支持拦截器、转换器自动生成API接口类
手动处理JSON解析请求取消、超时配置需手动处理JSON解析强类型API定义依赖Diohttp作为底层
优点无需额外依赖功能全面,扩展性强官方维护,轻量稳定类型安全,减少模板代码高度抽象,减少重复代码
适合简单请求场景拦截器方便统一处理日志/错误适合快速开发简单API支持多种数据转换格式Dio深度集成,功能强大
缺点代码冗余,需手动封装学习成本稍高功能有限,需自行扩展依赖代码生成,配置复杂需结合代码生成,灵活性低
不支持高级功能(如拦截器)体积略大缺少拦截器等高级功能文档较少,社区较小对复杂请求支持有限
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
适用场景简单请求、小型项目中大型项目、需要高级功能轻量级应用、快速原型需要强类型API接口的项目需要高度抽象的REST API项目
维护状态官方维护,更新稳定社区活跃,更新频繁官方维护,更新稳定社区维护,更新较慢社区维护,依赖Dio更新

五、进阶应用

5.1、网络请求封装(缩减版)

import 'package:dio/dio.dart';
import 'package:flutter_demo/http/http_res.dart';

class ApiService {
  /// static 声明为类级变量,与类本身绑定而非实例
  /// final 确保实例不可被重新赋值
  /// 延迟初始化:静态变量具有懒加载特性,只有在首次访问时才会创建变量
  /// 内初管理:整个应用声明周期只存在一个实例
  static final ApiService _instance = ApiService._internal();

  /// 不总是创建新实例
  /// 返回已存在实例
  /// 控制实例化的核心机制
  factory ApiService() => _instance;

  late final Dio _dio;

  ApiService._internal() {
    // 初始化Dio配置
    _dio = Dio(
      BaseOptions(
        baseUrl: "https://apis.tianapi.com",
        connectTimeout: const Duration(seconds: 30),
        receiveTimeout: const Duration(seconds: 15),
        headers: {"Accept": "application/json"},
      ),
    );
  }

  // GET请求
  Future<HttpRes> get(String url,
      {Map<String, dynamic>? queryParameters}) async {
    try {
      final response = await _dio.get(
        url,
        queryParameters: queryParameters,
      );
      return HttpRes.fromJson(response.data);
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  // POST请求
  Future<HttpRes> post(String url, {dynamic data}) async {
    try {
      final response = await _dio.post(
        url,
        data: {
          "key": "0e241b716c61db756ab5e29eb28a92c0",
        },
        options: Options(contentType: Headers.formUrlEncodedContentType),
      );
      return HttpRes.fromJson(response.data);
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  // 统一错误处理
  Exception _handleError(DioException e) {
    String message = '请求发生错误';
    if (e.type == DioExceptionType.connectionTimeout) {
      message = '连接超时';
    } else if (e.type == DioExceptionType.receiveTimeout) {
      message = '接收超时';
    } else if (e.response?.data != null) {
      message = e.response!.data['message'] ?? message;
    }
    return Exception(message);
  }
}

5.2、智能分页加载

场景需求:实现列表分页加载,支持自动重试并发控制页面保序

import 'package:flutter/material.dart';
import 'dart:async';

import '../http/api_service.dart';
import '../http/http_res.dart';

/// 分页演示页面
class PaginationDemo extends StatefulWidget {
  const PaginationDemo({super.key});

  @override
  State<PaginationDemo> createState() => _PaginationDemoState();
}

class _PaginationDemoState extends State<PaginationDemo> {
  final ScrollController _scrollController = ScrollController();
  int _currentPage = 1;
  bool _isLoading = false;
  bool _hasError = false;
  bool _hasMore = true;
  final _api = ApiService();
  final List<Drug> _items = [];

  @override
  void initState() {
    super.initState();
    _loadFirstPage();
    _scrollController.addListener(_scrollListener);
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  /// 滚动监听
  void _scrollListener() {
    if (_scrollController.position.pixels >
        _scrollController.position.maxScrollExtent - 200) {
      _loadMore();
    }
  }

  /// 加载第一页
  Future<void> _loadFirstPage() async {
    setState(() => _isLoading = true);
    _api.get("/bcgm/index", queryParameters: {
      "key": "0e241b716c61db756ab5e29eb28a92c0",
      "num": 10,
      "page": 1,
    }).then((value) {
      final drugs = value.result?.list ?? [];
      for (int i = 0; i < drugs.length; i++) {
        _items.add(drugs[i]);
      }
      setState(() {
        _currentPage = 2;
        _hasMore = _items.isNotEmpty;
      });
    }).catchError((e) {
      setState(() => _hasError = true);
    }).whenComplete(() {
      setState(() => _isLoading = false);
    });
  }

  /// 加载更多
  Future<void> _loadMore() async {
    if (!_hasMore || _isLoading || _hasError) return;

    setState(() {
      _isLoading = true;
      _hasError = false;
    });

    _api.get("/bcgm/index", queryParameters: {
      "key": "0e241b716c61db756ab5e29eb28a92c0",
      "num": 10,
      "page": _currentPage,
    }).then((value) {
      final drugs = value.result?.list ?? [];
      for (int i = 0; i < drugs.length; i++) {
        _items.add(drugs[i]);
      }
      setState(() {
        if (_items.isEmpty) {
          _hasMore = false;
        } else {
          _currentPage++;
        }
      });
    }).catchError((e) {
      setState(() => _hasError = true);
    }).whenComplete(() {
      setState(() => _isLoading = false);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('智能分页加载示例'),
      ),
      body: RefreshIndicator(
        onRefresh: _loadFirstPage,
        child: ListView.builder(
          controller: _scrollController,
          itemCount: _items.length + 1,
          itemBuilder: (context, index) {
            if (index < _items.length) {
              return _ListItem(item: _items[index]);
            }
            return _buildFooter();
          },
        ),
      ),
    );
  }

  /// 底部状态显示
  Widget _buildFooter() {
    if (_hasError) {
      return _ErrorRetry(
        onRetry: _loadMore,
      );
    }
    if (_isLoading) {
      return const _LoadingIndicator();
    }
    if (!_hasMore) {
      return const _NoMoreItems();
    }
    return Container();
  }
}

/// 列表项组件
class _ListItem extends StatelessWidget {
  final Drug item;

  const _ListItem({required this.item});

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 80,
      margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(8),
      ),
      child: ListTile(
        title: Text(item.name ?? ""),
        subtitle: Text(
          item.content ?? "",
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
        ),
        trailing: const Icon(Icons.chevron_right),
      ),
    );
  }
}

/// 加载指示器
class _LoadingIndicator extends StatelessWidget {
  const _LoadingIndicator();

  @override
  Widget build(BuildContext context) {
    return const Padding(
      padding: EdgeInsets.all(16.0),
      child: Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

/// 错误重试组件
class _ErrorRetry extends StatelessWidget {
  final VoidCallback onRetry;

  const _ErrorRetry({required this.onRetry});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        children: [
          const Text('加载失败,请重试', style: TextStyle(color: Colors.red)),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: onRetry,
            child: const Text('重试'),
          ),
        ],
      ),
    );
  }
}

/// 没有更多数据
class _NoMoreItems extends StatelessWidget {
  const _NoMoreItems();

  @override
  Widget build(BuildContext context) {
    return const Padding(
      padding: EdgeInsets.all(16.0),
      child: Center(
        child: Text('已经到底了~', style: TextStyle(color: Colors.grey)),
      ),
    );
  }
}

六、总结

掌握Dio需要建立三层认知:基础层理解请求生命周期管理,进阶层掌握拦截器管道机制,架构层学会与状态管理依赖注入等模式结合。真正的精通不在于记住每个API,而是能根据应用场景灵活设计拦截策略优化请求链路

当你能将文件下载进度控制JWT自动刷新请求优先级调度等功能像搭积木一样组合时,才意味着真正系统化掌握了Dio的精髓。优秀的网络层架构,永远是业务复杂度与技术深度的平衡艺术

欢迎一键四连关注 + 点赞 + 收藏 + 评论