在Flutter上优雅的请求网络数据

当你点进来看这篇文章时,应该和我一样在思考如何优雅的请求网络、处理加载状态、处理加载异常。希望这篇文章和案例能给你带来不一样的思考。

解决的问题

  • 通用异常处理
  • 请求资源状态可见(加载成功,加载中,加载失败)
  • 通用重试逻辑

效果展示

为了演示请求失败的处理,特意在wanApi抛了两次错 LBeZ5Q.gif

正文

搜索一下关于flutter网络封装的多半都是dio相关的封装,简单的封装、复杂的封装百花齐放,思路都是工具类的封装。今天换一个思路来实现,引入repository对数据层进行操作,在repository里使用dio作为一个数据源供repository使用,需要使用数据就对repository进行操作不直接调用数据源(在repositoy里是不允许直接操作数据源的)。用WanAndroid的接口写个示例demo

定义数据源

使用retrofit作为数据源,感兴趣的小伙伴可以看下retrofit这个库

@RestApi(baseUrl: "https://www.wanandroid.com")
abstract class WanApi {
  factory WanApi(Dio dio, {String baseUrl}) = _WanApi;

  @GET("/banner/json")
  Future<BannerModel> getBanner();

  @GET("/article/top/json")
  Future<TopArticleModel> getTopArticle();

  @GET("/friend/json")
  Future<PopularSiteModel> getPopularSite();
}
复制代码

生成的代码

class _WanApi implements WanApi {
  _WanApi(this._dio, {this.baseUrl}) {
    baseUrl ??= 'https://www.wanandroid.com';
  }

  final Dio _dio;

  String? baseUrl;

  @override
  Future<BannerModel> getBanner() async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _headers = <String, dynamic>{};
    final _data = <String, dynamic>{};
    final _result = await _dio.fetch<Map<String, dynamic>>(
        _setStreamType<BannerModel>(
            Options(method: 'GET', headers: _headers, extra: _extra)
                .compose(_dio.options, '/banner/json',
                    queryParameters: queryParameters, data: _data)
                .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    final value = BannerModel.fromJson(_result.data!);
    return value;
  }

  @override
  Future<TopArticleModel> getTopArticle() async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _headers = <String, dynamic>{};
    final _data = <String, dynamic>{};
    final _result = await _dio.fetch<Map<String, dynamic>>(
        _setStreamType<TopArticleModel>(
            Options(method: 'GET', headers: _headers, extra: _extra)
                .compose(_dio.options, '/article/top/json',
                    queryParameters: queryParameters, data: _data)
                .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    final value = TopArticleModel.fromJson(_result.data!);
    return value;
  }

  @override
  Future<PopularSiteModel> getPopularSite() async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _headers = <String, dynamic>{};
    final _data = <String, dynamic>{};
    final _result = await _dio.fetch<Map<String, dynamic>>(
        _setStreamType<PopularSiteModel>(
            Options(method: 'GET', headers: _headers, extra: _extra)
                .compose(_dio.options, '/friend/json',
                    queryParameters: queryParameters, data: _data)
                .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    final value = PopularSiteModel.fromJson(_result.data!);
    return value;
  }
  
  RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
    if (T != dynamic &&
        !(requestOptions.responseType == ResponseType.bytes ||
            requestOptions.responseType == ResponseType.stream)) {
      if (T == String) {
        requestOptions.responseType = ResponseType.plain;
      } else {
        requestOptions.responseType = ResponseType.json;
      }
    }
    return requestOptions;
  }
}
复制代码

repository封装

Resource是封装的资源加载状态类,用于包装资源

enum ResourceState { loading, failed, success }

class Resource<T> {
  final T? data;
  final ResourceState state;
  final dynamic error;
  Resource._({required this.state, this.error, this.data});

  factory Resource.failed(dynamic error) {
    return Resource._(state: ResourceState.failed, error: error);
  }

  factory Resource.success(T data) {
    return Resource._(state: ResourceState.success, data: data);
  }

  factory Resource.loading() {
    return Resource._(state: ResourceState.loading);
  }

  bool get isLoading => state == ResourceState.loading;
  bool get isSuccess => state == ResourceState.success;
  bool get isFailed => state == ResourceState.failed;
}
复制代码

接下来我们在Repository里使用WanApi来封装,我们通过流的方式返回了资源加载的状态可供View层根据状态展示不同的界面,使用try-catch保证网络请求的健壮性

class WanRepository extends BaseRepository {
  late WanApi wanApi = GetInstance().find();
  ///获取首页所需的所有数据
  Stream<Resource<HomeDataMapper>> homeData() async* {
    //加载中
    yield Resource.loading();
    try {
      var result = await Future.wait<dynamic>([
        wanApi.getBanner(),
        wanApi.getPopularSite(),
        wanApi.getTopArticle()
      ]);
      final BannerModel banner = result[0];
      final PopularSiteModel site = result[1];
      final TopArticleModel article = result[2];
      //加载成功
      yield Resource.success(
          HomeDataMapper(site.data, banner.data, article.data));
    } catch (e) {
      //加载失败
      yield Resource.failed(e);
    }
  }
}
复制代码

咋一看感觉没啥问题细思之下问题很多,每一个请求还多了try-catch以外那么多的模板方法,实际开发中只写try包裹的内容才符合摸鱼佬的习惯。ok,我们把模板方法提取出来到一个公共方法里去,就变成了这样:

class WanRepository extends BaseRepository {
  late WanApi wanApi = GetInstance().find();
  ///获取首页所需的所有数据
  Stream<Resource<HomeDataMapper>> homeData() async* {
    ///定义加载函数
    loadHomeData()async*{
      var result = await Future.wait<dynamic>([
        wanApi.getBanner(),
        wanApi.getPopularSite(),
        wanApi.getTopArticle()
      ]);
      final BannerModel banner = result[0];
      final PopularSiteModel site = result[1];
      final TopArticleModel article = result[2];
      //加载成功
      yield Resource.success(
          HomeDataMapper(site.data, banner.data, article.data));
    }
    ///将加载函数放在一个包装器里执行
    yield* MyWrapper.customStreamWrapper(loadHomeData);
  }
}
复制代码

得益于Dart中函数可以作为参数传递,所以我们可以定义一个包装方法,入参是具体业务的函数,出参和业务函数一致,在这个方法里可以处理各种异常,甚至可以实现通用的请求重试(只需要在失败的时候弹窗提醒用户重试,获得认可后再次执行function就可以了,更关键的是此时状态管理里对repository的调用依旧是完整的,也就是说这是一个通用的重试功能) 包装器代码:

class MyWrapper {
  //流的方式
  static Stream<Resource<T>> customStreamWrapper<T>(
      Stream<Resource<T>> Function() function,
      {bool retry = false}) async* {
    yield Resource.loading();
    try {
      var result = function.call();
      await for(var data in result){
        yield data;
      }
    } catch (e) {
      //重试代码
      if (retry) {
        var toRetry = await Get.dialog(const RequestRetryDialog());
        if (toRetry == true) {
          yield* customStreamWrapper(function,retry: retry);
        } else {
          yield Resource.failed(e);
        }
      } else {
        yield Resource.failed(e);
      }
    }
  }
}
复制代码

其实就是把相同的地方封装成一个通用方法,不同的地方单独拎出来编写,然后作为一个参数传到包装器里执行。显然这样的方法却不够优雅,每次在写repository的时候都得创建一个函数在里面编写请求数据的逻辑然后交给包装器执行。我们肯定希望repository里代码长成这个样子:

@Repo()
abstract class WanRepository extends BaseRepository {
  late WanApi wanApi = GetInstance().find();

  ///获取首页所需的所有数据
  @ProxyCall()
  @Retry()
  Stream<Resource<HomeDataMapper>> homeData() async* {
    var result = await Future.wait<dynamic>(
        [wanApi.getBanner(), wanApi.getPopularSite(), wanApi.getTopArticle()]);
    final BannerModel banner = result[0];
    final PopularSiteModel site = result[1];
    final TopArticleModel article = result[2];
    yield Resource.success(
        HomeDataMapper(site.data, banner.data, article.data));
  }
}
复制代码

是的没错,最终的repository就长这个样子,你只需要在类上打个注解@Repo在需要代理调用的方法上注解@ProxyCall,运行 flutter pub run build_runner build 就可以生成对应的包装代码:

扫描二维码关注公众号,回复: 13798411 查看本文章
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'wan_repository.dart';

// **************************************************************************
// RepositoryGenerator
// **************************************************************************

class WanRepositoryImpl = WanRepository with _WanRepository;

mixin _WanRepository on WanRepository {
  @override
  Stream<Resource<HomeDataMapper>> homeData() {
    return MyWrapper.customStreamWrapper(() => super.homeData(), retry: true);
  }
}
复制代码

结语

感谢你的阅读,这只是一个网络请求封装的思路不是最优解,但希望给你带来新思考

附demo地址:gitee.com/cysir/examp…

flutter版本:2.8

猜你喜欢

转载自juejin.im/post/7088223867017101343
今日推荐