Flutter 小白入门教程Flutter 小白入门教程
首页
学习指南
项目实战
Flutter 官网
编程指南
首页
学习指南
项目实战
Flutter 官网
编程指南
  • 入门基础

    • 📚 基础教程
    • 第1章 - 认识 Flutter
    • 第2章 - 环境搭建
    • 第3章 - Dart 语言基础
    • 第4章 - 第一个 Flutter 应用
    • 第5章 - Widget 基础
    • 第6章 - 布局系统
    • 第7章 - 状态管理入门
    • 第8章 - 页面导航
    • 第9章 - 资源管理
  • 进阶开发

    • 第10章 - 网络请求
    • 第11章 - 本地存储
    • 第12章 - 对话框与反馈
    • 第13章 - 列表进阶
    • 第14章 - 主题定制
    • 第15章 - 状态管理进阶
    • 第16章 - 动画入门
    • 第17章 - 常用第三方包
  • 调试与发布

    • 第18章 - 调试与性能优化
    • 第19章 - 打包与发布
  • 附录

    • 附录A - UI 框架与组件库推荐
    • 附录B - 项目结构最佳实践
    • 附录C - 国际化配置
    • 附录D - 权限处理

第10章 - 网络请求

嗨,朋友!我是长安。

现代 App 基本都需要从网络获取数据,比如获取新闻列表、用户信息、天气数据等。这一章,我们来学习如何在 Flutter 中进行网络请求。

🌐 HTTP 基础知识

常见的 HTTP 方法

方法用途示例
GET获取数据获取用户列表
POST创建数据注册新用户
PUT更新数据(全量)更新用户信息
PATCH更新数据(部分)修改用户昵称
DELETE删除数据删除用户

HTTP 状态码

2xx - 成功
  200 OK:请求成功
  201 Created:创建成功

4xx - 客户端错误
  400 Bad Request:请求参数错误
  401 Unauthorized:未授权(需要登录)
  403 Forbidden:禁止访问
  404 Not Found:资源不存在

5xx - 服务器错误
  500 Internal Server Error:服务器内部错误
  502 Bad Gateway:网关错误
  503 Service Unavailable:服务不可用

📦 使用 http 包

Flutter 官方推荐使用 http 包进行网络请求。

1. 添加依赖

在 pubspec.yaml 中添加:

dependencies:
  http: ^1.1.0

然后运行 flutter pub get。

2. 基本 GET 请求

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> fetchData() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
  );

  if (response.statusCode == 200) {
    // 请求成功,解析 JSON
    final data = json.decode(response.body);
    print('标题: ${data['title']}');
  } else {
    // 请求失败
    print('请求失败: ${response.statusCode}');
  }
}

3. GET 请求带参数

Future<void> searchUsers(String keyword) async {
  // 方式一:手动拼接
  final url = 'https://api.example.com/search?q=$keyword';
  
  // 方式二:使用 Uri(推荐,自动处理特殊字符)
  final uri = Uri.https('api.example.com', '/search', {
    'q': keyword,
    'page': '1',
    'limit': '20',
  });

  final response = await http.get(uri);
  // 处理响应...
}

4. POST 请求

Future<void> createUser(String name, String email) async {
  final response = await http.post(
    Uri.parse('https://api.example.com/users'),
    headers: {
      'Content-Type': 'application/json',
    },
    body: json.encode({
      'name': name,
      'email': email,
    }),
  );

  if (response.statusCode == 201) {
    final newUser = json.decode(response.body);
    print('创建成功,用户ID: ${newUser['id']}');
  } else {
    print('创建失败: ${response.body}');
  }
}

5. 带 Token 的请求

Future<void> fetchUserProfile(String token) async {
  final response = await http.get(
    Uri.parse('https://api.example.com/profile'),
    headers: {
      'Authorization': 'Bearer $token',
      'Content-Type': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    final profile = json.decode(response.body);
    print('用户名: ${profile['name']}');
  } else if (response.statusCode == 401) {
    print('登录已过期,请重新登录');
  }
}

🔄 在 Widget 中使用网络请求

使用 FutureBuilder

FutureBuilder 是处理异步数据最常用的方式:

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

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

class _PostListPageState extends State<PostListPage> {
  late Future<List<Post>> _futurePosts;

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

  Future<List<Post>> fetchPosts() async {
    final response = await http.get(
      Uri.parse('https://jsonplaceholder.typicode.com/posts'),
    );

    if (response.statusCode == 200) {
      final List<dynamic> jsonData = json.decode(response.body);
      return jsonData.map((json) => Post.fromJson(json)).toList();
    } else {
      throw Exception('加载失败');
    }
  }

  @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());
          }
          
          // 加载出错
          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(Icons.error, size: 60, color: Colors.red),
                  const SizedBox(height: 16),
                  Text('错误: ${snapshot.error}'),
                  const SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        _futurePosts = fetchPosts();
                      });
                    },
                    child: const Text('重试'),
                  ),
                ],
              ),
            );
          }
          
          // 加载成功
          final posts = snapshot.data!;
          return ListView.builder(
            itemCount: posts.length,
            itemBuilder: (context, index) {
              final post = posts[index];
              return ListTile(
                title: Text(post.title),
                subtitle: Text(
                  post.body,
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
              );
            },
          );
        },
      ),
    );
  }
}

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

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

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

手动管理状态

有时候你需要更细粒度的控制:

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

  @override
  State<UserListPage> createState() => _UserListPageState();
}

class _UserListPageState extends State<UserListPage> {
  List<User> _users = [];
  bool _isLoading = false;
  String? _error;

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

  Future<void> _loadUsers() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final response = await http.get(
        Uri.parse('https://jsonplaceholder.typicode.com/users'),
      );

      if (response.statusCode == 200) {
        final List<dynamic> jsonData = json.decode(response.body);
        setState(() {
          _users = jsonData.map((json) => User.fromJson(json)).toList();
          _isLoading = false;
        });
      } else {
        throw Exception('请求失败: ${response.statusCode}');
      }
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

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

  Widget _buildBody() {
    if (_isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    if (_error != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('错误: $_error'),
            ElevatedButton(
              onPressed: _loadUsers,
              child: const Text('重试'),
            ),
          ],
        ),
      );
    }

    return 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),
        );
      },
    );
  }
}

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'],
      name: json['name'],
      email: json['email'],
    );
  }
}

🔧 使用 Dio(推荐)

Dio 是一个更强大的 HTTP 客户端,支持拦截器、文件上传下载、超时等高级功能。

1. 添加依赖

dependencies:
  dio: ^5.4.0

2. 基本用法

import 'package:dio/dio.dart';

final dio = Dio();

// GET 请求
Future<void> getUser() async {
  final response = await dio.get('https://api.example.com/user/1');
  print(response.data);
}

// POST 请求
Future<void> createUser() async {
  final response = await dio.post(
    'https://api.example.com/users',
    data: {
      'name': '长安',
      'email': 'changan@example.com',
    },
  );
  print(response.data);
}

3. 配置 Dio

class ApiService {
  static final Dio _dio = Dio(
    BaseOptions(
      baseUrl: 'https://api.example.com',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
      headers: {
        'Content-Type': 'application/json',
      },
    ),
  );

  // 添加拦截器
  static void init() {
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          // 请求前处理(如添加 Token)
          final token = getToken(); // 从本地获取 Token
          if (token != null) {
            options.headers['Authorization'] = 'Bearer $token';
          }
          print('请求: ${options.uri}');
          return handler.next(options);
        },
        onResponse: (response, handler) {
          // 响应处理
          print('响应: ${response.statusCode}');
          return handler.next(response);
        },
        onError: (error, handler) {
          // 错误处理
          print('错误: ${error.message}');
          if (error.response?.statusCode == 401) {
            // Token 过期,跳转登录页
          }
          return handler.next(error);
        },
      ),
    );
  }

  // GET 请求
  static Future<Response> get(String path, {Map<String, dynamic>? params}) {
    return _dio.get(path, queryParameters: params);
  }

  // POST 请求
  static Future<Response> post(String path, {dynamic data}) {
    return _dio.post(path, data: data);
  }
}

4. 错误处理

Future<void> fetchData() async {
  try {
    final response = await dio.get('https://api.example.com/data');
    print(response.data);
  } on DioException catch (e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
        print('连接超时');
        break;
      case DioExceptionType.receiveTimeout:
        print('接收超时');
        break;
      case DioExceptionType.badResponse:
        print('服务器错误: ${e.response?.statusCode}');
        break;
      case DioExceptionType.connectionError:
        print('网络连接失败,请检查网络');
        break;
      default:
        print('请求失败: ${e.message}');
    }
  }
}

5. 文件上传

Future<void> uploadImage(File file) async {
  final formData = FormData.fromMap({
    'file': await MultipartFile.fromFile(
      file.path,
      filename: 'avatar.jpg',
    ),
    'description': '用户头像',
  });

  final response = await dio.post(
    'https://api.example.com/upload',
    data: formData,
    onSendProgress: (sent, total) {
      final progress = (sent / total * 100).toStringAsFixed(0);
      print('上传进度: $progress%');
    },
  );

  print('上传成功: ${response.data}');
}

🏗️ API 服务封装

实际项目中,建议封装 API 服务:

// lib/services/api_service.dart
import 'package:dio/dio.dart';

class ApiService {
  static final Dio _dio = Dio(
    BaseOptions(
      baseUrl: 'https://api.example.com',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
    ),
  );

  // 用户相关 API
  static Future<User> getUser(int id) async {
    final response = await _dio.get('/users/$id');
    return User.fromJson(response.data);
  }

  static Future<List<User>> getUsers() async {
    final response = await _dio.get('/users');
    return (response.data as List)
        .map((json) => User.fromJson(json))
        .toList();
  }

  static Future<User> createUser(String name, String email) async {
    final response = await _dio.post('/users', data: {
      'name': name,
      'email': email,
    });
    return User.fromJson(response.data);
  }

  static Future<void> deleteUser(int id) async {
    await _dio.delete('/users/$id');
  }

  // 文章相关 API
  static Future<List<Post>> getPosts({int page = 1, int limit = 20}) async {
    final response = await _dio.get('/posts', queryParameters: {
      'page': page,
      'limit': limit,
    });
    return (response.data as List)
        .map((json) => Post.fromJson(json))
        .toList();
  }
}

// 使用
void main() async {
  final users = await ApiService.getUsers();
  final user = await ApiService.getUser(1);
  final newUser = await ApiService.createUser('张三', 'zhangsan@example.com');
}

📱 完整实战示例

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '新闻列表',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const NewsListPage(),
    );
  }
}

// 新闻模型
class News {
  final int id;
  final String title;
  final String summary;
  final String imageUrl;
  final String date;

  News({
    required this.id,
    required this.title,
    required this.summary,
    required this.imageUrl,
    required this.date,
  });

  factory News.fromJson(Map<String, dynamic> json) {
    return News(
      id: json['id'],
      title: json['title'],
      summary: json['body'].toString().substring(0, 50),
      imageUrl: 'https://picsum.photos/200/100?random=${json['id']}',
      date: '2024-01-${json['id'].toString().padLeft(2, '0')}',
    );
  }
}

// 新闻列表页
class NewsListPage extends StatefulWidget {
  const NewsListPage({super.key});

  @override
  State<NewsListPage> createState() => _NewsListPageState();
}

class _NewsListPageState extends State<NewsListPage> {
  final Dio _dio = Dio();
  List<News> _newsList = [];
  bool _isLoading = false;
  bool _hasError = false;

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

  Future<void> _loadNews() async {
    setState(() {
      _isLoading = true;
      _hasError = false;
    });

    try {
      final response = await _dio.get(
        'https://jsonplaceholder.typicode.com/posts',
        queryParameters: {'_limit': 20},
      );

      setState(() {
        _newsList = (response.data as List)
            .map((json) => News.fromJson(json))
            .toList();
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _hasError = true;
        _isLoading = false;
      });
    }
  }

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

  Widget _buildBody() {
    if (_isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    if (_hasError) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error_outline, size: 60, color: Colors.grey),
            const SizedBox(height: 16),
            const Text('加载失败,请重试'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _loadNews,
              child: const Text('重新加载'),
            ),
          ],
        ),
      );
    }

    return RefreshIndicator(
      onRefresh: _loadNews,
      child: ListView.builder(
        itemCount: _newsList.length,
        itemBuilder: (context, index) {
          final news = _newsList[index];
          return Card(
            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: InkWell(
              onTap: () {
                // 跳转到详情页
              },
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Row(
                  children: [
                    ClipRRect(
                      borderRadius: BorderRadius.circular(8),
                      child: Image.network(
                        news.imageUrl,
                        width: 100,
                        height: 80,
                        fit: BoxFit.cover,
                        errorBuilder: (context, error, stackTrace) {
                          return Container(
                            width: 100,
                            height: 80,
                            color: Colors.grey[200],
                            child: const Icon(Icons.image),
                          );
                        },
                      ),
                    ),
                    const SizedBox(width: 12),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            news.title,
                            style: const TextStyle(
                              fontSize: 16,
                              fontWeight: FontWeight.bold,
                            ),
                            maxLines: 2,
                            overflow: TextOverflow.ellipsis,
                          ),
                          const SizedBox(height: 4),
                          Text(
                            news.summary,
                            style: TextStyle(
                              color: Colors.grey[600],
                              fontSize: 14,
                            ),
                            maxLines: 2,
                            overflow: TextOverflow.ellipsis,
                          ),
                          const SizedBox(height: 4),
                          Text(
                            news.date,
                            style: TextStyle(
                              color: Colors.grey[400],
                              fontSize: 12,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

📝 小结

这一章我们学习了:

  • ✅ HTTP 基础知识(方法、状态码)
  • ✅ 使用 http 包进行网络请求
  • ✅ 使用 FutureBuilder 处理异步数据
  • ✅ 使用 Dio 进行更强大的网络请求
  • ✅ 拦截器、错误处理、文件上传
  • ✅ API 服务的封装

网络请求最佳实践

  1. 统一封装 - 把 API 请求封装成服务类
  2. 错误处理 - 处理网络异常和业务错误
  3. 加载状态 - 显示加载中、加载失败的状态
  4. 数据缓存 - 合理缓存数据,减少请求
  5. 超时设置 - 设置合理的超时时间

💪 练习题

  1. 使用 http 包请求一个免费 API,在列表中显示数据
  2. 封装一个 API 服务类,包含 GET 和 POST 方法
  3. 实现一个带下拉刷新功能的列表页面

🚀 下一步

学会了网络请求后,下一章我们来学习 本地存储,让应用能够持久化数据!


由 编程指南 提供

最近更新: 2026/2/3 16:24
Contributors: 王长安
Next
第11章 - 本地存储