likes
comments
collection
share

懂 Vue、React 就懂 Flutter 网络请求管理

作者站长头像
站长
· 阅读数 6

在移动应用开发中,网络请求是一个非常常见的场景。在 Flutter 中,我们可以使用 Dio 作为网络请求库来进行网络请求管理。Dio 是一款功能强大且易于使用的网络请求库,它提供了丰富的功能和灵活的配置选项,可以满足我们在开发过程中的各种需求。

为什么不用 Flutter 的内置库发请求呢? 这就和写前端代码不用 fetch API 而用 axios 一个道理。先看一个 Flutter 原生如何发请求

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

Future<dynamic> fetchData() async {
  final url = '<https://example.com/api/data>';
  final response = await http.get(url);

  if (response.statusCode == 200) {
    final data = json.decode(response.body);
    return data;
  } else {
    throw Exception('Failed to fetch data');
  }
}

void main() async {
  final data = await fetchData();
  print(data);
}

这个例子看起来非常简单,它展示了使用原生的 http 库进行网络请求的方式。我们使 用http.get方法发送GET请求,并根据响应的状态码和返回的数据进行相应的处理。然而,使用原生网络请求通常需要编写更多的代码来处理请求和响应,还需要手动处理错误和异常等情况。

如果我们使用 Dio 的话,可以将网络请求的逻辑封装到独立的类中,使代码更具可读性和可维护性。Dio 提供的拦截器可以实现统一的网络请求逻辑,如添加请求头、处理返回数据、处理错误和异常等,简化了代码的编写和维护。同时,Dio也支持并发请求和请求的取消,提高了应用程序的性能和用户体验。还有就是这个 Dio 是跨平台的,就像 Axios 也是跨平台的,可以在 Node.js 和浏览器 2个环境使用。 Dio 可以在手机 Flutter 应用中使用,也可以在 Dart 写的 PC 应用中使用。

Dio == Axios

下面看看如何使用 Dio 发送网络请求

// 导入Dio库
import 'package:dio/dio.dart';

// 创建ApiClient类
class ApiClient {
  Dio _dio;

  ApiClient() {
    _dio = Dio();
    // 添加拦截器
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (RequestOptions options) async {
          // 在请求前添加请求头、请求参数等
          options.headers.addAll({
            'Authorization': 'Bearer your_token_here',
          });
          return options;
        },
        onResponse: (Response response) async {
          // 在响应后处理返回的数据
          return response;
        },
        onError: (DioError error) async {
          // 处理请求错误和异常,例如处理网络连接错误、超时等情况
          return error;
        },
      ),
    );
  }

  // 发起网络请求
  Future<dynamic> fetchData() async {
    try {
      final response = await _dio.get('<https://example.com/api/data>');
      // 处理返回的数据
      return response.data;
    } catch (error) {
      throw Exception('Failed to fetch data');
    }
  }
}

// 在需要使用网络请求的地方调用ApiClient类
void fetchData() async {
  final apiClient = ApiClient();
  final data = await apiClient.fetchData();
  // 使用返回的数据进行操作
}

在这个例子中,我们使用了Dio库来进行网络请求管理。通过创建一个ApiClient类,我们可以将网络请求的逻辑封装起来,使代码更加清晰和易于维护。同时,拦截器的使用可以帮助我们处理各种网络请求场景,如添加请求头、处理返回数据、处理错误和异常等。

对于有 React 和 Vue 经验的前端开发者来说,你可能会熟悉axios、useRequest 或者 react-query 等等网络请求库。在 Flutter 中,使用Dio进行网络请求管理的方式与这些前端框架类似,都是通过拦截器来处理请求和响应,从而实现统一的网络请求逻辑。

Future == Promise

在 Flutter 中是通过 Future 来做异步操作的,和前端的 Promise 非常相似,可以在异步任务完成后获取结果,通过注册回调的方式。甚至,它们都可以通过 async 和 await 关键字来处理异步任务。除了语法有些许不同外,能力上的差异基本都可以通过其他方式实现,比如 Promise 在创建后无法取消,但可以通过捕获错误并处理来模拟取消操作。而 Flutter 可以通过取消订阅来取消异步任务的执行。

下面做一个代码对比

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

Future<void> fetchData() async {
  try {
    final response = await Dio().get('<https://api.example.com/data>');
    // 处理响应数据
    print(response.data);
  } catch (error) {
    // 处理错误
    print('An error occurred: $error');
  }
}

void main() {
  fetchData();
}

而这是 Promise 语法糖 async, await 的代码, 是不是感觉没有区别?

import axios from 'axios';

async function fetchData() {
  try {
    const response = await axios.get('<https://api.example.com/data>');
    // 处理响应数据
    console.log(response.data);
  } catch (error) {
    // 处理错误
    console.log('An error occurred:', error);
  }
}

fetchData();

重试机制

那既然都是我们熟悉的概念和语法,那写一些常见的封装真是随手就来,写一个重试机制吧,假设我们的后台接口不太稳定,总有请求失败的情况,我们为了保障用户体验加一个重试功能,接口请求时最多重试 3 次。

import 'package:dio/dio.dart';

Future<void> fetchDataWithRetry() async {
  const maxRetries = 3;
  var retryCount = 0;

  while (retryCount < maxRetries) {
    try {
      final response = await Dio().get('<https://api.example.com/data>');
      // 处理响应数据
      break; // 成功获取数据,跳出循环
    } catch (error) {
      if (retryCount < maxRetries - 1) {
        retryCount++;
        continue; // 出现错误,继续重试
      } else {
        throw Exception('Failed to fetch data'); // 达到最大重试次数,抛出异常
      }
    }
  }
}

上面代码中我们用 while 循环来控制重试 3 次的逻辑,当数据没有获取时也就是报错时,我们在 catch 中通过 continue 继续发送请求,如果获取到数据了,我们通过 break 结束发送请求。

封装全局 loading

每次发网络请求我们如果都想在等待请求响应时给个loading动画,我们就需要在网络请求发送前后做个标记位:

  • 发送请求之前, loading 为 true, 开始展示 loading 动画
  • 请求响应后(或者超时),loading 为 false, 停止 loading 动画 看起来这就是个拦截网络请求的操作,通过 Dio 的拦截器我们做这件事情轻而易举。我们先继承 Interceptor 然后加入我们上面提到的标记位,然后将这个拦截器加入到全局的客户端请求实例中即可。
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class LoadingInterceptor extends Interceptor {
  bool _isLoading = false;

  @override
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    _isLoading = true;
    // 显示加载动画
    // ...
    super.onRequest(options, handler);
  }

  @override
  void onResponse(
    Response<dynamic> response,
    ResponseInterceptorHandler handler,
  ) {
    _isLoading = false;
    // 隐藏加载动画
    // ...
    super.onResponse(response, handler);
  }

  @override
  void onError(
    DioError err,
    ErrorInterceptorHandler handler,
  ) {
    _isLoading = false;
    // 隐藏加载动画
    // ...
    super.onError(err, handler);
  }

  bool get isLoading => _isLoading;
}

void main() {
  final dio = Dio();
  final loadingInterceptor = LoadingInterceptor();
  dio.interceptors.add(loadingInterceptor);

  runApp(MyApp(dio: dio, loadingInterceptor: loadingInterceptor));
}

class MyApp extends StatelessWidget {
  final Dio dio;
  final LoadingInterceptor loadingInterceptor;

  const MyApp({
    Key? key,
    required this.dio,
    required this.loadingInterceptor,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Loading Example'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () async {
                  // 发起网络请求
                  await dio.get('<https://api.example.com/data>');
                },
                child: const Text('Fetch Data'),
              ),
              if (loadingInterceptor.isLoading)
                const CircularProgressIndicator(), // 显示加载动画
            ],
          ),
        ),
      ),
    );
  }
}

Line 80 我们的组件就可以通过判断 isLoading 是否展示加载动画了。

续签 token

另一个经典使用拦截器的例子就是续签 token, 通过拦截器在拦截器里去处理 token过期需要续签的例子,在应用层只管发请求就行。

先看调用代码, 我们在继承一个拦截器来做 token 续签的相关逻辑。

void main() {
  final dio = Dio();
  final refreshToken = 'refresh_token_here'; // 假设已经获取到刷新令牌
  final newToken = 'new_token_here'; // 假设已经获取到新的访问令牌
  dio.interceptors.add(TokenInterceptor(dio, refreshToken, newToken));

  runApp(MyApp(dio: dio));
}

class MyApp extends StatelessWidget {
  final Dio dio;

  const MyApp({Key? key, required this.dio}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Token Example'),
        ),
        body: Center(
          child: ElevatedButton(
            onPressed: () async {
              // 发起需要授权的请求
              await dio.get('<https://api.example.com/protected_data>');
            },
            child: const Text('Fetch Protected Data'),
          ),
        ),
      ),
    );
  }
}

下面我们就来实现这个 token 拦截器,还是和前面 loading 的例子类似,我们要 override onrequest 方法,因为这个方法会在每次发起请求之前被调用。通过覆盖onRequest方法,我们可以自定义请求的行为,本例中的自定义行为是在请求头中添加访问令牌(access token),以便服务器可以验证用户身份。

import 'dart:async';
import 'package:dio/dio.dart';

class TokenInterceptor extends Interceptor {
  final Dio _dio;
  final String _refreshToken;
  final String _newToken;

  TokenInterceptor(this._dio, this._refreshToken, this._newToken);

  @override
  FutureOr<dynamic> onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    final accessToken = await _getAccessToken();
    options.headers['Authorization'] = 'Bearer $accessToken';
    return super.onRequest(options, handler);
  }

  Future<String> _getAccessToken() async {
    // 检查是否需要续签令牌
    final shouldRefreshToken = _shouldRefreshToken();

    if (shouldRefreshToken) {
      // 发起刷新令牌的请求
      final response = await _dio.post(
        '<https://api.example.com/refresh_token>',
        data: {'refresh_token': _refreshToken},
      );

      // 保存新的访问令牌
      _saveAccessToken(response.data['access_token']);
    }

    return _newToken; // 返回新的访问令牌
  }

  bool _shouldRefreshToken() {
    // 检查是否需要续签令牌的逻辑
    // ...
    return true; // 需要续签令牌
  }

  void _saveAccessToken(String accessToken) {
    // 保存新的访问令牌的逻辑
    // ...
  }
}

_getAccessToken方法中,我们检查是否需要续签令牌。如果需要,我们会发起一个刷新令牌的请求,并保存新的访问令牌。然后,我们将新的访问令牌返回给onRequest方法,以便将其添加到请求头中。

Line 39 就是与服务端约定的核心逻辑了,即什么情况下,客户端需要请求新的 token, 比如我们后端程序设置签发token 为15分钟过期,过期后客户端再请求接口时就会返回比如 40x (自定义多少都可以),客户端拿到判断这个返回状态码,如果是 40x 则请求新的token。

也许有细心的同学会问,我同时有很多请求都发送了怎么办,续签很多次吗?这个问题留给你,动手试试做个队列拦截过期的请求吧!

结尾

本系列文章介绍了 Flutter 如何做网络请求管理,给出了一些常见的代码示例,更重要的是让大家对技术产生连接,移动端和前端都是交付技术的最后一公里,有很多共性,特别是 Dart 语言层面的一些特性,有前端开发经验的同学会非常好理解。

如果你觉得这篇文章有帮助,请点个赞和关注吧❤️!我将持续分享更多有价值的技术知识和经验。谢谢!

转载自:https://juejin.cn/post/7283791013626052620
评论
请登录