

中高端软件定制开发服务商

13245491521 13245491521
跟杰哥一起学Flutter (实战进阶-网络请求封装一条) 点击关注公众号,“技术干货”及时达! ?本文为稀土掘金技术社区首发签约文章 ?1. 引言?? 之前写的 《六、项目实战-非UI部分???♂?》(https://juejin.cn/post/7312723512723521590)中关于 「Json解析」 和 「网络请求」 写得有些简陋,实际开发中非常不好用??,恰逢上节《十五、玩转状态管理之——Riverpod使用详解》(https://juejin.cn/post/7359402114018689076)学了 状态管理库Riverpod,索性本节带着大伙来封装下 「网络请求」,让相关代码写起来稍微 "舒适" 一些。 「??」 「封装无止境」,本节的封装思路和代码不一定足够好或通用,主要是 「授之以渔」,读者可以根据自己的实际情况进行调整或优化。一千个人眼里就有一千个哈姆雷特,「适合自己」 就好,也欢迎大佬评论区不吝赐教,感谢?? 2. 封装后的效果演示1?? 定义API接口处: 2?? UI页面调用处: 运行输出结果: 「Demo下载地址」:Flutter网络请求封装Demo【dio+riverpod】(https://pan.quark.cn/s/91814778de6f#/list/share) ?? 如果你对 「Dart中常见的封装技巧」 或 「封装思路&实践过程」 感兴趣,可以往下阅读?? 3. 常见封装技巧???♂? 捋一捋Flutter中常见的封装技巧,欢迎补充?? 3.1. 单例 & 多例「单例」 是一种常见的 「设计模式」:确保 「一个类只有一个实例」,并提供一个 「全局访问点」 来获取该实例。用它一般是出于下述目的: 「全局唯一」:多个对象需要访问 「相同的资源或数据」,单例可以保证 「所有对象共享同一份数据」,而且避免了重复创建实例导致的 「资源浪费」,如:数据库连接、配置管理等。「处理资源访问冲突」:确保对 「共享资源的访问是受控的」,如:日志工具类,如果有多个实例同时写入可能存在互相覆盖的情况。然后,在Dart中实现一个单例类的核心步骤如下: ① 「私有构造函数」:确保外部无法通过构造函数直接创建类实例。② 「定义静态私有实例」:在类内部声明一个静态实例变量,作为该类的唯一实例。③ 「定义获取实例的静态方法」:如果实例未创建,初始化后返回。实现单例的简单代码示例如下: classSingleton{ //①私有构造函数 Singleton._internal(); //②定义静态私有实例 staticSingleton?_instance=Singleton._internal(); //③定义获取实例的静态方法,也可以用factory构造函数来创建 staticSingletongetinstance=_instance??=Singleton._internal(); //添加需要的变量或方法 int_counter=0; voidincrementCounter(){ _counter++; } intgetcounter=_counter; } voidmain(){ vars1=Singleton.instance; vars2=Singleton.instance; print(identical(s1,s2));//输出:true,表示两个完全相等的对象 s1.incrementCounter(); print(s2.counter);//输出:1 } 「单例」 指的是 「一个类只能创建一个实例」,对应的 「多例」 则是:「一个类能创建多个实例」,「但数量是有限的」。实现方法很简单,核心就是用一个 「Map」 来存实例,每个实例对应一个 「特定的Key」,请求相同的Key返回同一个实例。实现多例的简单代码示例如下: classMultiple{ //私有构造函数 Multiple._internal(); //静态容器实例 staticfinalMapString,Multiple_instances= //获取实例的静态方法,根据给定的key staticMultiplegetInstance(Stringkey){ //如果实例不存在,则创建一个新的实例并存储在映射中 _instances.putIfAbsent(key,()=Multiple._internal()); return_instances[key]!; } } //测试代码 voidmain(){ varm1=Multiple.getInstance("a"); varm2=Multiple.getInstance("b"); varm3=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_runner」:「Dart命令行工具」,可以运行 source_gen 中的 Generator 并将生成的代码写入到文件中。还可以 「监视源代码变化」,并在代码变化时自动重新运行 Generator。写个简单的 「自定义Generator代码示例」 → 为每个带 「@ToString」 注解的类生成 「toString()」 方法,新建一个 Dart 库,命名为 「to_string_generator」,在库中定义两个文件,现实 「注解」 → 「lib/to_string_annotation.dart:」 classToString{ constToString(); } 然后是 「生成器 → 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,用于为具有@ToString 注解的类生成 toString()。 classToStringGeneratorextendsGeneratorForAnnotationToString{ //重写generateForAnnotatedElement()为每个使用@ToString注解的元素(本例中为类)生成代码 @override FutureStringgenerateForAnnotatedElement(Elementelement,ConstantReaderannotation,BuildStepbuildStep)async{ //检查传递的元素是否为 ClassElement(一个类)。如果不是,抛出异常。 if(elementis!ClassElement){ throwInvalidGenerationSourceError('`@ToString()`canonlybedefinedonclasses.',element:element); } //将 element 强制转换为 ClassElement 类型,以便访问类特有的属性和方法。 ClassElementclassElement=element; //构建包含所有字段名称和对应值的字符串表示。 //遍历classElement的fields,每个字段都生成'${field.name}:$${field.name}'的形式, //然后使用 join 方法将它们连接成单一字符串,字段之间用逗号和空格分隔。 StringfieldsString=classElement.fields.map((field){ return'${field.name}:$${field.name}'; }).join(','); //返回一个包含新生成的 toString 方法的字符串。 //这将为类定义一个扩展方法,覆写 toString 方法,返回类名和所有字段的值。 return''' extensionToString${classElement.name}on${classElement.name}{ @override StringtoString(){ return'${classElement.name}{$fieldsString } } '''; } } 接着添加 「本地依赖」: dev_dependencies: build_runner:^2.1.4 to_string_generator: path:../to_string_generator 在 「build.yam」l 中配置下 「生成器」: 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() classPerson{ finalStringname; finalint 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属性」 来获取泛型的类型信息。「运行时获取泛型参数类型」 的代码示例如下: voidmain(){ Listintnumbers1=[1,2,3]; print(numbers1isListint//输出:true Listint?numbers2= print("${numbers1.runtimeType}==${numbers2.runtimeType}→${numbers1.runtimeType==numbers2.runtimeType}");//输出:Listint== Listint?→ false //定义变量赋值 Typetype=numbers1.runtimeType; //格式化输出 print("${numbers1.runtimeType}==${type}→${numbers1.runtimeType==type}");//Listint==Listint→true //验证相同类型泛型参数不同是否相等 ListStringstringList=['a','b','c']; print("${stringList.runtimeType}==${type}→${stringList.runtimeType==type}");//输出:ListString== Listint→ false //运行时类型判定 if(numbers1.runtimeType==Listint)print("true");//输出:true //验证泛型嵌套是否能返回完整的泛型类型信息 ListListListStringlist= print("${list.runtimeType}");//输出:ListListListString } 除此之外,还可以通过 「显式传递类型信息」 来实现 「运行时获取泛型的类型信息」,简单代码示例: voidmain(){ varintBox=Boxint(type:int); varstringBox=BoxString(type:String); checkType(intBox); checkType(stringBox); } voidcheckType(Boxbox){ if(box.type==int){ print('Boxcontainsint'); }elseif(box.type==String){ print('BoxcontainsString'); }else{ print('Boxcontainsunknowntype'); } } classBoxT{ finalTypetype;//显式传递类型信息 Box({requiredthis.type}); } 然后,在提下讲泛型必提的 "「三变」" 在Dart中的表现,以父类-Aniaml、子类-Dog 为例: ① 「不变」:Dog 是 Aniaml的子类型,但 List和 List是不同的类型: voidmain(){ ListAnimalanimals=[Animal(),Dog()]; ListDogdogs=[Dog()]; //List类型是不变的,下面的代码会报错 // dogs = animals;//错误:类型'ListAnimal'不能赋值给'ListDog' } ② 「协变」:「Dart中的函数返回类型」 AnimalgetAnimal()=Animal(); DoggetDog()=Dog(); voidmain(){ //函数返回类型的协变 AnimalFunction()animalGetter=getDog;//这是允许的 print(animalGetter()isDog);//输出:true } ③ 「逆变」:「Dart中的函数参数」 classAnimal{} classDogextendsAnimal{} voidtakeAnimal(Animalanimal){} voidtakeDog(Dogdog){} voidmain(){ //函数参数类型的逆变 voidFunction(Dog)dogTaker=takeAnimal;//这是允许的 dogTaker(Dog());//实际调用takeAnimal,但这里传递的是Dog类型 } 3.4. 函数闭包 (Closure)「官方文档」:《深入理解 Function & Closure》?? 讨论闭包前,得先了解一个词 → 「词法作用域」 (Lexical scoping),即每个变量都有它的作用域,在 「同一个词法作用域」 中 「不允许出现同名变量」,否则编译器会提示语法错误。 //?这样写编译器会报错 voidmain(){ vara=0; vara=1;// Error:The name 'a' is already defined } //?这样可以,因为「vara=0」是「dart文件」的词法作用域中定义的变量 //而「vara=1」则是「main()」的词法作用域中定义的变量,两者不是同一空间,所以不会冲突 voidmain(){ vara=1; print(a);//=1 } vara=0; 然后,在一个词法作用域 「内部」 可以能访问到 「外部」 词法作用域中定义的变量: voidmain(){ varprintName=(){ varname='Vadaski'; printName();//?内部可以访问外部 print(name);//?外部不能访问内部,Error:Undefined name 'name' } 报 「未定义该变量的错误警告」,说明 print() 中定义的变量对于 main() 中的变量是不可见的。Dart 和 JavaScript 一样具有 「链式作用域」 → 「子作用域」 可以访问 「父/祖先作用域」 中的变量,而反过来不行。 然后是变量的 「访问规则」,「近者优先」,先在当前Scope查找,找不到再到它的上一层Scope中查找,以此类推,如果整条Scope链上不存在该变量,提示 Undefined。?? 说完这些,接着说下 「闭包的定义」: ?「特殊的函数对象 (有状态的函数)」 ,即使函数的调用对象在它原始作用域外,依然能访问它在词法作用域内的变量。 ?写个 「无状态」 和 「有状态函数」 的例子: voidmain(){ printNumber();//输出:1 printNumber();//输出:1 //①定义闭包/有状态函数,但未真正执行 varnumberPrinter=(){ intnum=0; //返回一个Function,它能拿到父级Scope中的num,让其自增并打印出来 return(){ ++num; print(num); //②创建该Fuction对象,真正执行「printNumber」 varpb1=numberPrinter(); //③访问numberPrinter中的闭包内容,这里间接访问了num变量,执行自增 //printNumber()作为一个闭包,保存了内部num的状态,只要它不被回收,其内部对象都不会被GC掉 //所以需要注意闭包可能造成内存泄露,或带来内存压力问题 pb1();//输出:1 pb1();//输出:2 //创建另外一个Fuction对象,所以num是从0开始的 varpb2=numberPrinter(); pb2();//输出:1 pb2();//输出:2 } //无状态函数 voidprintNumber(){ intnum=0; num print(num); } 然后是 「闭包在Flutter中的应用」 示例: ① 「在传递对象的地方执行方法」 //通过闭包语法(){}()立即执行闭包内容,并将data返回 Text((){ print(data); returndata; }()) ② 「实现策略模式」 voidmain(){ varres=exec(select('sum'),1,2); print(res); } Functionselect(StringopType){ if(opType=='sum')return if(opType=='sub')return return(a,b)=0; } intexec(NumberOpop,inta,intb){ returnop(a,b); } intsum(inta,intb)=a+ intsub(inta,intb)=a- typedefNumberOp=Function(inta,int ③ 「实现Builder模式/懒加载」 ListView.builder({ //... @requiredIndexedWidgetBuilderitemBuilder, //... }) //接收BuildContext和int作为参数,返回一个内部Widget,这样外部Scpoe也能访问 //IndexedWidgetBuilder的scope内部定义的Widget,从而实现builder模式,而且还自带懒加载 typedefIndexedWidgetBuilder=WidgetFunction(BuildContextcontext,intindex); 3.5. 混入 (Mixin)「混入 (Mixin)」 是Flutter中的一种强大特性,允许在 「不继承某个类」 的情况下,让类使用另一个类的方法和属性。三个关键字:「mixin (声明混入类)」 、「with (使用混入类)」 、「on (限制混入只能应用于特定的字类)」 。混入的实现是依靠 「生成中间类」 的方式,生成伪代码如下: classDwithA,B,C{ //D类现在可以使用A、B、C类的方法 } //生成的中间类(伪代码): class_Intermediate1extendsA{} class_Intermediate2extends_Intermediate1withB{} class_Intermediate3extends_Intermediate2withC{} classDextends_Intermediate3{ //可以添加自己的成员和方法 } 从伪代码不难看出 「混入是线性的」,优先级高于 「继承」,后面的混入类会覆盖前面的 「同名方法」,所以下面的代码: mixinA{voidprintName(){print("A");}} mixinB{voidprintName(){print("B");}} mixinC{voidprintName(){print("C");}} classDwithA,B,C{ voidprintName(){super.printName();} } voidmain(ListStringargs){ D().printName();//输出:C } 输出结果是"C",如果想实现 「每个混入类的同名方法都被调用 (链式调用)」 ,只需简单四步: ① 「定义一个父类」;② 「每个混入类用on限定只能被父类的子类混入」;③ 「方法中调用super」;④ 「使用混入的类继承父类」;然后每个mixin可以添加自己的逻辑,而不影响到其它mixin或基类,具体代码示例如下: classParent{voidprintName(){}} mixinAonParent{ voidprintName(){ super.printName(); print("A"); } } mixinBonParent{ voidprintName(){ super.printName(); print("B"); } } mixinConParent{ voidprintName(){ super.printName(); print("C"); } } classDextendsParentwithA,B,C{ voidprintName(){super.printName();} } voidmain(ListStringargs){ D().printName();//输出:ABC } ?「Tips」:源码 「runApp()」 → 「WidgetsFlutterBinding」 → 「BaseBinding」 中对应的应用~ ?3.6. 扩展 (Extension)Flutter 中的扩展,允许你在 「不修改原有类、枚举或接口源代码的前提下,为其添加新的方法、属性和操作符」。使用 「extension」 关键字来定义扩展,使用代码示例如下: //扩展基本类型 extensionStringExtensionsonString{ //检查字符串是否不为空 boolgetisNotEmpty=this.isNotEmpty; } //扩展类 extensionColorExtensionsonColor{ //为Color类添加一个生成半透明颜色的方法 ColorgetsemiTransparent=withOpacity(0.5); } //扩展枚举 enumFileType{image,text,video} //为FileType枚举添加一个获取中文名称的方法 extensionFileTypeExtensionsonFileType{ Stringgetname{ switch(this){ caseFileType.image: return'图片'; caseFileType.text: return'文本'; caseFileType.video: return'视频'; default: throwException('未知文件类型'); } } } 扩展本质上是通过 「静态方法」 来实现的,如果 「扩展属性/方法名与目标类现有同名」,扩展的定义不会被调用,「原始类的实现具有更高的优先级」。 3.7. .. 和 ...这两个是Dart很常用的操作符,也顺带提下吧,先是 「级联操作符(..)」 → 允许你对同一个对象进行一系列操作,而不需要重复引用该对象。在配置复杂对象时非常有用,它可以使得代码更简洁明了。代码示例: classMyClass{ Stringproperty=''; voidmethod1(){ print('method1called'); } voidmethod2(){ print('method2called'); } } voidmain(){ varmyObject=MyClass() ..property='value' ..method1() ..method2(); print(myObject.property);//输出:value } 然后是 「展开操作符(...)」 → 用于 「将一个集合中所有元素插入刀另一个集合中」。代码示例: varlist1=[1,2,3]; varlist2=[0,...list1]; print(list2);//输出:[0,1,2,3] Widgetbuild(BuildContextcontext){ varlist=WidGET@[ Text('Item1'), Text('Item2'), returnColumn( children:[ Text('Heading'), ...list,//将list中的所有项作为子组件插入 ], } ???♂? 关于Flutter中的常用封装伎俩就介绍到这,欢迎评论区补充,接着着手思考下,网络请求这块具体怎么封装~ 4. 封装思路 & 实践过程4.1. 原始写法?? 先用 「常规方式」 写个简单的网络请求示例,然后再思考如何封装: import'package:dio/dio.dart'; import'package:flutter/material.dart'; voidmain()=runApp(constMyApp()); classMyAppextendsStatelessWidget{ constMyApp({super.key}); @override Widgetbuild(BuildContextcontext){ returnconstMaterialApp(home:HomePage()); } } classHomePageextendsStatefulWidget{ constHomePage({super.key}); @override StateStatefulWidgetcreateState()=_HomePageState(); } class_HomePageStateextendsStateHomePage{ String?testGetResponse; String?testPostResponse; intcurPage=0; FuturevoidtestGet()async{ varresponse=awaitDio().get('https://mock.apifox.com/m1/4081539-3719383-default/flutter_article/testGet'); setState((){ testGetResponse="${response.data}"; } FuturevoidtestPost()async{ //获得当前毫秒时间戳 curPage=0; varresponse=awaitDio().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 Widgetbuild(BuildContextcontext){ returnScaffold( appBar:AppBar(title:constText('Home')), body:Center( child:SingleChildScrollView( child:Column( children:[ Row(children:[ ElevatedButton( onPressed:testGet, child:constText('testGet'), ), constSizedBox(width:20), Expanded(child:Text(testGetResponse??'')), ]), constSizedBox(height:20), Row(children:[ ElevatedButton( onPressed:testPost, child:constText('testPost'), ), constSizedBox(width:20), Expanded(child:Text(testPostResponse??'')), ]), ], ))), } } 点击两个按钮分别发起GET和POST请求,并将响应结果显示到Text上,「运行结果如下」: ?? 不难看出这种原始写法存在的问题 → 「数据层和UI层的耦合」,这让我想起了早期的Android开发,把什么代码都赛道 「Activity」 中,动辄上千甚至上万行的超大类,真丶令人害怕??。 ?? 所以,封装的核心就是这 「两者的解耦(分离)」 ,把代码拆解到不同的包/类中,然后通过一个 「"桥梁"」 进行连接,即:「请求数据」 → 「状态管理」 → 「UI更新」。 4.2. ApiClient?? 每次请求都创建一个Dio实例,大可不必,每个请求的 「配置项」 基本相同,无脑上 「单例」。 ?? 有些项目会做一层 「抽象」,抽取一些通用的方法,然后再由具体的请求库来实现,如:「api_client」 → 「dio_api_client」。搞它的目的,主要是为了 「解耦」,方便后面替换其它请求库时,无需改动大量代码,而且方便测试。 ???♂? 不过个人感觉,小项目搞这一层意义不大,笔者看过的绝大部分的Flutter项目,网络请求不是用内置的http,就是 dio,为了这个 「低频」 的 「方便替换」,得额外定义一些中间类,各种对字段 (互相赋值),着实没必要。比如,你得创建一个 「RequestOptions」 传递下请求的参数: 然后子类实现这个类,需要对一遍字段: ???♂? 所以啊,还不如直接就在 「api_client.dart」 对dio库进行封装: import'package:dio/dio.dart'; import'package:dio/io.dart'; import'package:flutter/foundation.dart'; import'interceptors.dart'; ///请求操作封装 classApiClient{ latefinalDio_dio; staticApiClient?_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]请求头 staticFuturevoidinit(StringbaseUrl,{MapString,String?requestHeaders})async{ _instance??=ApiClient._internal( Dio( BaseOptions( baseUrl:baseUrl, responseType:ResponseType.json, connectTimeout:constDuration(seconds:30), receiveTimeout:constDuration(seconds:30), headers:requestHeaders??await_defaultRequestHeaders, //请求是否成功的判断,返回false,会抛出DioError异常,类型为DioErrorType.RESPONSE //默认接收200-300间的状态码作为成功的请求,不想抛出异常,直接返回true validateStatus:(status)=true, ), ), } //暴露实例供外部访问 staticApiClientgetinstance{ if(_instance==null){ throwException('APIServiceisnotinitialized,callinit()first'); } return_instance!; } ///构造默认请求头 staticFutureMapString,dynamicget_defaultRequestHeadersasync{ MapString,dynamicheaders= returnheaders; } ///更新请求头 voidupdateHeaders(MapString,dynamicheaders){ _dio.options.headers.addAll(headers); } ///执行GET请求 /// ///[endpoint]接口地址例如:/api/v1/user ///[queryParameters]请求参数 ///[options]请求配置 ///[cancelToken]取消请求的token FutureResponseget(Stringendpoint, {MapString,dynamic?queryParameters,Options?options,CancelToken?cancelToken}){ return_dio.get(endpoint,queryParameters:queryParameters,options:options,cancelToken:cancelToken); } ///执行POST请求 ///[endpoint]接口地址 ///[data]请求数据 ///[queryParameters]请求参数 ///[options]请求配置 FutureResponsepost(Stringendpoint, {dynamicdata,MapString,dynamic?queryParameters,Options?options,CancelToken?cancelToken}){ return_dio.post(endpoint, data:data,queryParameters:queryParameters,options:options,cancelToken:cancelToken); } } 然后是拦截器相关的代码 → 「interceptors.dart」: import'dart:io'; import'package:dio/dio.dart'; ///默认拦截器 classDefaultInterceptorsWrapperextendsInterceptorsWrapper{ @override voidonRequest(RequestOptionsoptions,RequestInterceptorHandlerhandler){ //如果是POST请求且请求体为null,设置一个空的json字符串避免后端解析异常 if(options.method.toUpperCase()=="POST"options.data==null){ options.data="{}"; options.headers['content-type']="application/json"; } handler.next(options); } } ///本地代理抓包拦截器 HttpClientlocalProxyHttpClient(){ returnHttpClient() //将请求代理到本机IP:8888,是抓包电脑的IP!!!不要直接用localhost,会报错: //SocketException:Connectionrefused(OSError:Connectionrefused,errno=111),address=localhost,port=47972 ..findProxy=(uri){ return'PROXY192.168.102.117:8888'; } //抓包工具一般会提供一个自签名的证书,会通不过证书校验,这里需要禁用下,直接返回true ..badCertificateCallback=(X509Certificatecert,Stringhost,intport)=true; } 接着调用下试试,修改 「main.dart」 的代码,先调 「init()」 初始化 「ApiClient」: 然后,发起请求的地方: ?? 还是挺简单的,读者可按需添加其它功能,如:「Cookie持久化」 (配合dio_cookie_manager库)、「文件下载」 (dio提供了download(),有下载进度回调) 等。 4.3. API请求接口 & UI自动刷新?? 一种常见的玩法,会把所有 API接口单独抽到一个 「api_service.dart」 中: 调用处: ?? 抽完代码稍微少了一丢丢,但主要问题是: ?「发起异步请求获取数据后,需要手动调 setState() 来更新UI」 ??? 有点麻烦啊,这里可以想办法用上 「Riverpod」,利用它的 「watch()」 监听请求响应数据来 「自动更新UI」。???♂? 然后又有一个问题 ,「Riverpod」 中定义的 「Provider」 的生命周期是全局的,没法在类内部定义,需要把 Provider 变量定义成 「顶层变量」。定义的 「ApiService」 好像变得没啥用??,直接使用 「@riverpod 注解」 来生成 Provider,POST请求需要传递一个page参数: @riverpod FutureResponsetestGet(TestGetRefref)=ApiClient.instance.get("/testGet"); @riverpod FutureResponsetestPost(TestPostRefref,intpage)= ApiClient.instance.post("/testPost",data:{'page':page,"keyword":"${DateTime.now().millisecondsSinceEpoch}"}); 执行 「flutter pub run build_runner build」 生成 Provider 变量,修改下调用处的代码: 可以,实现了 UI 自动刷新,但有个 「小坑」,点击 testPost 按钮发起异步请求,会显示 「null」,接口响应才显示 「返回数据」: 产生这种现象的原因: ?refresh() 会强制重新构建 「Provider」,重新执行与其关联的异步任务并更新Provider的状态。当任务未完成时获取 data,值自然为 null。 ??? 一种解法是定义一个变量 「暂存旧值」,在执行异步任务前赋值,在异步任务执行时显示旧值,完成时再显示新值。监听 「FutureProvider」 的返回值是 「AsyncValue」 类型,使用 「switch」 关键字处理不同的任务状态,具体代码如下: 得在外部 「额外维护一个变量」,有些麻烦,另一种解法是使用特殊的 「Provider」 → 「Notifier」,更精细地控制 「状态」: 调用处: 异步任务执行完才设置state,所以只会触发AsyncData,不会走其它逻辑,不需要switch判断,直接: 也不会现实null。另外,如果想走loading,可在异步任务执行前设置下state的值为 「AsyncLoading」(): ?? 可以对state进行多次设置,把Provider玩法弄明白了,接着说下 「代码组织方式」,有些项目会搞一个 「Repository」 的类用于获取数据,然后 「Provider」 类只用于提供数据,一个简单的代码样例如下: //lib/repositories/user_repository.dart classUserRepository{ finalApiClientapiClient; UserRepository({requiredthis.apiClient}); FutureUsergetUser(Stringid)async{ finalresponse=awaitapiClient.get('/user/$id'); returnUser.fromJson(response.data); } } //lib/providers/user_providers.dart // Tips:用于注入ApiClient,实现单例 finaluserRepositoryProvider=ProviderUserRepository((ref){ finalapiClient=ApiClient.instance;//AssumingApiClientisasingleton returnUserRepository(apiClient:apiClient); }); finaluserProvider=FutureProvider.familyUser,String((ref,id)async{ finaluserRepository=ref.watch(userRepositoryProvider); returnuserRepository.getUser(id); }); //main.dart classUserWidgetextendsConsumerWidget{ finalStringuserId; UserWidget({requiredthis.userId}); @override Widgetbuild(BuildContextcontext,WidgetRefref){ finaluserAsyncValue=ref.watch(userProvider(userId)); returnuserAsyncValue.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'; ///自定义请求异常父类 classApiExceptionimplementsException{ finalint?code; finalString?message; String?stackInfo; ApiException([this.code,this.message]); factoryApiException.fromDioException(DioExceptionexception){ switch(exception.type){ caseDioExceptionType.connectionTimeout: returnBadRequestException(-1,"连接超时"); caseDioExceptionType.sendTimeout: returnBadRequestException(-1,"请求超时"); caseDioExceptionType.receiveTimeout: returnBadRequestException(-1,"响应超时"); caseDioExceptionType.cancel: returnBadRequestException(-1,"请求取消"); caseDioExceptionType.badResponse: int?errorCode=exception.response?.statusCode; switch(errorCode){ case400: returnBadRequestException(errorCode,"请求语法错误"); case401: returnUnauthorisedException(errorCode,"没有权限"); case403: returnUnauthorisedException(errorCode,"服务器拒绝执行"); case404: returnUnauthorisedException(errorCode,"请求资源不存在"); case405: returnUnauthorisedException(errorCode,"请求方法被禁止"); case500: returnUnauthorisedException(errorCode,"服务器内部错误"); case502: returnUnauthorisedException(errorCode,"错误网关"); case503: returnUnauthorisedException(errorCode,"服务器异常"); case504: returnUnauthorisedException(errorCode,"网关超时"); case505: returnUnauthorisedException(errorCode,"不支持HTTP协议请求"); default: returnApiException(errorCode,exception.response?.statusMessage??'未知错误'); } caseDioExceptionType.connectionError: if(exception.errorisSocketException){ returnDisconnectException(-1,"网络未连接"); }else{ returnApiException(-1,"连接错误"); } caseDioExceptionType.badCertificate: returnApiException(-1,"证书错误"); caseDioExceptionType.unknown: returnApiException(-1,exception.error!=null?exception.error.toString():"未知错误"); } } //将各种异常转换为ApiException方便进行统一处理 factoryApiException.from(dynamicexception){ if(exceptionisDioException){ returnApiException.fromDioException(exception); }elseif(exceptionisApiException){ returnexception; }else{ returnApiException(-1,"未知错误")..stackInfo=exception.toString(); } } @override StringtoString(){ return'ApiException{code:$code,message:$message,stackInfo:$stackInfo}'; } } ///错误请求异常 classBadRequestExceptionextendsApiException{ BadRequestException(super.code,super.message); } ///未认证异常 classUnauthorisedExceptionextendsApiException{ UnauthorisedException(super.code,super.message); } ///未登录异常 classNeedLoginExceptionextendsApiException{ NeedLoginException(super.code,super.message); } ///网络未连接异常 classDisconnectExceptionextendsApiException{ DisconnectException(super.code,super.message); } ///应用需要强更 classNeedUpdateExceptionextendsApiException{ NeedUpdateException(super.code,super.message); } ///错误响应格式异常 classErrorResponseFormatExceptionextendsApiException{ ErrorResponseFormatException(super.code,super.message); } ///未知响应类型异常 classNotKnowResponseTypeExceptionextendsApiException{ NotKnowResponseTypeException(super.code,super.message); } 然后是请求方法 (「api_client.dart」): ///通用请求封装 ///[R]data对应的响应类型,[D]Model类对应的类型 ///[dioCall]异步请求,[fromJsonT]响应实体类的fromJson()闭包 Future_performRequestR,D(FutureResponseFunction()dioCall,DFunction(dynamicjson)?fromJsonT)async{ try{ //执行请求,获取响应 Responseresponse=awaitdioCall(); //如果没有设置fromJsonT或者R是dynamic类型,直接返回响应数据 if(fromJsonT==null||R==dynamic||response.datais!MapString,dynamic)returnresponse.data; MapString,dynamic?responseObject=response.data; if(response.statusCode==200responseObject!=nullresponseObject.isEmpty==false){ switch(responseObject['errorCode']){ case200: if(R.toString().contains("DataResponse")){ returnDataResponse.fromJson(responseObject,fromJsonT)as }elseif(R.toString().contains("ListResponse")){ returnListResponse.fromJson(responseObject,fromJsonT)as }else{ throwNotKnowResponseTypeException(-1,'未知响应类型【${R.toString()}】,请检查是否未正确设置响应类型!'); } case105: throwNeedLoginException(-1,"需要登录"); case219: throwNeedLoginException(-1,"应用需要强更"); default: throwApiException(responseObject['errorCode'],responseObject['errorMsg']); } }else{ throwApiException(-1,"错误响应格式"); } }catch(e){ varexception=ApiException.from(e); throwexception; } } ///执行GET请求 /// ///[endpoint]接口地址例如:/api/v1/user ///[fromJsonT]响应实体类的fromJson()闭包 ///[queryParameters]请求参数 ///[options]请求配置 ///[cancelToken]取消请求的token ///[fromJsonT]响应实体类的fromJson()闭包 FuturegetR,D(Stringendpoint, {DFunction(dynamicjson)?fromJsonT, MapString,dynamic?queryParameters, Options?options, CancelToken?cancelToken})= _performRequestR,D( ()=_dio.get(endpoint,queryParameters:queryParameters,options:options,cancelToken:cancelToken), fromJsonT); ///执行POST请求 /// ///[endpoint]接口地址 ///[fromJsonT]响应实体类的fromJson()闭包 ///[data]请求数据 ///[queryParameters]请求参数 ///[options]请求配置 ///[cancelToken]取消请求的token FuturepostR,D(Stringendpoint, {DFunction(dynamicjson)?fromJsonT, dynamicdata, MapString,dynamic?queryParameters, Options?options, CancelToken?cancelToken})= _performRequestR,D( ()=_dio.post(endpoint, data:dataisMapString,dynamic?data:data?.toJson(), queryParameters:queryParameters, options:options, cancelToken:cancelToken), fromJsonT); } 接着改下「provider」,主要是指定 「两个泛型」: @riverpod FutureDataResponseIndexBannertestGet(TestGetRefref)= ApiClient.instance.get("/testGet",fromJsonT:(json)=IndexBanner.fromJson(json)); @riverpod classTestPostextends_$TestPost{ @override DataResponseArticle?build()=null; FuturevoidtestPost(curPage)async{ state=awaitApiClient.instance.postDataResponseArticle,Article("/testPost", fromJsonT:(json)=Article.fromJson(json), data:{'page':curPage,"keyword":"${DateTime.now().millisecondsSinceEpoch}"}); } } 修改下调用处,因为是直接抛异常,所以需要 「捕获下异常」,不然报错直接 「崩(红屏)」 ,UI界面一般会用到异常信息,如果直接在provider中捕获,抛异常不会奔溃,但信息没法向外传递,这种写法不太行???♂?: ?? FutureProvider的返回类型是 「AsyncValue」,自带异常捕获,只需重写下异常处理的回调,两种写法: 比如,修改下后台返回的code字段,不为200时会抛出异常,这里拦截并显示出来了: 4.4.2. 返回默认值/错误对象?? 需要在每个使用异步Provider的地方都得这样写,有点繁琐,可以试下另外一个思路:返回一个 「默认值/错误对象」,然后按需处理,这里直接粗暴地在 「DataResponse」 和 「ListResponse」 里加个可空的 「ApiExcetion」 属性: 接着改下请求解析部分的catch代码块,根据泛型返回对应的默认对象: 调用处直接把值打印出来: 运行输出结果: ?? 异常返回默认值这种写法还有一个好处,「少写一堆判空」,毕竟保证有值返回,具体选哪种,看自己/团队偏好。 4.4.3. 拦截器中统一处理 ?使用 「拦截器」 进行统一处理,返回解析后的对象数据,这种写法的好处 → 「解耦」: ?将数据解析的逻辑从请求方法中分离出来,使得请求方法只关注请求的发送,而不需要关心响应数据的处理。错误处理:在拦截器中处理响应数据,可以更好地进行错误处理。例如,如果服务器返回的数据格式不正确,可以在拦截器中捕获这个错误,并给出相应的错误提示。 ????♂? 理论上可以,实际上走不通,至少在Flutter用dio不行,这个坑我帮大伙踩了: ?dio的拦截器主要用于处理 「请求前的配置」 或 「响应后的数据」,而不是用于 「改变响应的数据类型」 ?????? ??? 也记录下探索过程吧,先是难点 → 「如何传递fromJson()」 「?」,这个简单,直接利用 「Options」 的 「extra字段」 传,这个字段在dio中是用来 「存储额外的请求信息」 的 「Map」,这些信息可在请求的生命周期内的任何地方被访问,在拦截器中,可以通过 「response.requestOptions.extra」 来获取。?? 不用担心请求处传递 「Options」 对象会覆盖原先的其它配置(入method、headers 等),放心,除非你明确指定,否则不会覆盖其它字段。?? 直接手撕请求方法: 把fromJsonT和R的类型都传过去了,接着重写拦截器的 「onResponse()」 ,完成数据解析,把结果赋值给 「response.data」: 最后是 「provider」: ?? 调用处改改,编译通过,程序跑起来了,正当我暗自窃喜的时候,直接报错: 类型转换失败,不能把结果塞到data中,断点看了下response.data的类型,em... Map类型: ?? 另外加一个字段,专门拿来存结果?取的时候多一层而已,试试~ ?? 然而触发了另一个报错: 「错误简述」: ?将DataResponse实例转换为一个可编码的格式错误,这里指的是转换为Json字符串。 ?哈?我费尽心思从Json字符串转对象,搁这又让我转回Json字符串 ??? 打扰了... 5. 小结Flutter Ban了 「反射」,代码封装的可玩性真的是骤减啊,大部分Kotlin能耍的,都走不通,??了。本节的demo只是简单封装,读者可以自行拓展,如:结合 「retrofit」 库,拆成两层 → 「网络请求层」 和 「状态管理层」,简易示例(Power by GPT4??): @RestApi(baseUrl:"https://yourapi.com") abstractclassUserApi{ factoryUserApi(Diodio,{StringbaseUrl})=_UserApi; @GET("/users/{id}") FutureUsergetUser(@Path("id")String @POST("/users") FutureUsercreateUser(@Body()Useruser); } finalcreateUserProvider=FutureProvider.autoDisposeUser((ref)async{ //获取ApiService实例 finalapiService=ref.watch(apiServiceProvider); //创建一个新用户对象 UsernewUser=User(name:"JohnDoe",email:"johndoe@example.com"); //调用API发送POST请求 returnapiService.userApi.createUser(newUser); }); //监听createUserProvider的状态 finaluserState=ref.watch(createUserProvider); //获取状态值 Center( child:userState.when( data:(Useruser){ //请求成功,显示用户信息 returnColumn( mainAxisAlignment:MainAxisAlignment.center, children:WidGET@[ Text('UserCreated!'), Text('Name:${user.name}'), Text('Email:${user.email}'), ], }, loading:()=CircularProgressIndicator(),//请求中,显示加载指示器 error:(error,stack)=Text('Error:$error'),//请求失败,显示错误信息 ), ) //刷新 ref.refresh(createUserProvider); 点击关注公众号,“技术干货”及时达! 阅读原文
| 上一篇:2025-07-19_世界首个「实时、无限」扩散视频生成模型,Karpathy投资站台 | 下一篇:2024-01-25_奥特曼,10亿美元砸向AI芯片 |
TAG标签: |
10 |
|
我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!
|
|
不达标就退款 高性价比建站 免费网站代备案 1对1原创设计服务 7×24小时售后支持 |
|
|
