flutter学习第 12 节:网络请求与 JSON 解析

在现代移动应用开发中,几乎所有应用都需要与后端服务器进行数据交互,获取远程数据并展示给用户。Flutter 提供了多种方式来处理网络请求和数据解析,本节课将详细介绍如何在 Flutter 中进行网络请求、处理响应数据以及解析 JSON 格式的数据。

一、HTTP 库选择:dio 安装与基本使用

Flutter 官方提供了 http 包用于网络请求,但在实际开发中,dio 库因其更强大的功能和更简洁的 API 而被广泛使用。dio 是一个强大的 Dart HTTP 客户端,支持拦截器、FormData、请求取消、超时设置等高级功能。

1. 安装 dio

pubspec.yaml 文件中添加依赖:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  dio: ^5.9.0  # 使用最新版本

运行 flutter pub get 安装依赖。

2. dio 基本使用

首先导入 dio 包:

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

创建 dio 实例:

dart 复制代码
// 创建默认实例
Dio dio = Dio();

// 也可以通过 BaseOptions 配置实例
BaseOptions options = BaseOptions(
  baseUrl: 'https://api.example.com',
  connectTimeout: const Duration(milliseconds: 5000),
  receiveTimeout: const Duration(milliseconds: 3000),
  headers: {
    'Content-Type': 'application/json',
  },
);

Dio dio = Dio(options);

二、发起 GET/POST 请求与参数处理

1. 发起 GET 请求

GET 请求通常用于从服务器获取数据:

dart 复制代码
// 简单的 GET 请求
Future<void> fetchData() async {
  try {
    Response response = await dio.get('/users');
    print('Response data: ${response.data}');
    print('Status code: ${response.statusCode}');
  } catch (e) {
    print('Error: $e');
  }
}

// 带查询参数的 GET 请求
Future<void> fetchUserData() async {
  try {
    // 方式一:直接在 URL 中添加参数
    Response response1 = await dio.get('/users?userId=123&name=John');
    
    // 方式二:使用 queryParameters
    Response response2 = await dio.get(
      '/users',
      queryParameters: {
        'userId': 123,
        'name': 'John',
      },
    );
    
    print('Response data: ${response2.data}');
  } catch (e) {
    print('Error: $e');
  }
}

2. 发起 POST 请求

POST 请求通常用于向服务器提交数据:

dart 复制代码
// 提交 JSON 数据
Future<void> submitData() async {
  try {
    Response response = await dio.post(
      '/users',
      data: {'name': 'John Doe', 'email': 'john@example.com', 'age': 30},
    );
    print('Response data: ${response.data}');
  } catch (e) {
    print('Error: $e');
  }
}

// 提交 FormData(表单数据)
Future<void> uploadForm() async {
  try {
    FormData formData = FormData.fromMap({
      'name': 'John Doe',
      'avatar': await MultipartFile.fromFile(
        '/path/to/avatar.jpg',
        filename: 'avatar.jpg',
      ),
      'hobbies': ['reading', 'sports'],
    });

    Response response = await dio.post('/user/profile', data: formData);
    print('Response data: ${response.data}');
  } catch (e) {
    print('Error: $e');
  }
}

3. 自定义请求头

可以为单个请求设置自定义请求头:

dart 复制代码
Future<void> fetchWithHeaders() async {
  try {
    Response response = await dio.get(
      '/protected/data',
      options: Options(
        headers: {
          'Authorization': 'Bearer your_token_here',
          'Custom-Header': 'custom_value',
        },
      ),
    );
    print('Response data: ${response.data}');
  } catch (e) {
    print('Error: $e');
  }
}

4. 处理请求超时

可以为单个请求设置超时时间:

dart 复制代码
Future<void> fetchWithTimeout() async {
  try {
    Response response = await dio.get(
      '/slow/endpoint',
      options: Options(
        sendTimeout: const Duration(seconds: 2),
        receiveTimeout: const Duration(seconds: 5),
      ),
    );
    print('Response data: ${response.data}');
  } on DioException catch (e) {
    if (e.type == DioExceptionType.connectionTimeout) {
      print('Connection timeout');
    } else if (e.type == DioExceptionType.receiveTimeout) {
      print('Receive timeout');
    } else {
      print('Other error: $e');
    }
  }
}

三、JSON 数据解析

服务器返回的数据通常是 JSON 格式,Flutter 提供了多种方式来解析 JSON 数据。

1. 手动解析 JSON

Dart 内置了 dart:convert 库,可以手动解析 JSON 数据:

dart 复制代码
import 'dart:convert';

// 假设服务器返回的 JSON 数据如下:
// {
//   "id": 1,
//   "name": "John Doe",
//   "email": "john@example.com",
//   "age": 30,
//   "hobbies": ["reading", "sports"]
// }

// 创建模型类
class User {
  final int id;
  final String name;
  final String email;
  final int age;
  final List<String> hobbies;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.age,
    required this.hobbies,
  });

  // 从 JSON 映射创建 User 实例
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
      age: json['age'],
      hobbies: List<String>.from(json['hobbies']),
    );
  }

  // 转换为 JSON 映射
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'age': age,
      'hobbies': hobbies,
    };
  }
}

// 解析 JSON 数据
Future<void> parseUserJson() async {
  try {
    Response response = await dio.get('/users/1');

    // 将 JSON 字符串转换为 Map
    Map<String, dynamic> jsonData = response.data;

    // 转换为 User 对象
    User user = User.fromJson(jsonData);

    print('User name: ${user.name}');
    print('User email: ${user.email}');
  } catch (e) {
    print('Error: $e');
  }
}

// 解析 JSON 数组
Future<void> parseUsersJson() async {
  try {
    Response response = await dio.get('/users');

    // 将 JSON 数组转换为 List
    List<dynamic> jsonList = response.data;

    // 转换为 User 对象列表
    List<User> users = jsonList.map((json) => User.fromJson(json)).toList();

    print('Number of users: ${users.length}');
    print('First user: ${users[0].name}');
  } catch (e) {
    print('Error: $e');
  }
}

手动解析的优点是简单直接,不需要额外依赖,但缺点是当 JSON 结构复杂或字段较多时,编写解析代码繁琐且容易出错。

2. 使用 json_serializable 自动生成代码

json_serializable 是一个自动化的源代码生成器,可以为 JSON 序列化和反序列化生成代码,减少手动编写解析代码的工作量。

安装依赖

pubspec.yaml 中添加依赖:

yaml 复制代码
dependencies:
  # ... 其他依赖
  json_annotation: ^4.8.1  # 注解包

dev_dependencies:
  # ... 其他开发依赖
  build_runner: ^2.4.4     # 构建工具
  json_serializable: ^6.7.1 # 代码生成器

运行 flutter pub get 安装依赖。

创建模型类

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

// 生成的代码将在 user.g.dart 文件中
part 'user.g.dart';

@JsonSerializable()
class User {
  final int id;
  final String name;
  
  // 使用 @JsonKey 注解指定 JSON 字段名与类属性名不同的情况
  @JsonKey(name: 'email_address')
  final String email;
  
  final int age;
  
  // 忽略该字段,不参与序列化和反序列化
  @JsonKey(ignore: true)
  final String? token;
  
  final List<String> hobbies;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.age,
    this.token,
    required this.hobbies,
  });

  // 从 JSON 映射创建 User 实例
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  // 转换为 JSON 映射
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

生成代码

在项目根目录运行以下命令生成序列化代码:

bash 复制代码
flutter pub run build_runner build

如果需要在开发过程中自动生成代码(当模型类变化时),可以使用 watch 模式:

bash 复制代码
flutter pub run build_runner watch

运行成功后,会生成 user.g.dart 文件,包含自动生成的序列化和反序列化代码。

使用自动生成的代码

dart 复制代码
Future<void> useGeneratedCode() async {
  try {
    Response response = await dio.get('/users/1');
    
    // 使用自动生成的 fromJson 方法解析
    User user = User.fromJson(response.data);
    
    print('User name: ${user.name}');
    print('User email: ${user.email}');
    
    // 序列化示例
    Map<String, dynamic> userJson = user.toJson();
    print('Serialized user: $userJson');
  } catch (e) {
    print('Error: $e');
  }
}

json_serializable 的优点是减少手动编写解析代码的工作量,提高代码的可靠性和可维护性,特别适合处理复杂的 JSON 结构。


四、网络状态处理

在实际应用中,网络请求通常有几种状态:加载中、成功、失败。我们需要根据不同的状态展示不同的 UI。

1. 创建网络状态管理类

dart 复制代码
enum NetworkStatus { initial, loading, success, error }

class NetworkResult<T> {
  final NetworkStatus status;
  final T? data;
  final String? errorMessage;

  NetworkResult.initial()
      : status = NetworkStatus.initial,
        data = null,
        errorMessage = null;

  NetworkResult.loading()
      : status = NetworkStatus.loading,
        data = null,
        errorMessage = null;

  NetworkResult.success(this.data)
      : status = NetworkStatus.success,
        errorMessage = null;

  NetworkResult.error(this.errorMessage)
      : status = NetworkStatus.error,
        data = null;
}

2. 基于状态展示不同 UI

dart 复制代码
class DataScreen extends StatefulWidget {
  const DataScreen({super.key});

  @override
  State<DataScreen> createState() => _DataScreenState();
}

class _DataScreenState extends State<DataScreen> {
  final Dio _dio = Dio();
  NetworkResult<List<User>> _result = NetworkResult.initial();

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

  Future<void> fetchUsers() async {
    setState(() {
      _result = NetworkResult.loading();
    });

    try {
      Response response = await _dio.get('https://api.example.com/users');
      List<User> users = (response.data as List)
          .map((json) => User.fromJson(json))
          .toList();
      
      setState(() {
        _result = NetworkResult.success(users);
      });
    } catch (e) {
      setState(() {
        _result = NetworkResult.error(e.toString());
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User List')),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    switch (_result.status) {
      case NetworkStatus.initial:
        return const Center(child: Text('Tap to load data'));
      case NetworkStatus.loading:
        return const Center(child: CircularProgressIndicator());
      case NetworkStatus.success:
        return _buildUserList(_result.data!);
      case NetworkStatus.error:
        return Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Error: ${_result.errorMessage}'),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: fetchUsers,
                child: const Text('Retry'),
              ),
            ],
          ),
        );
    }
  }

  Widget _buildUserList(List<User> users) {
    return ListView.builder(
      itemCount: users.length,
      itemBuilder: (context, index) {
        User user = users[index];
        return ListTile(
          title: Text(user.name),
          subtitle: Text(user.email),
          trailing: Text('Age: ${user.age}'),
        );
      },
    );
  }
}

五、dio 拦截器

dio 提供了拦截器功能,可以在请求发送前或响应返回后进行一些统一处理,如添加认证 token、处理错误等。

1. 请求拦截器

dart 复制代码
// 添加请求拦截器
dio.interceptors.add(
  InterceptorsWrapper(
    onRequest: (options, handler) {
      // 在请求发送前做一些处理
      print('Request: ${options.method} ${options.uri}');
      
      // 添加认证 token
      options.headers['Authorization'] = 'Bearer your_token_here';
      
      // 继续发送请求
      return handler.next(options);
      
      // 如果想终止请求,可以调用 handler.reject()
      // return handler.reject(DioException(requestOptions: options, type: DioExceptionType.cancel));
    },
  ),
);

2. 响应拦截器

dart 复制代码
// 添加响应拦截器
dio.interceptors.add(
  InterceptorsWrapper(
    onResponse: (response, handler) {
      // 在响应返回后做一些处理
      print('Response: ${response.statusCode} ${response.data}');
      
      // 继续处理响应
      return handler.next(response);
    },
  ),
);

3. 错误拦截器

dart 复制代码
// 添加错误拦截器
dio.interceptors.add(
  InterceptorsWrapper(
    onError: (DioException e, handler) {
      // 处理错误
      print('Error: ${e.message}');
      
      // 统一处理 401 未授权错误
      if (e.response?.statusCode == 401) {
        // 可以在这里跳转到登录页面
        print('Unauthorized, redirecting to login');
      }
      
      // 继续处理错误
      return handler.next(e);
      
      // 如果想掩盖错误,可以返回一个成功的响应
      // return handler.resolve(Response(requestOptions: e.requestOptions, data: {}));
    },
  ),
);

4. 日志拦截器

dio 提供了一个内置的日志拦截器,方便调试:

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

dio.interceptors.add(LogInterceptor(
  request: true,  // 打印请求信息
  requestHeader: true,  // 打印请求头
  requestBody: true,  // 打印请求体
  responseHeader: true,  // 打印响应头
  responseBody: true,  // 打印响应体
  error: true,  // 打印错误信息
  logPrint: (object) {
    print('Dio Log: $object');
  },
));

六、实例:请求开源 API 展示新闻列表

下面我们将实现一个完整的示例,使用公开的新闻 API 获取新闻列表并展示。

1. 创建新闻模型类

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

part 'news.g.dart';

@JsonSerializable()
class NewsArticle {
  @JsonKey(name: 'source')
  final NewsSource source;
  
  @JsonKey(name: 'author')
  final String? author;
  
  @JsonKey(name: 'title')
  final String title;
  
  @JsonKey(name: 'description')
  final String? description;
  
  @JsonKey(name: 'url')
  final String url;
  
  @JsonKey(name: 'urlToImage')
  final String? urlToImage;
  
  @JsonKey(name: 'publishedAt')
  final String publishedAt;
  
  @JsonKey(name: 'content')
  final String? content;

  NewsArticle({
    required this.source,
    this.author,
    required this.title,
    this.description,
    required this.url,
    this.urlToImage,
    required this.publishedAt,
    this.content,
  });

  factory NewsArticle.fromJson(Map<String, dynamic> json) =>
      _$NewsArticleFromJson(json);

  Map<String, dynamic> toJson() => _$NewsArticleToJson(this);
}

@JsonSerializable()
class NewsSource {
  @JsonKey(name: 'id')
  final String? id;
  
  @JsonKey(name: 'name')
  final String name;

  NewsSource({
    this.id,
    required this.name,
  });

  factory NewsSource.fromJson(Map<String, dynamic> json) =>
      _$NewsSourceFromJson(json);

  Map<String, dynamic> toJson() => _$NewsSourceToJson(this);
}

@JsonSerializable()
class NewsResponse {
  @JsonKey(name: 'status')
  final String status;
  
  @JsonKey(name: 'totalResults')
  final int totalResults;
  
  @JsonKey(name: 'articles')
  final List<NewsArticle> articles;

  NewsResponse({
    required this.status,
    required this.totalResults,
    required this.articles,
  });

  factory NewsResponse.fromJson(Map<String, dynamic> json) =>
      _$NewsResponseFromJson(json);

  Map<String, dynamic> toJson() => _$NewsResponseToJson(this);
}

运行代码生成命令:

bash 复制代码
flutter pub run build_runner build

2. 创建新闻服务类

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

class NewsService {
  final Dio _dio = Dio();
  final String _apiKey = 'your_news_api_key'; // 替换为你的 API Key
  final String _baseUrl = 'https://newsapi.org/v2';

  NewsService() {
    // 配置 dio
    _dio.options.baseUrl = _baseUrl;
    _dio.options.connectTimeout = const Duration(seconds: 5);
    _dio.options.receiveTimeout = const Duration(seconds: 3);

    // 添加日志拦截器
    _dio.interceptors.add(LogInterceptor(responseBody: true));
  }

  // 获取头条新闻
  Future<NewsResponse> getTopHeadlines({String country = 'us'}) async {
    try {
      Response response = await _dio.get(
        '/top-headlines',
        queryParameters: {
          'country': country,
          'apiKey': _apiKey,
        },
      );
      return NewsResponse.fromJson(response.data);
    } on DioException catch (e) {
      print('News API error: ${e.message}');
      throw Exception('Failed to fetch news: ${e.message}');
    }
  }

  // 搜索新闻
  Future<NewsResponse> searchNews(String query) async {
    try {
      Response response = await _dio.get(
        '/everything',
        queryParameters: {
          'q': query,
          'apiKey': _apiKey,
        },
      );
      return NewsResponse.fromJson(response.data);
    } on DioException catch (e) {
      print('News API error: ${e.message}');
      throw Exception('Failed to search news: ${e.message}');
    }
  }
}

注意:需要在 News API 网站注册获取 API Key。

3. 实现新闻列表页面

dart 复制代码
class NewsListScreen extends StatefulWidget {
  const NewsListScreen({super.key});

  @override
  State<NewsListScreen> createState() => _NewsListScreenState();
}

class _NewsListScreenState extends State<NewsListScreen> {
  final NewsService _newsService = NewsService();
  NetworkResult<List<NewsArticle>> _newsResult = NetworkResult.initial();
  final TextEditingController _searchController = TextEditingController();

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

  Future<void> _fetchTopHeadlines() async {
    setState(() {
      _newsResult = NetworkResult.loading();
    });

    try {
      NewsResponse response = await _newsService.getTopHeadlines();
      setState(() {
        _newsResult = NetworkResult.success(response.articles);
      });
    } catch (e) {
      setState(() {
        _newsResult = NetworkResult.error(e.toString());
      });
    }
  }

  Future<void> _searchNews() async {
    String query = _searchController.text.trim();
    if (query.isEmpty) return;

    setState(() {
      _newsResult = NetworkResult.loading();
    });

    try {
      NewsResponse response = await _newsService.searchNews(query);
      setState(() {
        _newsResult = NetworkResult.success(response.articles);
      });
    } catch (e) {
      setState(() {
        _newsResult = NetworkResult.error(e.toString());
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Latest News'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              controller: _searchController,
              decoration: InputDecoration(
                hintText: 'Search news...',
                suffixIcon: IconButton(
                  icon: const Icon(Icons.search),
                  onPressed: _searchNews,
                ),
                border: const OutlineInputBorder(),
              ),
              onSubmitted: (value) => _searchNews(),
            ),
          ),
          Expanded(
            child: _buildNewsContent(),
          ),
        ],
      ),
    );
  }

  Widget _buildNewsContent() {
    switch (_newsResult.status) {
      case NetworkStatus.initial:
        return const Center(child: Text('Loading news...'));
      case NetworkStatus.loading:
        return const Center(child: CircularProgressIndicator());
      case NetworkStatus.success:
        return _buildNewsList(_newsResult.data!);
      case NetworkStatus.error:
        return Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Error: ${_newsResult.errorMessage}'),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: _fetchTopHeadlines,
                child: const Text('Retry'),
              ),
            ],
          ),
        );
    }
  }

  Widget _buildNewsList(List<NewsArticle> articles) {
    if (articles.isEmpty) {
      return const Center(child: Text('No news found'));
    }

    return ListView.builder(
      itemCount: articles.length,
      itemBuilder: (context, index) {
        NewsArticle article = articles[index];
        return Card(
          margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
          child: Column(
            children: [
              if (article.urlToImage != null)
                Image.network(
                  article.urlToImage!,
                  height: 180,
                  width: double.infinity,
                  fit: BoxFit.cover,
                  errorBuilder: (context, error, stackTrace) {
                    return Container(
                      height: 180,
                      color: Colors.grey[200],
                      child: const Center(child: Icon(Icons.image_not_supported)),
                    );
                  },
                ),
              Padding(
                padding: const EdgeInsets.all(12.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      article.source.name,
                      style: TextStyle(
                        color: Colors.grey[600],
                        fontSize: 12,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      article.title,
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 8),
                    if (article.description != null)
                      Text(
                        article.description!,
                        style: const TextStyle(fontSize: 14),
                        maxLines: 3,
                        overflow: TextOverflow.ellipsis,
                      ),
                    const SizedBox(height: 8),
                    Text(
                      _formatDate(article.publishedAt),
                      style: TextStyle(
                        color: Colors.grey[600],
                        fontSize: 12,
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }

  String _formatDate(String dateString) {
    DateTime date = DateTime.parse(dateString);
    return DateFormat.yMMMd().add_jm().format(date);
  }

注意:需要添加 intl 依赖来格式化日期,在 pubspec.yaml 中添加 intl: ^0.18.1 并运行 flutter pub get

4. 配置网络权限

对于 Android,需要在 android/app/src/main/AndroidManifest.xml 中添加网络权限:

xml 复制代码
<uses-permission android:name="android.permission.INTERNET" />

对于 iOS,需要在 ios/Runner/Info.plist 中添加:

xml 复制代码
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>

七、网络请求最佳实践

  1. 封装网络层:将网络请求逻辑封装在专门的服务类中,与 UI 层分离,提高代码复用性和可维护性。
  2. 统一错误处理:使用拦截器统一处理网络错误,如超时、无网络、认证失败等。
  3. 合理管理请求状态:清晰展示加载中、成功、失败等状态,提供良好的用户体验。
  4. 数据缓存:对于不经常变化的数据,可以实现本地缓存,减少网络请求,提高应用性能。
  5. 请求取消:在页面销毁时取消未完成的网络请求,避免内存泄漏和不必要的资源消耗。
  6. 图片处理 :使用 cached_network_image 等库处理网络图片,实现缓存和占位图功能。
  7. 避免在 UI 线程处理复杂任务:确保网络请求在异步线程执行,避免阻塞 UI。
  8. 添加日志:在开发环境添加详细的网络日志,方便调试;在生产环境关闭或简化日志。
相关推荐
DU_YULIN13 分钟前
Camera HAL ThreadManager概述
android·camera hal
淡淡的香烟1 小时前
Android中的MQTT通信原理解析
android·物联网
淡淡的香烟1 小时前
Android Camera开发详解
android·音视频
mmsx2 小时前
基于 Android 的校园信息管理系统源码
android·java·okhttp
xcyxiner3 小时前
flutter wsl2安装记录(使用宿主机虚拟机)
前端·flutter
YF02113 小时前
如何进入Android工程模式
android
Kevin Coding4 小时前
JsonConvert:适用于 Android、鸿蒙与 Flutter 的 JSON 转 Model 插件
android·flutter·harmonyos
2601_965798474 小时前
Build a Fast, High-Ranking Restaurant Website with Rolanda Theme
开发语言·ios·swift
极客互动API5 小时前
企业微信 iPad 协议消息接口开发:文本 / 图片 / 群发消息的统一封装
人工智能·ios·微信·机器人·企业微信·ipad
xcLeigh5 小时前
Go入门:基本数据类型全览与选择指南
android·服务器·golang·教程·go热门