GetX:数据拉取与Dio

Dio 是什么

Dio 是一个 基于 Dart 的强大 HTTP 客户端,用于发起网络请求(GET、POST 等)和处理响应。

相比原生 http 包,Dio 更灵活,功能更强大,特别适合在 Flutter App 中做 API 调用

Dio 的主要特点

  • 支持拦截器(Interceptor):可以在请求/响应前做统一处理,例如添加 token、打印日志、统一错误处理。
  • 支持全局配置:如 baseUrl、请求超时、headers 等。
  • 支持请求取消:可以通过 CancelToken 取消请求,非常适合页面销毁时取消未完成请求。
  • 支持 FormData 和文件上传:可以直接上传文件、表单数据,很方便。
  • 支持请求重试和拦截错误:可以根据响应状态码统一处理错误。
  • 支持 JSON 自动解析:返回数据可以直接解析成 MapList

Dio 的使用

安装

dependencies:
  dio: ^5.9.0

创建 Dio 实例

import 'package:dio/dio.dart';

final dio = Dio(
  BaseOptions(
    baseUrl: "https://jsonplaceholder.typicode.com",
    connectTimeout: 5000, // 连接超时 5 秒
    receiveTimeout: 3000, // 接收超时 3 秒
  ),
);

BaseOptions 常用属性

属性 说明
baseUrl 基础 URL,可在请求中省略重复的域名
connectTimeout 连接超时时间(毫秒)
receiveTimeout 响应超时时间(毫秒)
headers 全局请求头
responseType 响应类型,jsonplainstreambytes
contentType 请求内容类型,常用 "application/json"
followRedirects 是否跟随重定向
validateStatus 自定义状态码验证,例如只接收 200~299

发送请求

// 发送get请求
Response response = await dio.get("/path",
  // Map<String, dynamic>? queryParameters, 
  queryParameters: {"id": 123},
  options: Options(headers: {"Authorization": "Bearer TOKEN"}),
);


// POST 请求
Response response = await dio.post("/path",
  data: {"title": "Hello", "body": "World"},
  options: Options(contentType: Headers.jsonContentType),
);


// PUT / PATCH / DELETE
await dio.put("/path", data: {...});
await dio.patch("/path", data: {...});
await dio.delete("/path", data: {...});

Dio 中,data 参数的类型是 Object?,也就是说你可以传入:①Map(最常用)②List(数组)③String(通常是 JSON 字符串)④其他可序列化对象(比如自定义对象,只要你先转成 JSON)

  • 如果传 String,最好设置 contentType: Headers.jsonContentType,否则服务器可能无法识别为 JSON。

    await dio.post("/raw", data: '{"title":"Hello"}', 
       options: Options(contentType: Headers.jsonContentType));
    
  • Map 或 List 会被 Dio 自动序列化成 JSON(默认 application/json)。

Options 常用属性

属性 说明
headers 请求单独设置 headers
responseType 请求单独设置响应类型
contentType 单次请求设置 Content-Type
extra 自定义额外参数,可在拦截器中使用
sendTimeout 发送数据超时时间

Options 和 BaseOptions的区别

Dio 中,OptionsBaseOptions 都是用来配置请求的,但作用范围和使用场景不同:

配置对象 作用范围 常用属性 覆盖关系
BaseOptions Dio 实例全局默认 baseUrl、headers、timeout、responseType 被单次请求的 Options 覆盖
Options 单次请求 method、headers、contentType、validateStatus 等 覆盖 BaseOptions 中对应值

上传与下载文件

// 下载文件
await dio.download(
  "https://example.com/file.png",
  "./file.png",
  onReceiveProgress: (received, total) {
    print("$received/$total");
  },
);

// 上传文件
FormData formData = FormData.fromMap({
  "file": await MultipartFile.fromFile("path/to/file.png"),
});
await dio.post("/upload", data: formData);

拦截器

final dio = Dio(BaseOptions(baseUrl: "https://jsonplaceholder.typicode.com"));
dio.interceptors.add(
  InterceptorsWrapper(
    onRequest: (options, handler) {
      print("请求路径: ${options.path}");
      return handler.next(options);
    },
    onResponse: (response, handler) {
      print("响应: ${response.statusCode}");
      return handler.next(response);
    },
    onError: (DioError e, handler) {
      print("请求错误: ${e.message}");
      return handler.next(e);
    },
  ),
);

什么时候添加拦截器

拦截器一般是在 Dio 实例创建后立刻添加

原因:拦截器会影响请求/响应的整个生命周期,所以必须在发起请求前添加。

可以添加多个拦截器,顺序按添加顺序执行。

拦截器的作用

Dio 的 拦截器(Interceptor) 可以在请求发出前、响应到达前和请求出错时,统一处理一些逻辑,比如:

  • 请求日志打印
  • 添加公共请求头
  • 请求/响应统一处理错误
  • 全局鉴权(Token刷新)

onRequest

触发时机:每次请求发出 调用

参数

  • options:包含请求的 URL、方法、headers、queryParameters 等
  • handler:控制请求继续、停止或返回自定义数据

作用

  • 可以修改请求信息,比如添加统一 Header、修改参数
  • 日志打印请求信息

必须调用 handler.next(options) 才会继续请求

onResponse

触发时机:服务器返回响应 成功 时调用(状态码 2xx 或自定义 validateStatus)

参数

  • response:服务器返回的数据、状态码、请求信息
  • handler:控制响应继续处理

作用

  • 可以统一处理响应数据,比如解包 JSON、日志打印
  • 可以根据状态码统一抛错或者做数据转换

必须调用 handler.next(response) 才会把响应继续返回到调用处

onError

触发时机:请求出错时调用(网络错误、超时、响应错误等)

参数

  • DioError e:包含错误类型、请求信息、响应信息
  • handler:可以继续抛出错误或者返回自定义数据

作用

  • 统一处理错误日志
  • 可以做重试、错误提示、Token 刷新等

必须调用 handler.next(e) 或者 handler.resolve(response) 才会继续流程

请求取消

CancelToken cancelToken = CancelToken();
// 发送请求
dio.get("/path", cancelToken: cancelToken);
// 取消请求
cancelToken.cancel("取消请求");
  • CancelToken 是一个 令牌,请求时绑定这个 token
  • 调用 cancelToken.cancel() 时:
    • 请求会立即中止
    • Dio 会抛出一个 DioError,类型为 DioErrorType.cancel
    • 你可以在 onError 拦截器里捕获并处理取消请求的情况

请求取消的典型场景

  • 用户切换页面或关闭页面时,当前页面的请求已经不需要了
  • 用户快速连续触发多次搜索,前一次请求的结果不再需要
  • 长时间请求超时前手动取消
flowchart LR
    classDef bigFont font-size:25px;

    A[发起 Dio 请求]:::bigFont --> B{绑定 CancelToken?}:::bigFont
    B -- 是 --> C[请求发送到服务器]:::bigFont
    B -- 否 --> C
    C --> D{用户或条件触发取消?}:::bigFont
    D -- 是 --> E[调用 cancelToken.cancel]:::bigFont
    E --> F[Dio 抛出 DioErrorType.cancel]:::bigFont
    F --> G[onError 拦截器捕获错误]:::bigFont
    G --> H[处理取消逻辑,比如忽略响应]:::bigFont
    D -- 否 --> I[服务器响应正常返回]:::bigFont
    I --> J[onResponse 拦截器处理数据]:::bigFont
    J --> K[回调调用处处理响应结果]:::bigFont

Dio与GetXController

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:dio/dio.dart';

// ------------------ 1. 创建 Controller --------------------

class PostController extends GetxController {
  // Dio 实例
  /*
    JSONPlaceholder 的 CORS 或 UA 限制
      在 Web 或某些模拟器环境下,如果没有设置请求头,有些公共接口可能会返回 403。
      特别是 User-Agent 为空时,有些服务器会拒绝请求。

    Dio 默认行为
      Dio 默认 validateStatus 会对非 2xx 的响应抛异常。
      所以你拿到 403,会直接抛 DioException,不会返回 response.statusCode。
  */
  final Dio dio = Dio(
    BaseOptions(
      baseUrl: "https://jsonplaceholder.typicode.com",
      headers: {"User-Agent": "Mozilla/5.0", "Accept": "application/json"},
      validateStatus: (status) => status! < 500, // 只对 500+ 抛异常
    ),
  );

  // 数据列表
  var posts = <dynamic>[].obs;

  // 状态
  var isLoading = false.obs;
  var isEmpty = false.obs;
  var error = "".obs;

  // 获取数据
  void fetchPosts() async {
    isLoading.value = true;
    isEmpty.value = false;
    error.value = "";

    try {
      final response = await dio.get("/posts");
      debugPrint("Status Code: ${response.statusCode}");

      if (response.statusCode == 200) {
        final data = response.data;
        if (data.isEmpty) {
          posts.clear();
          isEmpty.value = true;
        } else {
          posts.value = data;
        }
      } else {
        error.value = "请求失败: ${response.statusCode}";
      }
    } catch (e) {
      debugPrint("请求异常: $e");
      error.value = e.toString();
    } finally {
      isLoading.value = false;
    }
  }
}

// -----------------------2. 创建 Binding --------------------

class PostBinding extends Bindings {
  @override
  void dependencies() {
    // 延迟注入 PostController
    Get.lazyPut<PostController>(() => PostController());
  }
}

// -----------------------3. 创建页面 ------------------------

class PostPage extends StatelessWidget {
  PostPage({super.key});

  final PostController controller = Get.find<PostController>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Dio + GetController 示例")),
      body: Column(
        children: [
          ElevatedButton(
            onPressed: () {
              // 点击按钮触发请求
              controller.fetchPosts();
            },
            child: Text("加载数据"),
          ),
          Expanded(
            child: Obx(() {
              if (controller.isLoading.value) {
                return Center(child: CircularProgressIndicator());
              } else if (controller.error.isNotEmpty) {
                return Center(child: Text("错误: ${controller.error.value}"));
              } else if (controller.posts.isEmpty) {
                return Center(child: Text("暂无数据"));
              } else {
                return ListView.builder(
                  itemCount: controller.posts.length,
                  itemBuilder: (context, index) {
                    final post = controller.posts[index];
                    return ListTile(
                      title: Text(post["title"]),
                      subtitle: Text(post["body"]),
                    );
                  },
                );
              }
            }),
          ),
        ],
      ),
    );
  }
}

// -----------------------4. 路由配置 ------------------------

void main() {
  runApp(
    GetMaterialApp(
      initialRoute: '/posts',
      getPages: [
        GetPage(name: '/posts', page: () => PostPage(), binding: PostBinding()),
      ],
    ),
  );
}

×

喜欢就点赞,疼爱就打赏