MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter http插件的封装:简化网络请求代码

2021-11-272.8k 阅读

一、Flutter 中的网络请求基础

在 Flutter 应用开发中,网络请求是非常常见的操作。无论是获取数据展示给用户,还是将用户数据提交到服务器,都离不开网络请求。Flutter 官方提供了 http 插件来处理 HTTP 请求,它是一个强大且灵活的工具,能满足大多数网络请求场景。

在开始封装之前,我们先来了解一下如何直接使用 http 插件进行简单的网络请求。

  1. 添加依赖 首先,在 pubspec.yaml 文件中添加 http 依赖:
dependencies:
  http: ^0.13.4

然后运行 flutter pub get 来获取依赖。

  1. GET 请求示例
import 'package:http/http.dart' as http;
import 'dart:convert';

Future<void> fetchData() async {
  final response = await http.get(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    final jsonData = jsonDecode(response.body);
    print(jsonData);
  } else {
    print('Request failed with status: ${response.statusCode}.');
  }
}

在这个例子中,我们使用 http.get 方法发送一个 GET 请求到指定的 URL。如果响应状态码为 200,说明请求成功,我们将响应体解析为 JSON 格式数据并打印出来;否则,打印错误信息。

  1. POST 请求示例
import 'package:http/http.dart' as http;
import 'dart:convert';

Future<void> postData() async {
  final data = {
    'key1': 'value1',
    'key2': 'value2'
  };
  final response = await http.post(
    Uri.parse('https://example.com/api/submit'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(data),
  );
  if (response.statusCode == 200) {
    print('Data posted successfully.');
  } else {
    print('Request failed with status: ${response.statusCode}.');
  }
}

这里我们使用 http.post 方法发送 POST 请求。设置了请求头 Content - Typeapplication/json,并将数据转换为 JSON 格式作为请求体发送。同样,根据响应状态码判断请求是否成功。

二、为什么要封装 http 插件

虽然直接使用 http 插件能实现网络请求功能,但在实际项目中,随着业务的增长和复杂度的提高,会发现存在以下问题:

  1. 代码重复 在不同页面或模块中进行网络请求时,很多代码是重复的,比如设置请求头、处理响应状态码等。例如,每个请求都可能需要设置 Content - Typeapplication/json,以及对常见的状态码(如 200、404、500 等)进行类似的处理。

  2. 可维护性差 如果项目后期需要修改网络请求的一些通用逻辑,如添加统一的认证头、修改请求超时时间等,需要在所有使用网络请求的地方逐一修改,这不仅工作量大,还容易出错。

  3. 代码可读性降低 直接使用 http 插件的代码可能会在业务逻辑中显得比较突兀,使代码的整体可读性变差。例如,在一个复杂的页面逻辑中,混杂着网络请求的具体实现代码,会让开发者难以快速理解业务流程。

通过封装 http 插件,可以有效地解决这些问题。封装后的代码更加简洁、易于维护和复用,提高整个项目的开发效率和代码质量。

三、Flutter http 插件封装步骤

  1. 创建网络请求基类 首先,我们创建一个基类来处理一些通用的网络请求逻辑。
import 'package:http/http.dart' as http;
import 'dart:convert';

class BaseApiService {
  final String baseUrl;

  BaseApiService({required this.baseUrl});

  Future<dynamic> get(String endpoint) async {
    final response = await http.get(Uri.parse('$baseUrl$endpoint'));
    return _handleResponse(response);
  }

  Future<dynamic> post(String endpoint, Map<String, dynamic> data) async {
    final response = await http.post(
      Uri.parse('$baseUrl$endpoint'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(data),
    );
    return _handleResponse(response);
  }

  dynamic _handleResponse(http.Response response) {
    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else if (response.statusCode == 404) {
      throw Exception('Not Found');
    } else if (response.statusCode == 500) {
      throw Exception('Internal Server Error');
    } else {
      throw Exception('Request failed with status: ${response.statusCode}');
    }
  }
}

在这个基类 BaseApiService 中,我们定义了 baseUrl 作为基础 URL,所有请求的 URL 都将基于此。getpost 方法分别处理 GET 和 POST 请求,它们都调用了 _handleResponse 方法来统一处理响应。_handleResponse 方法根据不同的状态码进行相应的处理,成功时解析 JSON 数据,错误时抛出异常。

  1. 创建具体的 API 服务类 接下来,我们创建具体的 API 服务类,继承自 BaseApiService,用于处理特定业务的网络请求。
class UserApiService extends BaseApiService {
  UserApiService() : super(baseUrl: 'https://example.com/api/');

  Future<dynamic> fetchUser() async {
    return get('user');
  }

  Future<dynamic> createUser(Map<String, dynamic> data) async {
    return post('user', data);
  }
}

UserApiService 类中,我们设置了特定的 baseUrlhttps://example.com/api/。然后定义了 fetchUsercreateUser 方法,分别用于获取用户信息和创建新用户。这些方法内部调用了基类的 getpost 方法,使得代码更加简洁和清晰。

  1. 在项目中使用封装后的 API 服务 在 Flutter 项目的页面或其他逻辑中,我们可以这样使用封装后的 API 服务:
import 'package:flutter/material.dart';
import './UserApiService.dart';

class UserPage extends StatefulWidget {
  @override
  _UserPageState createState() => _UserPageState();
}

class _UserPageState extends State<UserPage> {
  UserApiService userApiService = UserApiService();
  dynamic userData;

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

  Future<void> _fetchUser() async {
    try {
      userData = await userApiService.fetchUser();
      setState(() {});
    } catch (e) {
      print('Error: $e');
    }
  }

  Future<void> _createUser() async {
    final newUserData = {
      'name': 'John Doe',
      'email': 'johndoe@example.com'
    };
    try {
      await userApiService.createUser(newUserData);
      print('User created successfully.');
    } catch (e) {
      print('Error: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('User Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (userData != null)
              Text('User Name: ${userData['name']}'),
            ElevatedButton(
              onPressed: _createUser,
              child: Text('Create User'),
            ),
          ],
        ),
      ),
    );
  }
}

在这个 UserPage 页面中,我们创建了 UserApiService 的实例,并在 initState 中调用 fetchUser 方法获取用户信息。createUser 方法用于创建新用户,当点击按钮时会触发该方法。通过这种方式,我们的业务逻辑代码与网络请求代码分离,使代码更易于理解和维护。

四、封装的高级特性扩展

  1. 添加请求拦截器 有时候我们需要在请求发送之前对请求进行一些处理,比如添加认证头、修改请求参数等。这时候可以通过添加请求拦截器来实现。
import 'package:http/http.dart' as http;
import 'dart:convert';

class BaseApiService {
  final String baseUrl;
  List<Function(http.Request)> requestInterceptors = [];

  BaseApiService({required this.baseUrl});

  Future<dynamic> get(String endpoint) async {
    final request = http.Request('GET', Uri.parse('$baseUrl$endpoint'));
    _applyRequestInterceptors(request);
    final response = await http.Client().send(request);
    return _handleResponse(response);
  }

  Future<dynamic> post(String endpoint, Map<String, dynamic> data) async {
    final request = http.Request('POST', Uri.parse('$baseUrl$endpoint'));
    request.headers['Content-Type'] = 'application/json; charset=UTF-8';
    request.body = jsonEncode(data);
    _applyRequestInterceptors(request);
    final response = await http.Client().send(request);
    return _handleResponse(response);
  }

  void _applyRequestInterceptors(http.Request request) {
    for (final interceptor in requestInterceptors) {
      interceptor(request);
    }
  }

  dynamic _handleResponse(http.StreamedResponse response) async {
    final responseBody = await response.stream.bytesToString();
    if (response.statusCode == 200) {
      return jsonDecode(responseBody);
    } else if (response.statusCode == 404) {
      throw Exception('Not Found');
    } else if (response.statusCode == 500) {
      throw Exception('Internal Server Error');
    } else {
      throw Exception('Request failed with status: ${response.statusCode}');
    }
  }
}

在这个扩展后的 BaseApiService 中,我们添加了一个 requestInterceptors 列表,用于存储请求拦截器函数。在 getpost 方法中,通过 _applyRequestInterceptors 方法应用这些拦截器。例如,我们可以这样添加一个认证头的拦截器:

class UserApiService extends BaseApiService {
  UserApiService() : super(baseUrl: 'https://example.com/api/');

  @override
  void init() {
    requestInterceptors.add((request) {
      request.headers['Authorization'] = 'Bearer your_token';
      return request;
    });
  }
}

UserApiServiceinit 方法中,我们添加了一个拦截器,为每个请求添加了 Authorization 头。

  1. 添加响应拦截器 类似地,我们也可以在响应返回之后对响应进行处理,比如统一处理错误提示、解析特定格式的数据等。
import 'package:http/http.dart' as http;
import 'dart:convert';

class BaseApiService {
  final String baseUrl;
  List<Function(dynamic)> responseInterceptors = [];

  BaseApiService({required this.baseUrl});

  Future<dynamic> get(String endpoint) async {
    final response = await http.get(Uri.parse('$baseUrl$endpoint'));
    return _handleResponse(response);
  }

  Future<dynamic> post(String endpoint, Map<String, dynamic> data) async {
    final response = await http.post(
      Uri.parse('$baseUrl$endpoint'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(data),
    );
    return _handleResponse(response);
  }

  dynamic _handleResponse(http.Response response) {
    dynamic result;
    if (response.statusCode == 200) {
      result = jsonDecode(response.body);
    } else if (response.statusCode == 404) {
      throw Exception('Not Found');
    } else if (response.statusCode == 500) {
      throw Exception('Internal Server Error');
    } else {
      throw Exception('Request failed with status: ${response.statusCode}');
    }
    for (final interceptor in responseInterceptors) {
      result = interceptor(result);
    }
    return result;
  }
}

在这个扩展中,我们添加了 responseInterceptors 列表,并在 _handleResponse 方法中应用这些拦截器。例如,我们可以添加一个拦截器来统一处理错误提示:

class UserApiService extends BaseApiService {
  UserApiService() : super(baseUrl: 'https://example.com/api/');

  @override
  void init() {
    responseInterceptors.add((data) {
      if (data is Exception) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(data.toString())),
        );
        return null;
      }
      return data;
    });
  }
}

UserApiServiceinit 方法中,我们添加了一个响应拦截器。如果响应结果是一个异常,它会在屏幕上显示一个 SnackBar 提示错误信息,并返回 null

  1. 设置请求超时 为了避免网络请求长时间等待,我们可以设置请求超时时间。
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';

class BaseApiService {
  final String baseUrl;
  Duration timeout = const Duration(seconds: 10);

  BaseApiService({required this.baseUrl});

  Future<dynamic> get(String endpoint) async {
    try {
      final response = await http.get(
        Uri.parse('$baseUrl$endpoint'),
        headers: <String, String>{
          'Content-Type': 'application/json; charset=UTF-8',
        },
      ).timeout(timeout);
      return _handleResponse(response);
    } on TimeoutException {
      throw Exception('Request timed out');
    }
  }

  Future<dynamic> post(String endpoint, Map<String, dynamic> data) async {
    try {
      final response = await http.post(
        Uri.parse('$baseUrl$endpoint'),
        headers: <String, String>{
          'Content-Type': 'application/json; charset=UTF-8',
        },
        body: jsonEncode(data),
      ).timeout(timeout);
      return _handleResponse(response);
    } on TimeoutException {
      throw Exception('Request timed out');
    }
  }

  dynamic _handleResponse(http.Response response) {
    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else if (response.statusCode == 404) {
      throw Exception('Not Found');
    } else if (response.statusCode == 500) {
      throw Exception('Internal Server Error');
    } else {
      throw Exception('Request failed with status: ${response.statusCode}');
    }
  }
}

在这个扩展中,我们定义了 timeout 变量,并在 getpost 方法中使用 timeout 方法来设置请求超时时间。如果请求超时,会抛出 TimeoutException,我们在捕获该异常时抛出一个自定义的异常提示请求超时。

五、实际项目中的注意事项

  1. 错误处理 在封装的网络请求中,虽然我们已经对常见的状态码进行了处理,但实际项目中可能会遇到更多复杂的错误情况。比如网络连接失败、服务器返回的数据格式不符合预期等。对于这些情况,我们需要更细致的错误处理。可以在 _handleResponse 方法中进一步扩展,例如:
dynamic _handleResponse(http.Response response) {
  try {
    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else if (response.statusCode == 404) {
      throw Exception('Not Found');
    } else if (response.statusCode == 500) {
      throw Exception('Internal Server Error');
    } else {
      throw Exception('Request failed with status: ${response.statusCode}');
    }
  } on FormatException {
    throw Exception('Invalid data format received');
  }
}

这里我们捕获了 FormatException,用于处理服务器返回的数据格式不符合 JSON 格式的情况。

  1. 性能优化 在高并发的网络请求场景下,性能优化是非常重要的。可以考虑使用 http.Client 的复用,避免每次请求都创建新的 Client 实例。例如:
class BaseApiService {
  final String baseUrl;
  final http.Client client = http.Client();

  BaseApiService({required this.baseUrl});

  Future<dynamic> get(String endpoint) async {
    final response = await client.get(Uri.parse('$baseUrl$endpoint'));
    return _handleResponse(response);
  }

  Future<dynamic> post(String endpoint, Map<String, dynamic> data) async {
    final response = await client.post(
      Uri.parse('$baseUrl$endpoint'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(data),
    );
    return _handleResponse(response);
  }

  // 记得在合适的地方释放资源,比如在页面销毁时
  void dispose() {
    client.close();
  }

  dynamic _handleResponse(http.Response response) {
    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else if (response.statusCode == 404) {
      throw Exception('Not Found');
    } else if (response.statusCode == 500) {
      throw Exception('Internal Server Error');
    } else {
      throw Exception('Request failed with status: ${response.statusCode}');
    }
  }
}

这样可以减少资源开销,提高性能。同时,在不需要使用 client 时,记得调用 client.close() 方法释放资源,例如在页面销毁时。

  1. 安全性 在处理网络请求时,安全性是至关重要的。对于敏感数据的传输,一定要使用 HTTPS 协议。此外,在请求和响应处理过程中,要防止 SQL 注入、XSS 攻击等安全漏洞。比如,在构建请求 URL 时,不要直接拼接用户输入的数据,而是使用 Uri 的参数构建方法:
Future<dynamic> get(String endpoint, Map<String, dynamic> params) async {
  final uri = Uri.parse('$baseUrl$endpoint').replace(queryParameters: params);
  final response = await http.get(uri);
  return _handleResponse(response);
}

这样可以有效防止 SQL 注入等安全问题。

  1. 缓存机制 在一些情况下,我们希望对网络请求的结果进行缓存,以减少不必要的网络请求,提高应用的响应速度。可以通过在内存或本地存储中缓存数据来实现。例如,使用 shared_preferences 插件在本地存储缓存数据:
import 'package:shared_preferences/shared_preferences.dart';

class BaseApiService {
  final String baseUrl;
  final Duration cacheDuration = const Duration(minutes: 5);

  BaseApiService({required this.baseUrl});

  Future<dynamic> get(String endpoint) async {
    final prefs = await SharedPreferences.getInstance();
    final cachedData = prefs.getString(endpoint);
    if (cachedData != null) {
      final cachedTime = prefs.getDouble('${endpoint}_time')?? 0;
      if (DateTime.now().millisecondsSinceEpoch - cachedTime < cacheDuration.inMilliseconds) {
        return jsonDecode(cachedData);
      }
    }
    final response = await http.get(Uri.parse('$baseUrl$endpoint'));
    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      prefs.setString(endpoint, jsonEncode(data));
      prefs.setDouble('${endpoint}_time', DateTime.now().millisecondsSinceEpoch.toDouble());
      return data;
    } else {
      throw Exception('Request failed with status: ${response.statusCode}');
    }
  }

  // 其他方法...

  dynamic _handleResponse(http.Response response) {
    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else if (response.statusCode == 404) {
      throw Exception('Not Found');
    } else if (response.statusCode == 500) {
      throw Exception('Internal Server Error');
    } else {
      throw Exception('Request failed with status: ${response.statusCode}');
    }
  }
}

在这个示例中,我们在 get 方法中检查本地缓存,如果缓存存在且未过期,则直接返回缓存数据;否则,发送网络请求,并在请求成功后更新缓存。

通过以上步骤和注意事项,我们可以对 Flutter 的 http 插件进行有效的封装,简化网络请求代码,提高项目的开发效率和质量,使其更适合实际项目的需求。无论是小型应用还是大型项目,良好的网络请求封装都能为开发过程带来诸多便利。同时,随着项目的发展和需求的变化,我们可以根据实际情况对封装的代码进行进一步的扩展和优化。