今天来学学flutter的网络请求

Dio插件的使用

dio 是 Flutter 里最常用的 HTTP 网络请求库之一,很多项目都会用它替代原生的 http 包,因为它功能更完整

(话说看到这个Dio我就想起了某个埃及艳妇 狗头狗头狗头)

OK回归正题

首先根目录执行

已复制
flutter pub add dio

打开你的pubspec.yaml文件

可以看到这个

YAML已复制
dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8
  dio: ^5.9.2     #dio下载完成

基本使用

Dio().get(地址).then().catchError()

DART已复制
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

void main() {
  runApp(MaterialApp(home: HomePage()));
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String text = "点击获取数据";

  void getData() async {
    Response response = await Dio().get('https://genshin.hoyoverse.com/en/');

    setState(() {
      text = response.data.toString();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("原神网页"), centerTitle: true),

      body: Center(
        child: SingleChildScrollView(
          child: Padding(
            padding: EdgeInsets.all(20),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: getData,
                  child: Text("获取网页", style: TextStyle(fontSize: 18)),
                ),

                SizedBox(height: 20),

                Text(text, textAlign: TextAlign.center),
              ],
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: getData,
        child: Icon(Icons.refresh),
      ),
    );
  }
}
image-20260517163247875

封装Dio工具

​ image-20260614195619480

建立连接 → 发送数据 → 等待服务器返回
↑ ↑ ↑
connect send receive

DART已复制
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

void main() {}

class DioUtils {
  final Dio _dio = Dio(); // 创建Dio实例
  DioUtils() {
    _dio.options.baseUrl = 'https://github.com/'; // 基础地址
    _dio.options.connectTimeout = Duration(seconds: 5); // 连接超时,类型为Duration
    _dio.options.sendTimeout = Duration(seconds: 5); // 发送超时,类型为Duration
    _dio.options.receiveTimeout = Duration(seconds: 5); // 响应超时,类型为Duration
  }
}

我们可以创建Dio工具类和实例对象,但是这种写法较为繁琐

使用“..”级操作符

作用是对同一个对象连续调用多个属性或方法时,不需要反复写对象名

DART已复制
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

void main() {}

class DioUtils {
  final Dio _dio = Dio(); // 创建Dio实例
  DioUtils() {
   
    _dio.options
      ..baseUrl =
          'https://github.com/' // 基础地址
      ..connectTimeout =
          Duration(seconds: 5) // 连接超时,类型为Duration
      ..sendTimeout =
          Duration(seconds: 5) // 发送超时,类型为Duration
      ..receiveTimeout = Duration(seconds: 5); // 响应超时,类型为Duration
  }
}
DART已复制
// ❌ 不用级联:重复写对象名
var buffer = StringBuffer();
buffer.write('Hello');
buffer.write(' ');
buffer.write('World');
var result = buffer.toString();

// ✅ 用级联:链式调用,更简洁
var result = (StringBuffer()
  ..write('Hello')
  ..write(' ')
  ..write('World'))
  .toString();

使用BaseOptions

flutter 的 BaseOptions 是 Dio 网络库中的核心配置类,用于定义全局请求的基础参数。它本身不是 Flutter SDK 的一部分,而是来自 dio 包

BaseOptions 封装了所有 HTTP 请求的默认配置,创建 Dio 实例时传入,后续所有请求都会继承这些基础参数

DART已复制
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

void main() {}

class DioUtils {
  final Dio _dio = Dio(
    BaseOptions(
      baseUrl: 'https://github.com/',
      connectTimeout: Duration(seconds: 5),
      sendTimeout: Duration(seconds: 5),
      receiveTimeout: Duration(seconds: 5),
    ),
  );
}

Dio拦截器(Interceptors)

拦截器是 Dio 的核心机制,允许你在请求发出前和响应返回后插入自定义逻辑,实现统一的日志、鉴权、错误处理、缓存等

Flutter 里的 Dio 拦截器(Interceptors)主要用于统一处理请求、响应、错误,比如:

  • 自动添加 Token
  • 打印网络日志
  • 刷新 Token
  • 统一错误处理
  • 请求加密/解密
  • Loading 状态管理

Dio 的拦截器语法核心是继承 Interceptor,然后重写三个方法:

已复制
onRequest()
onResponse()
onError()

三个阶段执行顺序

一次请求:

已复制
dio.get()
    |
    ↓
onRequest()
    |
    ↓
服务器
    |
    ↓
onResponse()
    |
    ↓
业务代码

如果失败:

已复制
dio.get()
    |
    ↓
onRequest()
    |
    ↓
服务器
    |
    ↓
onError()

基本结构

先导入:

DART已复制
import 'package:dio/dio.dart';

创建 Dio:

DART已复制
final dio = Dio();

添加拦截器:

DART已复制
dio.interceptors.add(
  MyInterceptor(),
);

定义拦截器:

DART已复制
class MyInterceptor extends Interceptor {  

  @override
  void onRequest(
      RequestOptions options,
      RequestInterceptorHandler handler,
  ) {

    print("请求地址: ${options.uri}");

    handler.next(options);
  }


  @override
  void onResponse(
      Response response,
      ResponseInterceptorHandler handler,
  ) {

    print("响应数据: ${response.data}");

    handler.next(response);
  }


  @override
  void onError(
      DioException err,
      ErrorInterceptorHandler handler,
  ) {

    print("请求失败: ${err.message}");

    handler.next(err);
  }
}

handler 三种操作

Request
已复制
handler.next(options);

意思: 处理完了,继续发送请求

Response
已复制
handler.next(response);

意思: 继续返回数据

Error
已复制
handler.next(err);

意思: 继续抛错误

我们先试一下在终端输出

其中onRequest() onResponse() onError()函数里的参数可以直接写参数名,无需声明类型

DART已复制
import 'package:dio/dio.dart';

void main() async {
  Dio dio = Dio();

  dio.interceptors.add(
    InterceptorsWrapper(
      // 请求前
      onRequest: (options, handler) {
        print("===发送请求===");
        print("请求地址: ${options.uri}");

        handler.next(options);
      },

      // 成功返回
      onResponse: (response, handler) {
        print("===收到响应===");
        print(response.data);

        handler.next(response);
      },

      // 出错
      onError: (error, handler) {
        print("===发生错误===");
        print(error.message);

        handler.next(error);
      },
    ),
  );

  try {
    Response res = await dio.get(
      'https://jsonplaceholder.typicode.com/posts/1',
    );

    print("===最终结果===");
    print(res.data);
  } catch (e) {
    print(e);
  }
}
DART已复制
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

void main() {}

class DioUtils {
  final Dio _dio = Dio(); // 创建Dio实例

  //设置基础地址和超时时间
  DioUtils() {
    _dio.options
      ..baseUrl =
          'https://jsonplaceholder.typicode.com' // 基础地址
      ..connectTimeout =
          Duration(seconds: 5) // 连接超时,类型为Duration
      ..sendTimeout =
          Duration(seconds: 5) // 发送超时,类型为Duration
      ..receiveTimeout = Duration(seconds: 5); // 响应超时,类型为Duration

    // 添加请求拦截器
    _addInterceptors();
  }

  // 添加拦截器
  void _addInterceptors() {
    _dio.interceptors.add(
      InterceptorsWrapper(
        // 请求拦截器
        onRequest: (context, handler) {
          handler.next(context);
        },
        // 响应拦截器
        onResponse: (context, handler) {
          //http状态码为2XX成功,3XX跳转,4XX错误,5XX服务器错误
          if (context.statusCode! >= 200 && context.statusCode! < 300) {
            handler.next(context);
            return;
          }
          // 其他状态码视为错误,抛出异常
          handler.reject(DioException(requestOptions: context.requestOptions));
        },
        // 错误拦截器
        onError: (context, handler) {
          handler.reject(context);
        },
      ),
    );
  }

  //向外暴露get方法,封装请求
  Future<Response> get(String url, {Map<String, dynamic>? params}) {
    return _dio.get(url, queryParameters: params);
  }
}

然后与MaterialApp结合

DART已复制
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

void main() {
  runApp(const MaterialApp(home: DioDemoPage()));
}

class DioDemoPage extends StatefulWidget {
  const DioDemoPage({super.key});

  @override
  State<DioDemoPage> createState() => _DioDemoPageState();
}

class _DioDemoPageState extends State<DioDemoPage> {
  final Dio dio = Dio();

  String result = "点击按钮发送请求";

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

    dio.interceptors.add(
      InterceptorsWrapper(
        // 请求前
        onRequest: (options, handler) {
          print("===== 请求拦截器 =====");
          print("请求地址: ${options.uri}");

          handler.next(options);
        },

        // 请求成功
        onResponse: (response, handler) {
          print("===== 响应拦截器 =====");
          print("状态码: ${response.statusCode}");

          handler.next(response);
        },

        // 请求失败
        onError: (error, handler) {
          print("===== 错误拦截器 =====");
          print(error.message);

          handler.next(error);
        },
      ),
    );
  }

  Future<void> loadData() async {
    try {
      Response response = await dio.get(
        "https://jsonplaceholder.typicode.com/posts/1",
      );

      setState(() {
        result = response.data.toString();
      });
    } catch (e) {
      setState(() {
        result = e.toString();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Dio拦截器演示"), centerTitle: true),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Center(
          child: Column(
            children: [
              ElevatedButton(onPressed: loadData, child: const Text("发送请求")),
              const SizedBox(height: 20),
              Expanded(child: SingleChildScrollView(child: Text(result))),
            ],
          ),
        ),
      ),
    );
  }
}

初始化获取数据

我们使用测试网站https://jsonplaceholder.typicode.com/

我们可以使用如下后缀来测试数据

路径 数量
/posts 100 posts
/comments 500 comments
/albums 100 albums
/photos 5000 photos
/todos 200 todos
/users 10 users

这里我们使用/posts/1获得第一篇测试文章的数据

GET /posts
GET /posts/1
GET /posts/1/comments
GET /comments?postId=1
POST /posts
PUT /posts/1
PATCH /posts/1
DELETE /posts/1

​ image-20260718170625979

对比一下网站内容,完全一致

​ image-20260718170658143

DART已复制
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(home: MainPage()));
}

class MainPage extends StatefulWidget {
  const MainPage({super.key});

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  String result = '点击获取数据';
  //初始化
  void initState() {
    //使用initState()方法初始化
    super.initState();
  }

  void _getChannels() async {
    DioUtils util = DioUtils();

    try {
      // 获取数据
      Response<dynamic> response = await util.get('/posts/1');
      setState(() {
        result = response.data.toString();
      });
      print(response);
    } catch (e) {
      setState(() {
        result = e.toString();
      });
      print(e);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Dio')),

      body: Padding(
        padding: EdgeInsets.all(16),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(onPressed: _getChannels, child: Text("获取数据")),
              const SizedBox(height: 20),
              Expanded(child: SingleChildScrollView(child: Text(result))),
            ],
          ),
        ),
      ),
    );
  }
}

//Dio 工具类
class DioUtils {
  final Dio _dio = Dio(); // 创建Dio实例

  //设置基础地址和超时时间
  DioUtils() {
    _dio.options
      ..baseUrl =
          'https://jsonplaceholder.typicode.com' // 基础地址
      ..connectTimeout =
          Duration(seconds: 5) // 连接超时,类型为Duration
      // ..sendTimeout =
      //     Duration(seconds: 5) // 发送超时,类型为Duration
      ..receiveTimeout = Duration(seconds: 5); // 响应超时,类型为Duration

    // 添加请求拦截器
    _addInterceptors();
  }

  // 添加拦截器
  void _addInterceptors() {
    _dio.interceptors.add(
      InterceptorsWrapper(
        // 请求拦截器
        onRequest: (context, handler) {
          handler.next(context);
        },
        // 响应拦截器
        onResponse: (context, handler) {
          //http状态码为2XX成功,3XX跳转,4XX错误,5XX服务器错误
          if (context.statusCode! >= 200 && context.statusCode! < 300) {
            handler.next(context);
            return;
          }
          // 其他状态码视为错误,抛出异常
          handler.reject(DioException(requestOptions: context.requestOptions));
        },
        // 错误拦截器
        onError: (context, handler) {
          handler.reject(context);
        },
      ),
    );
  }

  //向外暴露get方法,封装请求
  Future<Response<dynamic>> get(String url, {Map<String, dynamic>? params}) {
    return _dio.get(url, queryParameters: params);
  }
}

web端跨域问题

默认情况下,flutter运行 web 端加载网络资源会报跨域可能提示错误,解决方法是:

首先在flutter/packages/flutter_tools/lib/src/web/chrome.dart里添加’–disable-web-security’,

​ image-20260903222328495

然后删除flutter/bin/cache/下的 flutter_tools.snaphot和flutter_tools.stamp缓存文件

最后执行 flutter doctor -v

然后重新运行项目

这个我们单独在一篇文章里解释

父传子实现

DART已复制
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(debugShowCheckedModeBanner: false, home: MainPage()),
  );
}

// =========================
// 数据模型
// =========================

class PinData {
  final String title;
  final String imageUrl;

  PinData({required this.title, required this.imageUrl});
}

// =========================
// 父组件
// =========================

class MainPage extends StatefulWidget {
  const MainPage({super.key});

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  // 父组件保存数据
  List<PinData> pins = [];

  bool loading = false;

  // 获取数据
  Future<void> _getChannels() async {
    DioUtils util = DioUtils();

    setState(() {
      loading = true;
    });

    try {
      Response response = await util.get('/posts');

      List data = response.data;

      setState(() {
        pins = data.map((item) {
          return PinData(
            title: item['title'],

            // 暂时使用随机图片测试
            imageUrl: 'https://picsum.photos/300/${300 + item['id'] * 20}',
          );
        }).toList();

        loading = false;
      });
    } catch (e) {
      print(e);

      setState(() {
        loading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Pinterest Demo')),

      body: loading
          ? const Center(child: CircularProgressIndicator())
          : pins.isEmpty
          ? Center(
              child: ElevatedButton(
                onPressed: _getChannels,
                child: const Text('获取数据'),
              ),
            )
          : PinGrid(
              // =========================
              // 父传子
              // =========================
              pins: pins,
            ),
    );
  }
}

// =========================
// 子组件:图片网格
// =========================

class PinGrid extends StatelessWidget {
  final List<PinData> pins;

  const PinGrid({super.key, required this.pins});

  @override
  Widget build(BuildContext context) {
    // 获取当前窗口宽度
    double width = MediaQuery.of(context).size.width;

    // 根据屏幕宽度决定列数
    int columns;

    if (width < 600) {
      columns = 2; // 手机
    } else if (width < 900) {
      columns = 3; // 平板
    } else if (width < 1200) {
      columns = 4; // 小电脑
    } else if (width < 1600) {
      columns = 5; // 普通电脑
    } else {
      columns = 6; // 大屏幕
    }

    return GridView.builder(
      padding: const EdgeInsets.all(16),

      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: columns,

        // 横向间距
        crossAxisSpacing: 16,

        // 纵向间距
        mainAxisSpacing: 16,

        // 卡片比例
        childAspectRatio: 0.7,
      ),

      itemCount: pins.length,

      itemBuilder: (context, index) {
        return PinCard(pin: pins[index]);
      },
    );
  }
}

// =========================
// 子组件:一张图片卡片
// =========================

class PinCard extends StatelessWidget {
  final PinData pin;

  const PinCard({super.key, required this.pin});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 图片
        Expanded(
          child: ClipRRect(
            borderRadius: BorderRadius.circular(16),
            child: Image.network(
              pin.imageUrl,
              width: double.infinity,
              fit: BoxFit.cover,

              // 加载失败
              errorBuilder: (context, error, stackTrace) {
                return const Center(child: Icon(Icons.error));
              },
            ),
          ),
        ),

        const SizedBox(height: 6),

        // 标题
        Text(
          pin.title,
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
          style: const TextStyle(fontWeight: FontWeight.bold),
        ),
      ],
    );
  }
}

// =========================
// Dio 工具类
// =========================

class DioUtils {
  final Dio _dio = Dio();

  DioUtils() {
    _dio.options
      ..baseUrl = 'https://jsonplaceholder.typicode.com'
      ..connectTimeout = const Duration(seconds: 5)
      ..receiveTimeout = const Duration(seconds: 5);

    _addInterceptors();
  }

  void _addInterceptors() {
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (context, handler) {
          handler.next(context);
        },

        onResponse: (context, handler) {
          if (context.statusCode! >= 200 && context.statusCode! < 300) {
            handler.next(context);
            return;
          }

          handler.reject(DioException(requestOptions: context.requestOptions));
        },

        onError: (context, handler) {
          handler.reject(context);
        },
      ),
    );
  }

  Future<Response<dynamic>> get(String url, {Map<String, dynamic>? params}) {
    return _dio.get(url, queryParameters: params);
  }
}

​ image-20260903225959320