30天计划第12天-网络请求(Dio)

-
-
2025-11-13 13:48
  • 学习内容

    1. 安装 Dio 依赖(pubspec.yaml 配置)

    2. Dio 基础:get 请求(获取数据)、post 请求(提交数据)

  • 实践任务

    1. 用 Dio 调用公开 API(如 “https://jsonplaceholder.typicode.com/posts”)

    2. 获取数据后,用 ListView 显示接口返回的 “标题列表”

一、Dio简介与环境准备

1. 什么是Dio

Dio是一个强大的Dart HTTP请求库,支持RESTful API、FormData、拦截器、请求取消、超时设置等几乎所有网络请求场景。它比Flutter内置的http包功能更丰富,是社区最受欢迎的网络请求工具之一。

2. 安装Dio依赖

  • 在pubspec.yaml文件中添加dio依赖:

    dependencies:
      flutter:
        sdk: flutter
      # 添加 dio 依赖
      dio: ^5.4.0  # 请使用 pub.dev 上的最新版本
  • 在终端执行命令安装依赖:

    flutter pub get
  • 在需要使用的文件中导入:

    import 'package:dio/dio.dart';

二、Dio基础:GET请求

1. GET请求基本用法

// 创建 Dio 实例
final dio = Dio();

void fetchData() async {
  try {
    // 发起 GET 请求
    final response = await dio.get('https://jsonplaceholder.typicode.com/posts');
    
    // 请求成功(状态码 200-299)
    print('状态码: ${response.statusCode}');
    print('响应数据: ${response.data}');
    
  } catch (e) {
    // 请求失败
    print('请求失败: $e');
  }
}

2. 处理JSON数据

通常API返回JSON格式,需要将其转换为Dart对象

// 定义数据模型
class Post {
  final int userId;
  final int id;
  final String title;
  final String body;

  Post({
    required this.userId,
    required this.id,
    required this.title,
    required this.body,
  });

  // 从 JSON 映射到对象
  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

// 解析 JSON 数据
List<Post> parsePosts(List<dynamic> responseData) {
  return responseData.map((json) => Post.fromJson(json)).toList();
}

3. 在Widget中使用网络数据

结合FutureBuilder处理异步数据

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

  @override
  State<PostListPage> createState() => _PostListPageState();
}

class _PostListPageState extends State<PostListPage> {
  final Dio _dio = Dio();
  late Future<List<Post>> _futurePosts;

  @override
  void initState() {
    super.initState();
    _futurePosts = _fetchPosts();
  }

  Future<List<Post>> _fetchPosts() async {
    try {
      final response = await _dio.get('https://jsonplaceholder.typicode.com/posts');
      if (response.statusCode == 200) {
        List<dynamic> data = response.data;
        return data.map((json) => Post.fromJson(json)).toList();
      } else {
        throw Exception('请求失败');
      }
    } catch (e) {
      throw Exception('网络错误: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('文章列表')),
      body: FutureBuilder<List<Post>>(
        future: _futurePosts,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('错误: ${snapshot.error}'));
          } else if (snapshot.hasData) {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                Post post = snapshot.data![index];
                return ListTile(
                  title: Text(post.title),
                  subtitle: Text('用户ID: ${post.userId}'),
                );
              },
            );
          } else {
            return const Center(child: Text('没有数据'));
          }
        },
      ),
    );
  }
}

三、Dio基础:POST请求

1. POST请求基本用法

void createPost() async {
  try {
    final response = await dio.post(
      'https://jsonplaceholder.typicode.com/posts',
      data: {
        'title': 'Flutter Dio Post 请求',
        'body': '这是一个测试内容',
        'userId': 1,
      },
    );
    
    print('状态码:${response.ststusCode}');
    print('创建成功:${response.data}');
  } catch (e) {
    print('请求失败:$e');
  }
}

2. 设置请求头

void postWithHeaders() async {
  try {
    final response = await dio.post(
      'https://jsonplaceholder.typicode.com/posts',
      data: {'title': 'test'},
      options: Options(
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer token',
        }
      )
    );
    print(response.data);
  } catch (e) {
    print(e);
  }
}

四、综合实战

  • 任务需求
    • 使用Dio调用https://jsonplaceholder.typicode.com/posts 获取文章数据。
    • 将返回的JSON数据解析为Dart对象列表。
    • 使用ListView.builder展示文章标题列表。
    • 添加加载状态和错误处理。
  • 代码示例

    import 'package:flutter/material.dart';
    import 'package:dio/dio.dart';
    
    
    void main() => runApp(const App());
    
    class App extends StatelessWidget{
      const App({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Dio实践',
          theme: ThemeData(primarySwatch: Colors.yellow),
          home: const PostListPage(),
        );
      }
    }
    
    class Post {
      final int userId;
      final int id;
      final String title;
      final String body;
    
      Post({
        required this.userId,
        required this.id,
        required this.title,
        required this.body
      });
    
      factory Post.fromJson(Map<String, dynamic> json) {
        return Post(userId: json['userId'], id: json['id'], title: json['title'], body: json['body']);
      }
    }
    
    class PostListPage extends StatefulWidget{
      const PostListPage({super.key});
    
      @override
      State<PostListPage> createState() {
        return _PostListPageState();
      } 
    }
    
    class _PostListPageState extends State<PostListPage>{
      final Dio _dio = Dio();
      late Future<List<Post>> _futurePosts;
    
      @override
      void initState() {
        super.initState();
        _futurePosts = _fetchPosts();
      }
    
      Future<List<Post>> _fetchPosts() async {
        try {
          final response = await _dio.get('https://jsonplaceholder.typicode.com/posts');
          if (200 == response.statusCode) {
            List<dynamic> data = response.data;
            return data.map((json) => Post.fromJson(json)).toList();
          } else {
            throw Exception('请求失败,状态码:${response.statusCode}。');
          }
        } catch (e) {
          throw Exception('网络错误:$e。');
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('文章标题列表'),
            centerTitle: true,
          ),
          body: FutureBuilder<List<Post>>(
            future: _futurePosts,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(
                  child: CircularProgressIndicator(),
                );
              } else if (snapshot.hasError) {
                return Center(
                  child: Text(
                    '加载失败:${snapshot.error}。',
                    style: const TextStyle(color: Colors.red),
                  ),
                );
              } else if (snapshot.hasData) {
                return ListView.builder(
                  itemCount: snapshot.data!.length,
                  itemBuilder: (context, index) {
                    Post post = snapshot.data![index];
                    return ListTile(
                      title: Text(
                        post.title,
                        style: const TextStyle(fontSize: 16),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                      subtitle: Text(
                        '作者ID:${post.userId}',
                        style: TextStyle(color: Colors.grey[600]),
                      ),
                      leading: CircleAvatar(
                        child: Text('${post.id}'),
                      ),
                      onTap: () {
                        // 先关闭当前正在显示的 SnackBar(如果有),否则不会立马显示新的SnackBar
                        ScaffoldMessenger.of(context).hideCurrentSnackBar();
                        // 再显示新的 SnackBar
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('你点击了第${index + 1}篇文章')),
                        );
                      },
                    );
                  },
                );
              } else {
                return const Center(child: Text('没有数据'),);
              }
            },
          ),
        );
      }
    }
  • 补充说明
    • ScaffoldMessenger 的 showSnackBar 方法有个默认行为:如果当前已经有 SnackBar 在显示,新调用的 showSnackBar 不会替换它,而是把新 SnackBar 加入队列,等前一个执行完(消失)再执行。
    • 解决方法

      要实现 “快速点击时,新提示直接替换旧提示”,只需在 showSnackBar 前,先调用 hideCurrentSnackBar() 关闭当前显示的 SnackBar 即可。

      onTap: () {
        // 关键:先关闭当前正在显示的 SnackBar(如果有)
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
        // 再显示新的 SnackBar
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('你点击了第 ${index + 1} 篇文章'),
            duration: const Duration(milliseconds: 1000), // 可选:缩短显示时间(默认2秒)
          ),
        );
      }
    • 可选优化

      SnackBar(
        content: Text('你点击了第 ${index + 1} 篇文章'),
        duration: const Duration(milliseconds: 800), // 800毫秒后自动消失
        behavior: SnackBarBehavior.floating, // 可选:悬浮样式(默认是底部嵌入)
      )
  • 运行效果

五、常见问题与注意事项

1. 网络权限

  • Android:在AndroidMainfest.xml中添加

    <uses-permission android:name="android.permission.INTERNET"/>
  • iOS:在Info.plist中添加

    NSAppTransportSecurity -> NSAllowsArbitraryLoads = YES(如果使用 HTTP)

2. 异步处理

网络请求是异步操作,必须使用FutureBuilder或Consumer(结合状态管理)来更新UI。

3. 数据模型

建议使用json_serializable等工具自动生成JSON解析代码,提高效率和准确性。

4. 状态管理

在复杂应用中,建议将网络请求逻辑放在状态管理(如:Provider,Bloc)中,而不是直接在Widget中处理。


目录