4.2 网络请求与数据解析

基础知识

现代移动应用几乎都离不开网络请求,用于获取远程数据、与后端服务交互。Flutter 提供了多种进行网络请求的方式,最常用的是 Dart 内置的 http 包和第三方库 dio。数据解析通常涉及 JSON 格式,Dart 提供了内置的 dart:convert 库来处理 JSON。

1. http

http 是 Dart 官方推荐的用于进行 HTTP 请求的包。它提供了 getpostputdelete 等方法,以及处理请求头、请求体和响应的能力。

  • 添加依赖:在 pubspec.yaml 中添加 http 依赖。
    yaml
    复制代码

dependencies:
http: ^1.1.0 # 使用最新版本
```
运行 flutter pub get

  • 基本用法
    dart
    复制代码
    import 'package:http/http.dart' as http;
    import 'dart:convert'; // 用于 JSON 解析
    
    Future<void> fetchData() async {
      final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/1');
      try {
        final response = await http.get(url);
    
        if (response.statusCode == 200) {
          // 请求成功,解析 JSON 数据
          final Map<String, dynamic> data = jsonDecode(response.body);
          print('数据: $data');
          print('标题: ${data["title"]}');
        } else {
          // 请求失败
          print('请求失败,状态码: ${response.statusCode}');
        }
      } catch (e) {
        // 捕获网络错误
        print('网络请求错误: $e');
      }
    }
    
    void main() {
      fetchData();
    }

2. dio

dio 是一个功能强大的 Dart HTTP 客户端,它支持拦截器、全局配置、FormData、请求取消、文件上传/下载、超时等功能,更适合复杂的网络请求场景。

  • 添加依赖:在 pubspec.yaml 中添加 dio 依赖。
    yaml
    复制代码

dependencies:
dio: ^5.4.0 # 使用最新版本
```
运行 flutter pub get

  • 基本用法
    dart
    复制代码
    import 'package:dio/dio.dart';
    
    Future<void> fetchDataWithDio() async {
      final dio = Dio();
      try {
        final response = await dio.get('https://jsonplaceholder.typicode.com/posts/1');
    
        if (response.statusCode == 200) {
          // dio 自动解析 JSON,response.data 就是解析后的数据
          final Map<String, dynamic> data = response.data;
          print('数据: $data');
          print('标题: ${data["title"]}');
        } else {
          print('请求失败,状态码: ${response.statusCode}');
        }
      } on DioException catch (e) {
        // 捕获 Dio 异常
        if (e.response != null) {
          print('Dio 错误响应: ${e.response!.statusCode}');
          print('Dio 错误数据: ${e.response!.data}');
        } else {
          print('Dio 请求错误: ${e.message}');
        }
      } catch (e) {
        print('其他错误: $e');
      }
    }
    
    void main() {
      fetchDataWithDio();
    }

3. 数据解析 (JSON)

Dart 的 dart:convert 库提供了 jsonEncodejsonDecode 函数,用于 JSON 字符串和 Dart 对象之间的转换。

  • jsonDecode(String source):将 JSON 字符串解析为 Dart 对象(Map<String, dynamic>List<dynamic>)。
  • jsonEncode(Object object):将 Dart 对象编码为 JSON 字符串。

对于复杂的 JSON 结构,手动解析会变得繁琐且容易出错。推荐使用 JSON 序列化工具,例如 json_serializable,它可以自动生成 Dart 模型的 fromJsontoJson 方法。

  • 使用 json_serializable
    1. 添加依赖:在 pubspec.yaml 中添加 json_annotationjson_serializablebuild_runner 依赖。
      yaml
      复制代码

dependencies:
json_annotation: ^4.8.1 # 最新版本可能不同

dev_dependencies:
build_runner: ^2.4.8 # 最新版本可能不同
json_serializable: ^6.7.1 # 最新版本可能不同
运行 `flutter pub get`。 2. **创建模型类**:使用 `json_annotation` 注解。 dart
import 'package:json_annotation/json_annotation.dart';

plaintext
复制代码
    part 'post.g.dart'; // 这行是自动生成的文件,需要手动添加

    @JsonSerializable()
    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) => _$PostFromJson(json);
      Map<String, dynamic> toJson() => _$PostToJson(this);
    }
    ```
3.  **运行代码生成器**:在终端运行 `flutter pub run build_runner build`。这会生成 `post.g.dart` 文件。
4.  **使用模型**:
    ```dart
    final Map<String, dynamic> jsonMap = jsonDecode(response.body);
    final Post post = Post.fromJson(jsonMap);
    print(post.title);
    ```

官方文档链接

Flutter 开发中的应用案例

网络请求和数据解析是几乎所有真实世界 Flutter 应用的核心功能。无论是获取新闻、商品列表、用户数据,还是提交表单,都离不开与后端服务的交互。

案例:构建一个简单的商品列表应用,从 API 获取数据并显示

我们将创建一个应用,从一个模拟的 REST API 获取商品列表,并使用 http 包进行网络请求,手动解析 JSON 数据,然后显示在列表中。

步骤 1: 创建商品模型

dart
复制代码
// lib/models/product.dart
class Product {
  final int id;
  final String name;
  final double price;
  final String imageUrl;

  Product({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
  });

  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: json["id"] as int,
      name: json["name"] as String,
      price: (json["price"] as num).toDouble(), // JSON 可能返回 int 或 double
      imageUrl: json["imageUrl"] as String,
    );
  }
}

步骤 2: 创建商品服务

dart
复制代码
// lib/services/product_service.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/product.dart';

class ProductService {
  static const String _baseUrl = 'https://my-json-server.typicode.com/typicode/demo/posts'; // 模拟 API

  Future<List<Product>> fetchProducts() async {
    final response = await http.get(Uri.parse(_baseUrl));

    if (response.statusCode == 200) {
      // 模拟的 API 返回的是 posts,我们将其视为 products
      final List<dynamic> jsonList = jsonDecode(response.body);
      return jsonList.map((json) => Product.fromJson(json)).toList();
    } else {
      throw Exception('Failed to load products');
    }
  }
}

步骤 3: 在 UI 中使用服务

dart
复制代码
import 'package:flutter/material.dart';
import 'package:my_app/models/product.dart'; // 假设你的文件路径
import 'package:my_app/services/product_service.dart'; // 假设你的文件路径

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

  @override
  State<ProductListScreen> createState() => _ProductListScreenState();
}

class _ProductListScreenState extends State<ProductListScreen> {
  late Future<List<Product>> _productsFuture;

  @override
  void initState() {
    super.initState();
    _productsFuture = ProductService().fetchProducts();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('商品列表'),
      ),
      body: FutureBuilder<List<Product>>(
        future: _productsFuture,
        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) {
            final products = snapshot.data!;
            return ListView.builder(
              itemCount: products.length,
              itemBuilder: (context, index) {
                final product = products[index];
                return Card(
                  margin: const EdgeInsets.all(8.0),
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Row(
                      children: <Widget>[
                        // 模拟图片,实际应用中会加载网络图片
                        // Image.network(product.imageUrl, width: 80, height: 80, fit: BoxFit.cover),
                        Container(
                          width: 80,
                          height: 80,
                          color: Colors.grey[300],
                          child: Center(child: Text('图片\n${product.id}')),
                        ),
                        const SizedBox(width: 10),
                        Expanded(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: <Widget>[
                              Text(
                                product.name,
                                style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                              ),
                              Text('价格: \$${product.price.toStringAsFixed(2)}'),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              },
            );
          } else {
            return const Center(child: Text('没有商品数据。'));
          }
        },
      ),
    );
  }
}

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

案例分析:

  • Product 模型:定义了从 API 获取的商品数据结构。fromJson 工厂构造函数负责将 JSON Map 转换为 Product 对象。
  • ProductService:封装了网络请求逻辑。fetchProducts 方法使用 http.get 发送请求,并手动 jsonDecode 响应体,然后将 JSON 列表映射为 Product 对象列表。
  • FutureBuilder:在 UI 层,FutureBuilder 是处理异步操作结果的强大 Widget。它监听一个 Future(这里是 _productsFuture),并根据 Future 的状态(waitinghasErrorhasData)来构建不同的 UI。
    • ConnectionState.waiting:显示加载指示器。
    • snapshot.hasError:显示错误信息。
    • snapshot.hasData:显示商品列表。ListView.builder 用于高效地构建可滚动的列表。

这个案例展示了 Flutter 应用中网络请求和数据解析的完整流程。从定义数据模型、封装网络服务,到在 UI 中使用 FutureBuilder 处理异步数据,这些都是构建数据驱动型 Flutter 应用的基础。