跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

本文主要讲解关于跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)相关内容,让我们来一起学习下吧!

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

1. 引言

😀 之前写的 《六、项目实战-非UI部分🤷‍♂️》中关于 Json解析网络请求 写得有些简陋,实际开发中非常不好用😡,恰逢上节《十五、玩转状态管理之——Riverpod使用详解》学了 状态管理库Riverpod,索性本节带着大伙来封装下 网络请求,让相关代码写起来稍微 "舒适" 一些。

🤡 封装无止境,本节的封装思路和代码不一定足够好或通用,主要是 授之以渔,读者可以根据自己的实际情况进行调整或优化。一千个人眼里就有一千个哈姆雷特,适合自己 就好,也欢迎大佬评论区不吝赐教,感谢😆

2. 封装后的效果演示

1️⃣ 定义API接口处:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

2️⃣ UI页面调用处:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

运行输出结果:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

Demo下载地址:Flutter网络请求封装Demo【dio+riverpod】

😄 如果你对 Dart中常见的封装技巧封装思路&实践过程 感兴趣,可以往下阅读😏

3. 常见封装技巧

💁‍♂️ 捋一捋Flutter中常见的封装技巧,欢迎补充👏

3.1. 单例 & 多例

单例 是一种常见的 设计模式:确保 一个类只有一个实例,并提供一个 全局访问点 来获取该实例。用它一般是出于下述目的:

  • 全局唯一:多个对象需要访问 相同的资源或数据,单例可以保证 所有对象共享同一份数据,而且避免了重复创建实例导致的 资源浪费,如:数据库连接、配置管理等。
  • 处理资源访问冲突:确保对 共享资源的访问是受控的,如:日志工具类,如果有多个实例同时写入可能存在互相覆盖的情况。

然后,在Dart中实现一个单例类的核心步骤如下:

  • 私有构造函数:确保外部无法通过构造函数直接创建类实例。
  • 定义静态私有实例:在类内部声明一个静态实例变量,作为该类的唯一实例。
  • 定义获取实例的静态方法:如果实例未创建,初始化后返回。

实现单例的简单代码示例如下:

class Singleton {
  // ① 私有构造函数
  Singleton._internal();
  
  // ② 定义静态私有实例
  static Singleton? _instance = Singleton._internal();
  
  // ③ 定义获取实例的静态方法,也可以用factory构造函数来创建
  static Singleton get instance => _instance ??= Singleton._internal();
  
  // 添加需要的变量或方法
  int _counter = 0;
  
  void incrementCounter() {
    _counter++;
  }
  
  int get counter => _counter;
}


void main() {
  var s1 = Singleton.instance;
  var s2 = Singleton.instance;
  print(identical(s1, s2)); // 输出:true,表示两个完全相等的对象
  s1.incrementCounter();
  print(s2.counter);  // 输出:1
}

单例 指的是 一个类只能创建一个实例,对应的 多例 则是:一个类能创建多个实例但数量是有限的。实现方法很简单,核心就是用一个 Map 来存实例,每个实例对应一个 特定的Key,请求相同的Key返回同一个实例。实现多例的简单代码示例如下:

class Multiple {
  // 私有构造函数
  Multiple._internal();
  
  // 静态容器实例
  static final Map<String, Multiple> _instances = {};

  // 获取实例的静态方法,根据给定的key
  static Multiple getInstance(String key) {
    // 如果实例不存在,则创建一个新的实例并存储在映射中
    _instances.putIfAbsent(key, () => Multiple._internal());
    return _instances[key]!;
  }
}

// 测试代码
void main() {
  var m1 = Multiple.getInstance("a");
  var m2 = Multiple.getInstance("b");
  var m3 = Multiple.getInstance("a");
  print(identical(m1, m2)); // 输出结果:false
  print(identical(m1, m3)); // 输出结果:true
}

3.2. 编译时代码生成

😐 先提一嘴 反射Dart支持反射!!!通过 dart:mirrors 库来提供此功能,但而在 Flutter 中禁用了 运行时反射,因为它会干扰Dart的 tree shaking (摇树) 过程

tree shaking 是Dart编译器优化过程的一个术语,它会 移除 应用程序编译后的 未被使用的代码,以缩减应用的体积。而反射需要在运行时动态查询或调用对象的方法或属性,为此,编译器必须保留应用中所有可能会被反射机制调用的代码,即便这些代码在实际工作流程中可能永远不会被执行,这直接干扰到tree shaking,因为编译器无法确定哪些代码是"多余"的。因此,Flutter禁用了运行时反射 (不能用 dart:mirrors库),鼓励开发者使用 编译时代码生成 的方式来代替反射。

编译时代码生成 一般是通过 source_gen库build_runner工具 来实现的,简单介绍下:

  • source_gen库编译时生成Dart代码,通过 自定义Generator类 读取指定发 输入信息 (类、函数、变量、注解等),并根据这些信息生成新的代码。
  • build_runnerDart命令行工具,可以运行 source_gen 中的 Generator 并将生成的代码写入到文件中。还可以 监视源代码变化,并在代码变化时自动重新运行 Generator。

写个简单的 自定义Generator代码示例 → 为每个带 @ToString 注解的类生成 toString() 方法,新建一个 Dart 库,命名为 to_string_generator,在库中定义两个文件,现实 注解lib/to_string_annotation.dart:

class ToString {
  const ToString();
}

然后是 生成器 → lib/to_string_generator.dart

// Dart语法分析器包,用于分析 Dart代码和提取元素信息。

// 导入的三个包,依次为:
// analyzer → Dart语法分析器,用于分析 Dart代码 & 提取元素信息。
// build → 提供构建步骤中使用的API与模型。
// source_gen → 生成Dart代码

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';

// 导入自定义注解类
import 'to_string_annotation.dart';

// 定义 ToStringGenerator 类继承 GeneratorForAnnotation<T>,用于为具有 @ToString 注解的类生成 toString()。

class ToStringGenerator extends GeneratorForAnnotation<ToString> {
  
  // 重写 generateForAnnotatedElement() 为每个使用 @ToString 注解的元素 (本例中为类) 生成代码
  @override
  Future<String> generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) async {
    // 检查传递的元素是否为 ClassElement(一个类)。如果不是,抛出异常。
    if (element is! ClassElement) {
      throw InvalidGenerationSourceError('`@ToString()` can only be defined on classes.', element: element);
    }

    // 将 element 强制转换为 ClassElement 类型,以便访问类特有的属性和方法。
    ClassElement classElement = element;

    // 构建包含所有字段名称和对应值的字符串表示。
    // 遍历 classElement 的 fields,每个字段都生成 '${field.name}: $${field.name}' 的形式,
    // 然后使用 join 方法将它们连接成单一字符串,字段之间用逗号和空格分隔。
    String fieldsString = classElement.fields.map((field) {
      return '${field.name}: $${field.name}';
    }).join(', ');

    // 返回一个包含新生成的 toString 方法的字符串。
    // 这将为类定义一个扩展方法,覆写 toString 方法,返回类名和所有字段的值。
    return '''

extension ToString${classElement.name} on ${classElement.name} {
  @override
  String toString() {
    return '${classElement.name} { $fieldsString }';
  }
}
    ''';
  }
}

接着添加 本地依赖

dev_dependencies:
  build_runner: ^2.1.4
  to_string_generator:
    path: ../to_string_generator

build.yaml 中配置下 生成器

builders:
  # 构建器标识符
  to_string_generator:
    # 构建器所在的库
    import: "package:to_string_generator/to_string_generator.dart"
    # 构建器工程名称
    builder_factories: ["ToStringGenerator"]
    # 输入和输出文件的扩展名映射
    build_extensions: {".dart": [".g.dart"]}
    # 控制构建器的应用范围,这里设置 dependents 表示将自动应用于依赖当前包的其他包中的文件
    auto_apply: dependents
    # 生成文件的存储位置
    build_to: cache
    # 当前构建器依赖的其它构建器
    applies_builders: ["source_gen"]

然后给类添加上@ToString()注解:

import 'package:to_string_annotation/to_string_annotation.dart';

part 'person.g.dart';

@ToString()
class Person {
  final String name;
  final int age;

  Person(this.name, this.age);
}

最后,执行 flutter pub run build_runner build 即可生成代码~

3.3. 泛型 (Genetic)

😀 泛型的本质类型参数化要操作的数据类型 可以通过 参数的形式 来指定,就:把数据类型变成参数

Dart 的泛型支持 泛型类泛型方法泛型边界(extends) ,没有像Java那样的 通配符(?) ,不指定泛型参数的话,默认为 dynamic 动态类型。😄 然后:

  • Java泛型 是"假泛型",通过 类型擦除 来实现,泛型类型信息 只在 编译时存在,一旦代码被编译了就会被擦除,转换为它们的 边界类型 (如果指定了边界) 或 Object类型,这样做是为了 向后兼容早期的Java版本
  • Dart泛型 的类型是 具象化(reified) 的,即:在运行时保留了泛型的类型信息,因此,你可以在运行时进行类型检查,比如使用 is 关键字判断对象是否为特定的泛型类型。除此之外,还可以使用 Type对象runtimeType属性 来获取泛型的类型信息。

运行时获取泛型参数类型 的代码示例如下:

void main() {
  List<int> numbers1 = [1,2,3];
  print(numbers1 is List<int>); // 输出:true

  List<int?> numbers2 = [];
  print("${numbers1.runtimeType} == ${numbers2.runtimeType} → ${numbers1.runtimeType == numbers2.runtimeType}"); // 输出:List<int> == List<int?> → false

  // 定义变量赋值
  Type type = numbers1.runtimeType;

  // 格式化输出
  print("${numbers1.runtimeType} == ${type} → ${numbers1.runtimeType == type}");// List<int> == List<int> → true

  // 验证相同类型泛型参数不同是否相等
  List<String> stringList = ['a', 'b', 'c'];
  print("${stringList.runtimeType} == ${type} → ${stringList.runtimeType == type}"); // 输出:List<String> == List<int> → false

  // 运行时类型判定
  if(numbers1.runtimeType == List<int>) print("true");// 输出:true

  // 验证泛型嵌套是否能返回完整的泛型类型信息
  List<List<List<String>>> list = [];
  print("${list.runtimeType}"); // 输出:List<List<List<String>>>
}

除此之外,还可以通过 显式传递类型信息 来实现 运行时获取泛型的类型信息,简单代码示例:

void main() {
  var intBox = Box<int>(type: int);
  var stringBox = Box<String>(type: String);

  checkType(intBox);
  checkType(stringBox);
}

void checkType<T>(Box<T> box) {
  if (box.type == int) {
    print('Box contains int');
  } else if (box.type == String) {
    print('Box contains String');
  } else {
    print('Box contains unknown type');
  }
}

class Box<T> {
  final Type type;	// 显式传递类型信息
  Box({required this.type});
}

然后,在提下讲泛型必提的 "三变" 在Dart中的表现,以父类-Aniaml、子类-Dog 为例:

不变:Dog 是 Aniaml的子类型,但 List 和 List 是不同的类型:

void main() {
  List<Animal> animals = [Animal(), Dog()];
  List<Dog> dogs = [Dog()];

  // List 类型是不变的,下面的代码会报错
  // dogs = animals; // 错误:类型 'List<Animal>' 不能赋值给 'List<Dog>'
}

协变Dart中的函数返回类型

Animal getAnimal() => Animal();
Dog getDog() => Dog();

void main() {
  // 函数返回类型的协变
  Animal Function() animalGetter = getDog; // 这是允许的
  print(animalGetter() is Dog); // 输出:true
}

逆变Dart中的函数参数

class Animal {}
class Dog extends Animal {}

void takeAnimal(Animal animal) {}
void takeDog(Dog dog) {}

void main() {
  // 函数参数类型的逆变
  void Function(Dog) dogTaker = takeAnimal; // 这是允许的
  dogTaker(Dog()); // 实际调用 takeAnimal,但这里传递的是 Dog 类型
}

3.4. 函数闭包 (Closure)

官方文档:《深入理解 Function & Closure》😁 讨论闭包前,得先了解一个词 → 词法作用域 (Lexical scoping),即每个变量都有它的作用域,在 同一个词法作用域不允许出现同名变量,否则编译器会提示语法错误。

// ❎ 这样写编译器会报错
void main() {
  var a = 0;
  var a = 1; //  Error:The name 'a' is already defined
}

// ✅ 这样可以,因为「var a = 0」是「dart文件」的词法作用域中定义的变量
// 而「var a = 1」则是「main()」的词法作用域中定义的变量,两者不是同一空间,所以不会冲突
void main() {
  var a = 1; 
  print(a); // => 1
}

 var a = 0;

然后,在一个词法作用域 内部 可以能访问到 外部 词法作用域中定义的变量:

void main() {
  var printName = (){
    var name = 'Vadaski';
  };
  printName(); // ✅ 内部可以访问外部
  
  print(name); // ❎ 外部不能访问内部,Error:Undefined name 'name'
}

未定义该变量的错误警告,说明 print() 中定义的变量对于 main() 中的变量是不可见的。Dart 和 JavaScript 一样具有 链式作用域子作用域 可以访问 父/祖先作用域 中的变量,而反过来不行。

然后是变量的 访问规则近者优先,先在当前Scope查找,找不到再到它的上一层Scope中查找,以此类推,如果整条Scope链上不存在该变量,提示 Undefined。😄 说完这些,接着说下 闭包的定义

特殊的函数对象 (有状态的函数) ,即使函数的调用对象在它原始作用域外,依然能访问它在词法作用域内的变量。

写个 无状态有状态函数 的例子:

void main() {
  printNumber(); // 输出:1
  printNumber(); // 输出:1
  
  // ① 定义闭包/有状态函数,但未真正执行
  var numberPrinter = (){
    int num = 0;
    // 返回一个Function,它能拿到父级Scope中的num,让其自增并打印出来
    return (){
      ++num; 
      print(num);
    };
  };

  // ② 创建该Fuction对象,真正执行「printNumber」
  var pb1 = numberPrinter();
  // ③ 访问 numberPrinter 中的闭包内容,这里间接访问了num变量,执行自增
  // printNumber() 作为一个闭包,保存了内部num的状态,只要它不被回收,其内部对象都不会被GC掉
  // 所以需要注意闭包可能造成内存泄露,或带来内存压力问题
  pb1(); // 输出:1
  pb1(); // 输出:2
  // 创建另外一个Fuction对象,所以num是从0开始的
  var pb2 = numberPrinter();
  pb2(); // 输出:1
  pb2(); // 输出:2
}

// 无状态函数
void printNumber(){
  int num = 0;
  num ++;
  print(num);
}

然后是 闭包在Flutter中的应用 示例:

在传递对象的地方执行方法

// 通过闭包语法 (){}() 立即执行闭包内容,并将data返回
Text((){
    print(data);
    return data;
}())

实现策略模式

void main(){
  var res = exec(select('sum'),1 ,2);
  print(res);
}

Function select(String opType){
  if(opType == 'sum') return sum;
  if(opType == 'sub') return sub;
  return (a, b) => 0;
}

int exec(NumberOp op, int a, int b){
  return op(a,b);
}

int sum(int a, int b) => a + b;
int sub(int a, int b) => a - b;

typedef NumberOp = Function (int a, int b);

实现Builder模式/懒加载

ListView.builder({
//...
    @required IndexedWidgetBuilder itemBuilder,
//...
  })

// 接收 BuildContext 和 int 作为参数,返回一个内部Widget,这样外部Scpoe也能访问
// IndexedWidgetBuilder 的scope内部定义的Widget,从而实现builder模式,而且还自带懒加载
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);

3.5. 混入 (Mixin)

混入 (Mixin) 是Flutter中的一种强大特性,允许在 不继承某个类 的情况下,让类使用另一个类的方法和属性。三个关键字:mixin (声明混入类)with (使用混入类)on (限制混入只能应用于特定的字类) 。混入的实现是依靠 生成中间类 的方式,生成伪代码如下:

class D with A, B, C { 
  // D 类现在可以使用 A、B、C类的方法
}

// 生成的中间类(伪代码):
class _Intermediate1 extends A { }

class _Intermediate2 extends _Intermediate1 with B { }

class _Intermediate3 extends _Intermediate2 with C { }

class D extends _Intermediate3 { 
  // 可以添加自己的成员和方法
}

从伪代码不难看出 混入是线性的,优先级高于 继承,后面的混入类会覆盖前面的 同名方法,所以下面的代码:

mixin A { void printName() { print("A"); } }
mixin B { void printName() { print("B"); } }
mixin C { void printName() { print("C"); } }

class D with A,B,C {
  void printName() {super.printName(); }
}

void main(List<String> args) {
  D().printName();	// 输出:C
}

输出结果是"C",如果想实现 每个混入类的同名方法都被调用 (链式调用) ,只需简单四步:

  • 定义一个父类
  • 每个混入类用on限定只能被父类的子类混入
  • 方法中调用super
  • 使用混入的类继承父类

然后每个mixin可以添加自己的逻辑,而不影响到其它mixin或基类,具体代码示例如下:

class Parent { void printName() { } }
mixin A on Parent { 
  void printName() {
    super.printName();
    print("A"); 
  }
}

mixin B on Parent { 
  void printName() {
    super.printName();
    print("B"); 
  }
}

mixin C on Parent { 
  void printName() {
    super.printName();
    print("C"); 
  }
}

class D extends Parent with A,B,C {
  void printName() {super.printName();}
}

void main(List<String> args) {
  D().printName();  // 输出:ABC
}

Tips:源码 runApp()WidgetsFlutterBindingBaseBinding 中对应的应用~

3.6. 扩展 (Extension)

Flutter 中的扩展,允许你在 不修改原有类、枚举或接口源代码的前提下,为其添加新的方法、属性和操作符。使用 extension 关键字来定义扩展,使用代码示例如下:

// 扩展基本类型
extension StringExtensions on String {
  // 检查字符串是否不为空
  bool get isNotEmpty => this.isNotEmpty;
}

// 扩展类
extension ColorExtensions on Color {
  // 为Color类添加一个生成半透明颜色的方法
  Color get semiTransparent => withOpacity(0.5);
}

// 扩展枚举
enum FileType { image, text, video }
// 为 FileType 枚举添加一个获取中文名称的方法
extension FileTypeExtensions on FileType {
  String get name {
    switch (this) {
      case FileType.image:
        return '图片';
      case FileType.text:
        return '文本';
      case FileType.video:
        return '视频';
      default:
        throw Exception('未知文件类型');
    }
  }
}

扩展本质上是通过 静态方法 来实现的,如果 扩展属性/方法名与目标类现有同名,扩展的定义不会被调用,原始类的实现具有更高的优先级

3.7. .. 和 ...

这两个是Dart很常用的操作符,也顺带提下吧,先是 级联操作符(..) → 允许你对同一个对象进行一系列操作,而不需要重复引用该对象。在配置复杂对象时非常有用,它可以使得代码更简洁明了。代码示例:

class MyClass {
  String property = '';
  void method1() {
    print('method1 called');
  }
  void method2() {
    print('method2 called');
  }
}

void main() {
  var myObject = MyClass()
    ..property = 'value'
    ..method1()
    ..method2();

  print(myObject.property); // 输出: value
}

然后是 展开操作符(...) → 用于 将一个集合中所有元素插入刀另一个集合中。代码示例:

var list1 = [1, 2, 3];
var list2 = [0, ...list1];
print(list2);  // 输出: [0, 1, 2, 3]

Widget build(BuildContext context) {
  var list = <Widget>[
    Text('Item 1'),
    Text('Item 2'),
  ];

  return Column(
    children: [
      Text('Heading'),
      ...list, // 将list中的所有项作为子组件插入
    ],
  );
}

🤷‍♂️ 关于Flutter中的常用封装伎俩就介绍到这,欢迎评论区补充,接着着手思考下,网络请求这块具体怎么封装~

4. 封装思路 & 实践过程

4.1. 原始写法

😄 先用 常规方式 写个简单的网络请求示例,然后再思考如何封装:

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

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: HomePage());
  }
}

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

  @override
  State<StatefulWidget> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String? testGetResponse;
  String? testPostResponse;
  int curPage = 0;

  Future<void> testGet() async {
    var response = await Dio().get('https://mock.apifox.com/m1/4081539-3719383-default/flutter_article/testGet');
    setState(() {
      testGetResponse = "${response.data}";
    });
  }

  Future<void> testPost() async {
    // 获得当前毫秒时间戳
    curPage = 0;
    var response = await Dio().post('https://mock.apifox.com/m1/4081539-3719383-default/flutter_article/testPost',
        data: {'page': curPage, "keyword": "${DateTime.now().millisecondsSinceEpoch}"});
    setState(() {
      testPostResponse = "${response.data}";
      curPage++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
          child: SingleChildScrollView(
              child: Column(
        children: [
          Row(children: [
            ElevatedButton(
              onPressed: testGet,
              child: const Text('testGet'),
            ),
            const SizedBox(width: 20),
            Expanded(child: Text(testGetResponse ?? '')),
          ]),
          const SizedBox(height: 20),
          Row(children: [
            ElevatedButton(
              onPressed: testPost,
              child: const Text('testPost'),
            ),
            const SizedBox(width: 20),
            Expanded(child: Text(testPostResponse ?? '')),
          ]),
        ],
      ))),
    );
  }
}

点击两个按钮分别发起GET和POST请求,并将响应结果显示到Text上,运行结果如下

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

🤔 不难看出这种原始写法存在的问题 → 数据层和UI层的耦合,这让我想起了早期的Android开发,把什么代码都赛道 Activity 中,动辄上千甚至上万行的超大类,真丶令人害怕😱。

😄 所以,封装的核心就是这 两者的解耦(分离) ,把代码拆解到不同的包/类中,然后通过一个 "桥梁" 进行连接,即:请求数据状态管理UI更新

4.2. ApiClient

😳 每次请求都创建一个Dio实例,大可不必,每个请求的 配置项 基本相同,无脑上 单例

🤔 有些项目会做一层 抽象,抽取一些通用的方法,然后再由具体的请求库来实现,如:api_clientdio_api_client。搞它的目的,主要是为了 解耦,方便后面替换其它请求库时,无需改动大量代码,而且方便测试。

🤷‍♂️ 不过个人感觉,小项目搞这一层意义不大,笔者看过的绝大部分的Flutter项目,网络请求不是用内置的http,就是 dio,为了这个 低频方便替换,得额外定义一些中间类,各种对字段 (互相赋值),着实没必要。比如,你得创建一个 RequestOptions 传递下请求的参数:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

然后子类实现这个类,需要对一遍字段:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

🤷‍♂️ 所以啊,还不如直接就在 api_client.dart 对dio库进行封装:

import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/foundation.dart';
import 'interceptors.dart';

/// 请求操作封装
class ApiClient {
  late final Dio _dio;
  static ApiClient? _instance;

  // 私有命名构造函数
  ApiClient._internal(this._dio) {
    // 添加通用的默认拦截器
    _dio.interceptors.add(DefaultInterceptorsWrapper());
    if (kDebugMode) {
      // 添加请求日志拦截器,控制台可以看到请求日志
      _dio.interceptors.add(LogInterceptor(responseBody: true, requestBody: true));
      // 启用本地抓包代理,使用Charles等抓包工具可以抓包
      _dio.httpClientAdapter = IOHttpClientAdapter(createHttpClient: localProxyHttpClient);
    }
  }

  /// !!!单例初始化方法,需要在实例化前调用
  /// [baseUrl] 接口基地址
  /// [requestHeaders] 请求头
  static Future<void> init(String baseUrl, {Map<String, String>? requestHeaders}) async {
    _instance ??= ApiClient._internal(
      Dio(
        BaseOptions(
          baseUrl: baseUrl,
          responseType: ResponseType.json,
          connectTimeout: const Duration(seconds: 30),
          receiveTimeout: const Duration(seconds: 30),
          headers: requestHeaders ?? await _defaultRequestHeaders,
          // 请求是否成功的判断,返回false,会抛出DioError异常,类型为 DioErrorType.RESPONSE
          // 默认接收200-300间的状态码作为成功的请求,不想抛出异常,直接返回true
          validateStatus: (status) => true,
        ),
      ),
    );
  }

  // 暴露实例供外部访问
  static ApiClient get instance {
    if (_instance == null) {
      throw Exception('APIService is not initialized, call init() first');
    }
    return _instance!;
  }

  /// 构造默认请求头
  static Future<Map<String, dynamic>?> get _defaultRequestHeaders async {
    Map<String, dynamic> headers = {};
    return headers;
  }

  /// 更新请求头
  void updateHeaders(Map<String, dynamic> headers) {
    _dio.options.headers.addAll(headers);
  }

  /// 执行GET请求
  ///
  /// [endpoint] 接口地址 例如:/api/v1/user
  /// [queryParameters] 请求参数
  /// [options] 请求配置
  /// [cancelToken] 取消请求的token
  Future<Response<T>> get<T>(String endpoint,
      {Map<String, dynamic>? queryParameters, Options? options, CancelToken? cancelToken}) {
    return _dio.get(endpoint, queryParameters: queryParameters, options: options, cancelToken: cancelToken);
  }

  /// 执行POST请求
  /// [endpoint] 接口地址
  /// [data] 请求数据
  /// [queryParameters] 请求参数
  /// [options] 请求配置
  Future<Response<T>> post<T>(String endpoint,
      {dynamic data, Map<String, dynamic>? queryParameters, Options? options, CancelToken? cancelToken}) {
    return _dio.post<T>(endpoint,
        data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken);
  }
}

然后是拦截器相关的代码 → interceptors.dart

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

/// 默认拦截器
class DefaultInterceptorsWrapper extends InterceptorsWrapper {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // 如果是POST请求且请求体为null,设置一个空的json字符串避免后端解析异常
    if (options.method.toUpperCase() == "POST" && options.data == null) {
      options.data = "{}";
      options.headers['content-type'] = "application/json";
    }
    handler.next(options);
  }
}

/// 本地代理抓包拦截器
HttpClient localProxyHttpClient() {
  return HttpClient()
  // 将请求代理到 本机IP:8888,是抓包电脑的IP!!!不要直接用localhost,会报错:
  // SocketException: Connection refused (OS Error: Connection refused, errno = 111), address = localhost, port = 47972
    ..findProxy = (uri) {
      return 'PROXY 192.168.102.117:8888';
    }
  // 抓包工具一般会提供一个自签名的证书,会通不过证书校验,这里需要禁用下,直接返回true
    ..badCertificateCallback = (X509Certificate cert, String host, int port) => true;
}

接着调用下试试,修改 main.dart 的代码,先调 init() 初始化 ApiClient

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

然后,发起请求的地方:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

😄 还是挺简单的,读者可按需添加其它功能,如:Cookie持久化 (配合dio_cookie_manager库)、文件下载 (dio提供了download(),有下载进度回调) 等。

4.3. API请求接口 & UI自动刷新

😄 一种常见的玩法,会把所有 API接口单独抽到一个 api_service.dart 中:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

调用处:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

😄 抽完代码稍微少了一丢丢,但主要问题是:

发起异步请求获取数据后,需要手动调 setState() 来更新UI

🤔 有点麻烦啊,这里可以想办法用上 Riverpod,利用它的 watch() 监听请求响应数据来 自动更新UI。🤷‍♂️ 然后又有一个问题 ,Riverpod 中定义的 Provider 的生命周期是全局的,没法在类内部定义,需要把 Provider 变量定义成 顶层变量。定义的 ApiService 好像变得没啥用🤣,直接使用 @riverpod 注解 来生成 Provider,POST请求需要传递一个page参数:

@riverpod
Future<Response> testGet(TestGetRef ref) => ApiClient.instance.get("/testGet");

@riverpod
Future<Response> testPost(TestPostRef ref, int page) =>
    ApiClient.instance.post("/testPost", data: {'page': page, "keyword": "${DateTime.now().millisecondsSinceEpoch}"});

执行 flutter pub run build_runner build 生成 Provider 变量,修改下调用处的代码:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

可以,实现了 UI 自动刷新,但有个 小坑,点击 testPost 按钮发起异步请求,会显示 null,接口响应才显示 返回数据

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

产生这种现象的原因:

refresh() 会强制重新构建 Provider,重新执行与其关联的异步任务并更新Provider的状态。当任务未完成时获取 data,值自然为 null。

🤔 一种解法是定义一个变量 暂存旧值,在执行异步任务前赋值,在异步任务执行时显示旧值,完成时再显示新值。监听 FutureProvider 的返回值是 AsyncValue 类型,使用 switch 关键字处理不同的任务状态,具体代码如下:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

得在外部 额外维护一个变量,有些麻烦,另一种解法是使用特殊的 ProviderNotifier,更精细地控制 状态

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

调用处:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

异步任务执行完才设置state,所以只会触发AsyncData,不会走其它逻辑,不需要switch判断,直接:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

也不会现实null。另外,如果想走loading,可在异步任务执行前设置下state的值为 AsyncLoading():

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

😄 可以对state进行多次设置,把Provider玩法弄明白了,接着说下 代码组织方式,有些项目会搞一个 Repository 的类用于获取数据,然后 Provider 类只用于提供数据,一个简单的代码样例如下:

// lib/repositories/user_repository.dart
class UserRepository {
  final ApiClient apiClient;

  UserRepository({required this.apiClient});

  Future<User> getUser(String id) async {
    final response = await apiClient.get('/user/$id');
    return User.fromJson(response.data);
  }
}

// lib/providers/user_providers.dart

// Tips:用于注入ApiClient,实现单例
final userRepositoryProvider = Provider<UserRepository>((ref) {
  final apiClient = ApiClient.instance; // Assuming ApiClient is a singleton
  return UserRepository(apiClient: apiClient);
});

final userProvider = FutureProvider.family<User, String>((ref, id) async {
  final userRepository = ref.watch(userRepositoryProvider);
  return userRepository.getUser(id);
});

// main.dart
class UserWidget extends ConsumerWidget {
  final String userId;

  UserWidget({required this.userId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsyncValue = ref.watch(userProvider(userId));

    return userAsyncValue.when(
      data: (user) => Text(user.name),
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
    );
  }
}

🤔 em... 从 职责分离 的角度,这样做确实有意义,而且可以建多个Repository来分离不同业务的 API请求,便于管理维护。当然,要不要这样搞看自己哈,反正笔者的小项目是直接Providre一把梭滴🤣~

4.4. 数据解析 & 异常处理

😄 就是将接口返回的 Json字符串 解析为具体的 对象实例,Flutter 禁了 反射,得手动或使用工具来生成Bean类的 序列化-toJson()反序列化-fromJson() 代码,官方推荐使用 json_serializable 库来自动生成。这块内容可以查阅笔者之前写的《十二、实战进阶-Json序/反序列化的最佳实践》,这里不再复述。这里主要讨论两点:

  • 数据解析的时机:在 请求方法 中统一处理,还是在 拦截器 中处理?
  • 异常处理:请求或解析时发生错误,是直接 抛异常,还是返回一个 默认值/错误对象

4.4.1. 请求方法中统一解析 + 抛异常

先试下 请求方法中统一解析 + 抛异常 的写法,根据业务封装一个请求异常父类及相关子类 (api_exceptions.dart):

import 'dart:io';

import 'package:dio/dio.dart';

/// 自定义请求异常父类
class ApiException implements Exception {
  final int? code;
  final String? message;
  String? stackInfo;

  ApiException([this.code, this.message]);

  factory ApiException.fromDioException(DioException exception) {
    switch (exception.type) {
      case DioExceptionType.connectionTimeout:
        return BadRequestException(-1, "连接超时");
      case DioExceptionType.sendTimeout:
        return BadRequestException(-1, "请求超时");
      case DioExceptionType.receiveTimeout:
        return BadRequestException(-1, "响应超时");
      case DioExceptionType.cancel:
        return BadRequestException(-1, "请求取消");
      case DioExceptionType.badResponse:
        int? errorCode = exception.response?.statusCode;
        switch (errorCode) {
          case 400:
            return BadRequestException(errorCode, "请求语法错误");
          case 401:
            return UnauthorisedException(errorCode, "没有权限");
          case 403:
            return UnauthorisedException(errorCode, "服务器拒绝执行");
          case 404:
            return UnauthorisedException(errorCode, "请求资源不存在");
          case 405:
            return UnauthorisedException(errorCode, "请求方法被禁止");
          case 500:
            return UnauthorisedException(errorCode, "服务器内部错误");
          case 502:
            return UnauthorisedException(errorCode, "错误网关");
          case 503:
            return UnauthorisedException(errorCode, "服务器异常");
          case 504:
            return UnauthorisedException(errorCode, "网关超时");
          case 505:
            return UnauthorisedException(errorCode, "不支持HTTP协议请求");
          default:
            return ApiException(errorCode, exception.response?.statusMessage ?? '未知错误');
        }
      case DioExceptionType.connectionError:
        if (exception.error is SocketException) {
          return DisconnectException(-1, "网络未连接");
        } else {
          return ApiException(-1, "连接错误");
        }
      case DioExceptionType.badCertificate:
        return ApiException(-1, "证书错误");
      case DioExceptionType.unknown:
        return ApiException(-1, exception.error != null ? exception.error.toString() : "未知错误");
    }
  }

  // 将各种异常转换为ApiException方便进行统一处理
  factory ApiException.from(dynamic exception) {
    if (exception is DioException) {
      return ApiException.fromDioException(exception);
    } else if (exception is ApiException) {
      return exception;
    } else {
      return ApiException(-1, "未知错误")..stackInfo = exception.toString();
    }
  }

  @override
  String toString() {
    return 'ApiException{code: $code, message: $message, stackInfo: $stackInfo}';
  }
}

/// 错误请求异常
class BadRequestException extends ApiException {
  BadRequestException(super.code, super.message);
}

/// 未认证异常
class UnauthorisedException extends ApiException {
  UnauthorisedException(super.code, super.message);
}

/// 未登录异常
class NeedLoginException extends ApiException {
  NeedLoginException(super.code, super.message);
}

/// 网络未连接异常
class DisconnectException extends ApiException {
  DisconnectException(super.code, super.message);
}

/// 应用需要强更
class NeedUpdateException extends ApiException {
  NeedUpdateException(super.code, super.message);
}

/// 错误响应格式异常
class ErrorResponseFormatException extends ApiException {
  ErrorResponseFormatException(super.code, super.message);
}

/// 未知响应类型异常
class NotKnowResponseTypeException extends ApiException {
  NotKnowResponseTypeException(super.code, super.message);
}

然后是请求方法 (api_client.dart):

  /// 通用请求封装
  /// [R] data 对应的响应类型,[D] Model类对应的类型
  /// [dioCall] 异步请求,[fromJsonT] 响应实体类的fromJson()闭包
  Future<R?> _performRequest<R, D>(Future<Response> Function() dioCall, D Function(dynamic json)? fromJsonT) async {
    try {
      // 执行请求,获取响应
      Response response = await dioCall();
      // 如果没有设置fromJsonT或者R是dynamic类型,直接返回响应数据
      if (fromJsonT == null || R == dynamic || response.data is! Map<String, dynamic>) return response.data;
      Map<String, dynamic>? responseObject = response.data;
      if (response.statusCode == 200 && responseObject != null && responseObject.isEmpty == false) {
        switch (responseObject['errorCode']) {
          case 200:
            if (R.toString().contains("DataResponse")) {
              return DataResponse<D>.fromJson(responseObject, fromJsonT) as R?;
            } else if (R.toString().contains("ListResponse")) {
              return ListResponse<D>.fromJson(responseObject, fromJsonT) as R?;
            } else {
              throw NotKnowResponseTypeException(-1, '未知响应类型【${R.toString()}】,请检查是否未正确设置响应类型!');
            }
          case 105:
            throw NeedLoginException(-1, "需要登录");
          case 219:
            throw NeedLoginException(-1, "应用需要强更");
          default:
            throw ApiException(responseObject['errorCode'], responseObject['errorMsg']);
        }
      } else {
        throw ApiException(-1, "错误响应格式");
      }
    } catch (e) {
      var exception = ApiException.from(e);
      throw exception;
    }
  }

  /// 执行GET请求
  ///
  /// [endpoint] 接口地址 例如:/api/v1/user
  /// [fromJsonT] 响应实体类的fromJson()闭包
  /// [queryParameters] 请求参数
  /// [options] 请求配置
  /// [cancelToken] 取消请求的token
  /// [fromJsonT] 响应实体类的fromJson()闭包
  Future<R?> get<R, D>(String endpoint,
          {D Function(dynamic json)? fromJsonT,
          Map<String, dynamic>? queryParameters,
          Options? options,
          CancelToken? cancelToken}) =>
      _performRequest<R, D>(
          () => _dio.get(endpoint, queryParameters: queryParameters, options: options, cancelToken: cancelToken),
          fromJsonT);

  /// 执行POST请求
  /// 
  /// [endpoint] 接口地址
  /// [fromJsonT] 响应实体类的fromJson()闭包
  /// [data] 请求数据
  /// [queryParameters] 请求参数
  /// [options] 请求配置
  /// [cancelToken] 取消请求的token
  Future<R?> post<R, D>(String endpoint,
          {D Function(dynamic json)? fromJsonT,
          dynamic data,
          Map<String, dynamic>? queryParameters,
          Options? options,
          CancelToken? cancelToken}) =>
      _performRequest<R, D>(
          () => _dio.post(endpoint,
              data: data is Map<String, dynamic> ? data : data?.toJson(),
              queryParameters: queryParameters,
              options: options,
              cancelToken: cancelToken),
          fromJsonT);
}

接着改下provider,主要是指定 两个泛型

@riverpod
Future<DataResponse<IndexBanner>?> testGet(TestGetRef ref) =>
    ApiClient.instance.get("/testGet", fromJsonT: (json) => IndexBanner.fromJson(json));

@riverpod
class TestPost extends _$TestPost {
  @override
  DataResponse<Article>? build() => null;

  Future<void> testPost(curPage) async {
    state = await ApiClient.instance.post<DataResponse<Article>, Article>("/testPost",
        fromJsonT: (json) => Article.fromJson(json),
        data: {'page': curPage, "keyword": "${DateTime.now().millisecondsSinceEpoch}"});
  }
}

修改下调用处,因为是直接抛异常,所以需要 捕获下异常,不然报错直接 崩(红屏) ,UI界面一般会用到异常信息,如果直接在provider中捕获,抛异常不会奔溃,但信息没法向外传递,这种写法不太行🤷‍♂️:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

😁 FutureProvider的返回类型是 AsyncValue,自带异常捕获,只需重写下异常处理的回调,两种写法:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

比如,修改下后台返回的code字段,不为200时会抛出异常,这里拦截并显示出来了:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

4.4.2. 返回默认值/错误对象

😄 需要在每个使用异步Provider的地方都得这样写,有点繁琐,可以试下另外一个思路:返回一个 默认值/错误对象,然后按需处理,这里直接粗暴地在 DataResponseListResponse 里加个可空的 ApiExcetion 属性:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

接着改下请求解析部分的catch代码块,根据泛型返回对应的默认对象:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

调用处直接把值打印出来:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

运行输出结果:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

😄 异常返回默认值这种写法还有一个好处,少写一堆判空,毕竟保证有值返回,具体选哪种,看自己/团队偏好。

4.4.3. 拦截器中统一处理 ❓

使用 拦截器 进行统一处理,返回解析后的对象数据,这种写法的好处 → 解耦

将数据解析的逻辑从请求方法中分离出来,使得请求方法只关注请求的发送,而不需要关心响应数据的处理。错误处理:在拦截器中处理响应数据,可以更好地进行错误处理。例如,如果服务器返回的数据格式不正确,可以在拦截器中捕获这个错误,并给出相应的错误提示。

🤷‍♂️ 理论上可以,实际上走不通,至少在Flutter用dio不行,这个坑我帮大伙踩了:

dio的拦截器主要用于处理 请求前的配置响应后的数据,而不是用于 改变响应的数据类型 ❗️❗️❗️

🤡 也记录下探索过程吧,先是难点 → 如何传递fromJson() ,这个简单,直接利用 Optionsextra字段 传,这个字段在dio中是用来 存储额外的请求信息Map,这些信息可在请求的生命周期内的任何地方被访问,在拦截器中,可以通过 response.requestOptions.extra 来获取。💡 不用担心请求处传递 Options 对象会覆盖原先的其它配置(入method、headers 等),放心,除非你明确指定,否则不会覆盖其它字段。 🙁 直接手撕请求方法:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

把fromJsonT和R的类型都传过去了,接着重写拦截器的 onResponse() ,完成数据解析,把结果赋值给 response.data

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

最后是 provider

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

🤡 调用处改改,编译通过,程序跑起来了,正当我暗自窃喜的时候,直接报错:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

类型转换失败,不能把结果塞到data中,断点看了下response.data的类型,em... Map类型:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

🤔 另外加一个字段,专门拿来存结果?取的时候多一层而已,试试~

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

☹️ 然而触发了另一个报错:

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

错误简述

将DataResponse实例转换为一个可编码的格式错误,这里指的是转换为Json字符串。

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

哈?我费尽心思从Json字符串转对象,搁这又让我转回Json字符串 ❓❓❓ 打扰了...

跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)

5. 小结

Flutter Ban了 反射,代码封装的可玩性真的是骤减啊,大部分Kotlin能耍的,都走不通,🤮了。本节的demo只是简单封装,读者可以自行拓展,如:结合 retrofit 库,拆成两层 → 网络请求层状态管理层,简易示例(Power by GPT4🤡):

@RestApi(baseUrl: "https://yourapi.com")
abstract class UserApi {
  factory UserApi(Dio dio, {String baseUrl}) = _UserApi;

  @GET("/users/{id}")
  Future<User> getUser(@Path("id") String id);

  @POST("/users")
  Future<User> createUser(@Body() User user);
}

final createUserProvider = FutureProvider.autoDispose<User>((ref) async {
  // 获取ApiService实例
  final apiService = ref.watch(apiServiceProvider);
  // 创建一个新用户对象
  User newUser = User(name: "John Doe", email: "johndoe@example.com");
  // 调用API发送POST请求
  return apiService.userApi.createUser(newUser);
});

// 监听createUserProvider的状态
final userState = ref.watch(createUserProvider);

// 获取状态值
Center(
  child: userState.when(
    data: (User user) {
      // 请求成功,显示用户信息
      return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('User Created!'),
          Text('Name: ${user.name}'),
          Text('Email: ${user.email}'),
        ],
      );
    },
    loading: () => CircularProgressIndicator(), // 请求中,显示加载指示器
    error: (error, stack) => Text('Error: $error'), // 请求失败,显示错误信息
  ),
)

// 刷新
ref.refresh(createUserProvider);

以上就是关于跟🤡杰哥一起学Flutter (十六、实战进阶-网络请求封装一条🐲)相关的全部内容,希望对你有帮助。欢迎持续关注程序员导航网,学习愉快哦!

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...