《Flutter全栈开发实战指南:从零到高级》- 14 -网络请求与数据解析

网络请求与数据解析

在移动开发中需要与云端服务器进行频繁的数据交互,本节内容讲带你详细了解网络请求与数据解析,让你的应用真正地"活"起来。

一、 为什么网络层如此重要?

举个例子:你正在开发一个新闻App,那些滚动的时事新闻、视频等内容,不可能全部打包在App安装包里,它们需要从服务器实时获取。这个"获取"的过程,就是通过网络请求完成的。

简单来说,流程就是:App 问服务器要数据 -> 服务器返回数据 -> App 把数据展示出来

在Flutter中,常用的两个网络请求库:官方推荐的 http社区维护得 dio。我们将从两者入手,带你彻底玩转网络请求。

二、 http库dio库 如何选择?

选择哪个库,就像选择工具,没有绝对的好坏,只有合不合适。

1. http

http 库是Flutter团队维护的底层库,它:

  • 优点:官方维护,稳定可靠;API简单直接,学习成本低。
  • 缺点:功能相对基础,许多高级功能(如拦截器、文件上传/下载进度等)需要自己手动实现。

核心方法:

  • get(): 向指定URL发起GET请求,用于获取数据。
  • post(): 发起POST请求,用于提交数据。
  • put(), delete(), head() 等:对应其他HTTP方法。

2. dio

dio 是一个强大的第三方HTTP客户端,它:

  • 优点 :支持拦截器全局配置请求取消FormData文件上传/下载超时设置等。
  • 缺点 :相对于http库更重一些。

如何选择?

  • 新手入门 :可以从 http 开始,上手快。
  • 中大型项目 :强烈推荐 dio,它能帮你节省大量造轮子的时间。

本节内容主要以 dio 为例进行讲解,它更符合项目开发的实际情况。

三、 引入依赖

首先,在你的 pubspec.yaml 文件中声明依赖。

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  dio: ^5.0.0 
  # 用于JSON序列化
  json_annotation: ^4.8.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.3.0
  json_serializable: ^6.5.0

执行 flutter pub get 安装依赖。

四、 http

虽然我们推荐使用dio库,但了解http库的基本用法仍是必要的。

以获取一篇博客文章信息为例

dart 复制代码
import 'package:http/http.dart' as http; 
import 'dart:convert'; 

class HttpExample {
  
  static Future<void> fetchPost() async {
    try {
      // 1. 发起GET请求
      final response = await http.get(
        Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
      );

      // 2. 状态码200表示成功
      if (response.statusCode == 200) {
        // 3. 使用 dart:convert 解析返回的JSON字符串
        Map<String, dynamic> jsonData = json.decode(response.body);
        
        // 4. 从解析后的Map中取出数据
        String title = jsonData['title'];
        String body = jsonData['body'];
        print('标题: $title');
        print('内容: $body');
      } else {
        // 请求失败
        print('请求失败,状态码: ${response.statusCode}');
        print('响应体: ${response.body}');
      }
    } catch (e) {
      // 捕获异常
      print('请求发生异常: $e');
    }
  }
}

代码解读:

  1. async/await:网络请求是耗时操作,必须使用异步。await 会等待请求完成,而不会阻塞UI线程。
  2. Uri.parse:将字符串URL转换为Uri对象。
  3. response.statusCode:响应状态码,200系列表示成功
  4. json.decode():反序列化将JSON串转换为Dart中的 Map<String, dynamic>List

五、 dio

下面我们重点讲解下dio库:

1. Dio-发起请求

我们先创建一个Dio实例并进行全局配置。

dart 复制代码
import 'package:dio/dio.dart';

class DioManager {
  // 单例
  static final DioManager _instance = DioManager._internal();
  factory DioManager() => _instance;
  DioManager._internal() {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://jsonplaceholder.typicode.com', 
      connectTimeout: const Duration(seconds: 5), // 连接超时时间
      receiveTimeout: const Duration(seconds: 3), // 接收数据超时时间
      headers: {
        'Content-Type': 'application/json', 
      },
    ));
  }

  late final Dio _dio;
  Dio get dio => _dio;
}

// GET请求
void fetchPostWithDio() async {
  try {
    // baseUrl后面拼接路径
    Response response = await DioManager().dio.get('/posts/1');
    // dio会自动检查状态码,非200系列会抛异常,所以这里直接处理数据
    Map<String, dynamic> data = response.data; // 这里dio帮我们自动解析了JSON
    print('获取数据: ${data['title']}');
  } on DioException catch (e) {
    print('请求异常: $e');
    if (e.response != null) {
      // 错误状态码
      print('错误状态码: ${e.response?.statusCode}');
      print('错误信息: ${e.response?.data}');
    } else {
      // 抛异常
      print('异常: ${e.message}');
    }
  } catch (e) {
    // 未知异常
    print('未知异常: $e');
  }
}

Dio相比Http的优点:

  • 自动JSON解析response.data 直接就是Map或List,无需手动 json.decode,太方便了!
  • 配置清晰BaseOptions 全局配置一目了然。
  • 结构化异常DioException 包含了丰富的错误信息。

2. Dio-网络请求流程

为了让大家更直观地理解,我们用一个流程图来展示Dio处理请求的完整过程:

sequenceDiagram participant A as 客户端 participant I as 拦截器 participant D as Dio participant S as 服务端 A->>D: 发起请求 Note right of A: await dio.get('/users') D->>I: 请求拦截器 Note right of I: 添加Token、日志等 I->>D: 处理后的请求 D->>S: 发送网络请求 S-->>D: 返回响应 D->>I: 响应拦截器 Note right of I: 解析JSON、错误处理 I->>D: 处理后的响应 D-->>A: 返回最终结果

3. Dio-拦截器

拦截器允许我们在请求发送前和响应返回后,插入自定义逻辑,对所有经过的请求和响应进行检查和加工。

案例:自动添加认证Token

dart 复制代码
class AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // 请求发送前,为每个请求的Header加上Token
    const String token = 'your_auth_token_here';
    if (token.isNotEmpty) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // 响应成功处理
    print('请求成功: ${response.requestOptions.path}');
    handler.next(response);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    // 失败处理
    // 当Token过期时,自动跳转到登录页
    if (err.response?.statusCode == 401) {
      print('Token已过期,请重新登录!');
      // 这里可以跳转到登录页面
      // NavigationService.instance.navigateTo('/login');
    }
 
    handler.next(err);
  }
}

// 将拦截器添加到Dio实例中
void main() {
  final dio = DioManager().dio;
  dio.interceptors.add(AuthInterceptor());
  // 这里可以添加其他拦截器
  dio.interceptors.add(LogInterceptor(responseBody: true)); 
}

拦截器的添加顺序就是它们的执行顺序。onRequest 正序执行,onResponseonError 倒序执行。

六、 JSON序列化与反序列化

这是新手最容易踩坑的地方。直接从JSON转换成Dart对象(Model),能让我们的代码更安全、更易维护。

1. 为什么要序列化?

  • 类型安全 :直接操作Map,编译器不知道data['title']是String还是int,容易写错;
  • 代码效率 :使用点语法post.title访问属性,比post['title']更高效且有代码提示;
  • 可维护性:当接口字段变更时,你只需要修改一个Model类,而不是分散在各处的字符串key;;

2. 使用 json_serializable自动序列化

通过代码生成的方式,自动创建 fromJsontoJson 方法,一劳永逸。

步骤1:创建Model类并使用注解

dart 复制代码
// post.dart
import 'package:json_annotation/json_annotation.dart';

// 运行 `flutter pub run build_runner build` 后,会生成 post.g.dart 文件
part 'post.g.dart';

// 这个注解告诉生成器这个类需要生成序列化代码
@JsonSerializable()
class Post {
  // 使用@JsonKey可以自定义序列化行为
  // 例如,如果JSON字段名是`user_id`,而Dart字段是`userId`,可以这样映射:
  // @JsonKey(name: 'user_id')
  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);
}

步骤2:运行代码生成命令

在项目根目录下执行:

bash 复制代码
flutter pub run build_runner build

这个命令会扫描所有带有 @JsonSerializable() 注解的类,并生成对应的 .g.dart 文件(如 post.g.dart)。这个文件里包含了 _$PostFromJson_$PostToJson 的具体实现。

步骤3:自动生成

dart 复制代码
// 具体的请求方法中使用
void fetchPostModel() async {
  try {
    Response response = await DioManager().dio.get('/posts/1');
    // 将响应数据转换为Post对象
    Post post = Post.fromJson(response.data);
    print('文章标题: ${post.title}');
  } on DioException catch (e) {
    // ... 错误处理
  }
}

json_serializable 的优势:

  • 自动处理类型转换,避免手误;
  • 通过 @JsonKey 注解可以处理各种复杂场景;

七、 网络层在MVVM模式中的定位

实际项目中,我们不会直接在UI页面里写网络请求代码。让我们看看网络层在MVVM架构中是如何工作的:

graph LR A[View
视图层] -->|调用| B[ViewModel
视图模型] B -->|调用| C[Model
模型层] C -->|使用| D[Dio
网络层] D -->|返回JSON| C C -->|转换为Model| B B -->|更新状态| A

各个分层职责:

  • View:只关心数据的展示和用户交互;
  • ViewModel:持有业务状态,处理UI逻辑,不关心数据从哪里来;
  • Model:决定数据是从网络获取还是本地数据库读取,它调用网络层;
  • Dio:纯粹的网络请求执行者,负责API调用、错误初步处理等;

这样分层的好处是:最终目的是解耦,各司其职,修改网络层不会影响业务逻辑,代码结构清晰,同事方便单元测试。

八、 错误处理

一个好的应用,必须支持处理各种异常情况。

1. DioException

DioException 的类型 (type) 帮助我们准确判断错误根源。

dart 复制代码
void handleDioError(DioException e) {
  switch (e.type) {
    case DioExceptionType.connectionTimeout:
    case DioExceptionType.sendTimeout:
    case DioExceptionType.receiveTimeout:
      print('超时错误,请检查网络连接是否稳定。');
      break;
    case DioExceptionType.badCertificate:
      print('证书错误。');
      break;
    case DioExceptionType.badResponse:
      // 服务器返回了错误状态码
      print('服务器错误: ${e.response?.statusCode}');
      // 可以根据不同状态码做不同处理
      if (e.response?.statusCode == 404) {
        print('请求的资源不存在(404)');
      } else if (e.response?.statusCode == 500) {
        print('服务器内部错误(500)');
      } else if (e.response?.statusCode == 401) {
        print('未授权,请重新登录(401)');
      }
      break;
    case DioExceptionType.cancel:
      print('请求被取消。');
      break;
    case DioExceptionType.connectionError:
      print('网络连接错误,请检查网络是否开启。');
      break;
    case DioExceptionType.unknown:
      print('未知错误: ${e.message}');
      break;
  }
}

2. 重试机制

对于因网络波动导致的失败,自动重试能大幅提升用户体验。

dart 复制代码
class RetryInterceptor extends Interceptor {
  final Dio _dio;

  RetryInterceptor(this._dio);

  @override
  Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
    // 只对超时和网络连接错误进行重试
    if (err.type == DioExceptionType.connectionTimeout ||
        err.type == DioExceptionType.receiveTimeout ||
        err.type == DioExceptionType.connectionError) {
      
      final int retryCount = err.requestOptions.extra['retry_count'] ?? 0;
      const int maxRetryCount = 3;

      if (retryCount < maxRetryCount) {
        // 增加重试计数
        err.requestOptions.extra['retry_count'] = retryCount + 1;
        print('网络不稳定,正在尝试第${retryCount + 1}次重试...');

        // 等待一段时间后重试
        await Future.delayed(Duration(seconds: 1 * (retryCount + 1)));

        try {
          // 重新发送请求
          final Response response = await _dio.fetch(err.requestOptions);
          // 返回成功response
          handler.resolve(response);
          return;
        } catch (retryError) {
          // 如果失败继续传递错误
          handler.next(err);
          return;
        }
      }
    }
    // 如果不是指定错误或已达最大重试次数,则继续传递错误
    handler.next(err);
  }
}

九、 封装一个完整的网络请求库

到这已经把所有的网络请求知识学完了,下面我们用学到的知识封装一个通用的网络请求工具类。

dart 复制代码
// http_client.dart
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';

class HttpClient {
  static final HttpClient _instance = HttpClient._internal();
  factory HttpClient() => _instance;

  late final Dio _dio;

  HttpClient._internal() {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://api.yourserver.com/v1',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
      headers: {'Content-Type': 'application/json'},
    ));

    // 添加拦截器
    _dio.interceptors.add(LogInterceptor(
      requestBody: kDebugMode, 
      responseBody: kDebugMode,
    ));
    _dio.interceptors.add(AuthInterceptor());
    _dio.interceptors.add(RetryInterceptor(_dio));
  }

  // 封装GET请求
  Future<Response<T>> get<T>(
    String path, {
    Map<String, dynamic>? queryParameters,
    Map<String, dynamic>? headers,
  }) async {
    try {
      final options = Options(headers: headers);
      return await _dio.get<T>(
        path,
        queryParameters: queryParameters,
        options: options,
      );
    } on DioException {
      rethrow; 
    }
  }

  // 封装POST请求
  Future<Response<T>> post<T>(
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Map<String, dynamic>? headers,
  }) async {
    try {
      final options = Options(headers: headers);
      return await _dio.post<T>(
        path,
        data: data,
        queryParameters: queryParameters,
        options: options,
      );
    } on DioException {
      rethrow;
    }
  }

  // 获取列表数据
  Future<List<T>> getList<T>(
    String path, {
    Map<String, dynamic>? queryParameters,
    T Function(Map<String, dynamic>)? fromJson,
  }) async {
    final response = await get<List<dynamic>>(path, queryParameters: queryParameters);
    // 将List<dynamic>转换为List<T>
    if (fromJson != null) {
      return response.data!.map<T>((item) => fromJson(item as Map<String, dynamic>)).toList();
    }
    
    return response.data as List<T>;
  }

  // 获取单个对象
  Future<T> getItem<T>(
    String path, {
    Map<String, dynamic>? queryParameters,
    required T Function(Map<String, dynamic>) fromJson,
  }) async {
    final response = await get<Map<String, dynamic>>(path, queryParameters: queryParameters);
    return fromJson(response.data!);
  }
}

// 
class PostRepository {
  final HttpClient _client = HttpClient();

  Future<Post> getPost(int id) async {
    final response = await _client.getItem(
      '/posts/$id',
      fromJson: Post.fromJson, 
    );
    return response;
  }

  Future<List<Post>> getPosts() async {
    final response = await _client.getList(
      '/posts',
      fromJson: Post.fromJson,
    );
    return response;
  }

  Future<Post> createPost(Post post) async {
    // Model转JSON
    final response = await _client.post(
      '/posts',
      data: post.toJson(),
    );
    return Post.fromJson(response.data);
  }
}

总结

又到了写总结诶的时候了,让我们用一张表格来回顾所有知识点:

知识点 核心 用途
库选择 http 轻量,dio 强大 中大型项目首选 dio
异步编程 使用 async/await 处理耗时操作 不能阻塞UI线程
JSON序列化 自动生成 推荐 json_serializable
错误处理 区分网络异常和服务器错误 精确捕获 DioException 并分类处理
拦截器 统一处理请求/响应 用于添加Token、日志、重试逻辑
架构分层 MVVM 分离解耦
请求封装 统一封装GET/POST等基础方法 提供 getItem, getList 等语义化方法

网络请求在实际项目中直观重要,没有网络就没有数据,掌握好本章内容,你就能为你Flutter应用注入源源不断的活力。让我们下期见!

相关推荐
RollingPin2 小时前
iOS 内存管理之 autoreleasePool
ios·内存管理·runtime·autoreleasepool
程序员老刘2 小时前
华为小米都在布局的多屏协同,其实Android早就有了!只是你不知道...
android·flutter
清凉夏日2 小时前
Flutter 国际化完整指南
前端·flutter
猫林老师3 小时前
Flutter for HarmonyOS开发指南(九):测试、调试与质量保障体系
flutter·wpf·harmonyos
猫林老师3 小时前
Flutter for HarmonyOS开发指南(五):性能调优与性能分析全攻略
flutter·华为·harmonyos
2501_915921435 小时前
查看iOS App实时日志的正确方式,多工具协同打造高效调试与问题定位体系(2025最新指南)
android·ios·小程序·https·uni-app·iphone·webview
ajassi20007 小时前
开源 Objective-C IOS 应用开发(四)Xcode工程文件结构
ios·开源·objective-c
G佳伟8 小时前
如何解决解决,微信小程序ios无法长按复制问题<text>设置 selectable=“true“不起作用
ios·微信小程序·小程序
Nick56838 小时前
Apple Pay 与 Google Pay 开发与结算全流程文档
ios·安卓·android-studio