文章
问答
冒泡
Flutter最小可用技术栈方案

前言
我们在选择一种技术方案的时候,除了考虑技术方案本身的优势之外,相关的配套生态也是必不可少的,否则如果所有的功能都完全自己去开发,那么工作量是很庞大的,从企业的角度成本也是不可接受的。这里我们就从实际开发的角度梳理出一份技术栈方案。

场景
我们整理出相关的应用场景,并找出对于的组件进行支撑。

UI组件库
在大部分应用中,UI界面是必不可少的,我们可以根据自己的目标效果来选择适合的UI组件库并在此基础上进行定制。
flutter SDK自带的 Material-UI
由于material-ui的风格,在国内用的比较少,所以一般会基于他来做定制修改,包括很多其他组件库也是基于这个来封装的。

腾讯TDesign
 https://tdesign.tencent.com/flutter/overview
基于腾讯tdesign风格的flutter组件库,本质也是对material的封装。

Antd Mobile Flutter 
https://github.com/trionesdev/triones-antd-mobile-flutter
Ant design 风格的组件库。目前还在开发中,主要组件已经有了,如果想用ant风格的,可以尝试一下。

依赖注入
注入已经实例化的对象
Getx
地址 https://github.com/jonataslaw/getx

全局状态管理
在整个应用允许过程中,进行状态共享。
GetxService
Getx 的 GetxService模块

本地存储方案
用于存储登录的token等需要持久化的信息
GetStorage
地址 https://github.com/jonataslaw/get_storage

网络请求
Dio
dio是用的比较多的网络请求库。
https://github.com/cfug/dio
GetConnect
Getx的GetConnect模块
GetConnect在使用的时候,我们需要对其进行封装。例如

abstract class TrionesGetConnect extends GetConnect {
  TrionesGetConnect() {
    _interceptor();
  }

  void onUnauthorized() {}

  void beforeRequest(Request request) {}

  void _interceptor() {
    httpClient.addRequestModifier<dynamic>((request) {
      beforeRequest(request);
      return request;
    });
    httpClient.addResponseModifier<dynamic>((request, response) {
      if (response.isOk) {
        return response;
      }
      if (response.statusCode == 401) {
        onUnauthorized();
      } else if (response.statusCode == 400) {
        throw TrionesError(
          code: response.statusCode.toString(),
          message: "请求参数错误",
        );
      } else if (response.statusCode == 460) {
        throw TrionesError(
          code: response.body["code"],
          message: response.body["message"],
        );
      } else if (response.statusCode == 500) {
        throw TrionesError(
          code: response.statusCode.toString(),
          message: "服务器异常,请稍后再试",
        );
      } else {
        throw TrionesError(
          code: response.statusCode.toString(),
          message: "服务器异常,请稍后再试",
        );
      }
      return response;
    });
  }

  List<dynamic?> _listParamsConvert(List<dynamic?> params) {
    return params.map((v) {
      if (v is DateTime) {
        return v.millisecondsSinceEpoch;
      } else if (v is List) {
        return _listParamsConvert(v);
      } else if (v is Map) {
        return _queryParamsConvert(v);
      }
      return v.toString();
    }).toList();
  }

  Map<String, dynamic?> _queryParamsConvert(Map<dynamic, dynamic?> params) {
    return params.map((k, v) {
      if (v is DateTime) {
        return MapEntry(k, v.millisecondsSinceEpoch);
      } else if (v is List) {
        return MapEntry(k, _listParamsConvert(v));
      } else if (v is Map) {
        return MapEntry(k, _queryParamsConvert(v));
      }
      return MapEntry(k, v.toString());
    });
  }

  @override
  void onInit() {
    _interceptor();
  }

  Response<T> responseProcess<T>(Response<T> res) {
    if (res.statusCode == null) {
      throw TrionesError(code: "500", message: "网络异常,请稍后再试");
    }
    return res;
  }

  @override
  Future<Response<T>> get<T>(
    String url, {
    Map<String, String>? headers,
    String? contentType,
    Map<String, dynamic>? query,
    Decoder<T>? decoder,
  }) {
    return super
        .get(
          url,
          headers: headers,
          contentType: contentType,
          query: _queryParamsConvert(query ?? {}),
          decoder: decoder,
        )
        .then(responseProcess);
  }

  @override
  Future<Response<T>> post<T>(
    String? url,
    dynamic body, {
    String? contentType,
    Map<String, String>? headers,
    Map<String, dynamic>? query,
    Decoder<T>? decoder,
    Progress? uploadProgress,
  }) {
    return super
        .post(
          url,
          body,
          headers: headers,
          contentType: contentType,
          query: _queryParamsConvert(query ?? {}),
          decoder: decoder,
          uploadProgress: uploadProgress,
        )
        .then(responseProcess);
  }

  @override
  Future<Response<T>> put<T>(
    String url,
    dynamic body, {
    String? contentType,
    Map<String, String>? headers,
    Map<String, dynamic>? query,
    Decoder<T>? decoder,
    Progress? uploadProgress,
  }) {
    return super
        .put(
          url,
          body,
          headers: headers,
          contentType: contentType,
          query: _queryParamsConvert(query ?? {}),
          decoder: decoder,
          uploadProgress: uploadProgress,
        )
        .then(responseProcess);
  }

  @override
  Future<Response<T>> patch<T>(
    String url,
    dynamic body, {
    String? contentType,
    Map<String, String>? headers,
    Map<String, dynamic>? query,
    Decoder<T>? decoder,
    Progress? uploadProgress,
  }) {
    return super
        .patch(
          url,
          body,
          headers: headers,
          contentType: contentType,
          query: _queryParamsConvert(query ?? {}),
          decoder: decoder,
          uploadProgress: uploadProgress,
        )
        .then(responseProcess);
  }

  @override
  Future<Response<T>> request<T>(
    String url,
    String method, {
    dynamic body,
    String? contentType,
    Map<String, String>? headers,
    Map<String, dynamic>? query,
    Decoder<T>? decoder,
    Progress? uploadProgress,
  }) {
    return super
        .request(
          url,
          method,
          body: body,
          contentType: contentType,
          headers: headers,
          query: _queryParamsConvert(query ?? {}),
          decoder: decoder,
          uploadProgress: uploadProgress,
        )
        .then(responseProcess);
  }

  @override
  Future<Response<T>> delete<T>(
    String url, {
    Map<String, String>? headers,
    String? contentType,
    Map<String, dynamic>? query,
    Decoder<T>? decoder,
  }) {
    return super
        .delete(
          url,
          headers: headers,
          contentType: contentType,
          query: _queryParamsConvert(query ?? {}),
          decoder: decoder,
        )
        .then(responseProcess);
  }
}


json序列化
在使用网络请求的时候,一般返回的是dynamic或者map类型的数据。如果需要转换成对象,我们可以通过json序列化库来进行转换。

json_serializable
地址 https://pub.dev/packages/json_serializable
在需要转换的class 上添加 @JsonSerializable() 注解,执行 `dart run build_runner build --delete-conflicting-outputs`,class 所在文件会生成对应的 .g.dart文件。
在开发过程中,需要持续生成的话,只需要执行 `dart run build_runner watch` 即可。

扫码
mobile_scanner
地址 https://pub.dev/packages/mobile_scanner


文件选择
image_picker
地址 https://pub.dev/packages/image_picker

启动页
在应用启动的时候,在进行初始化操作的时候,经常会出现白屏情况,这个时候,我们需要一个启动页来解决这个情况。在初始化完成之后,再跳转到具体页面。如果自定义需求,一般可以自己写一个启动页面,或者使用flutter_native_splash 来简单处理。


flutter_native_splash
地址 https://github.com/jonbhanson/flutter_native_splash

使用 
在 pubspec.yaml 添加配置

flutter_native_splash:
  android: true
  color: "#42a5f5"
  image: assets/images/splash.png
  android_12:
    color: "#42a5f5"
    image: assets/images/splash.png


执行命令

dart run flutter_native_splash:create

 

应用图标生成
android和ios 需要多个尺寸的icon图片,如果一个个去处理,工作量会比较大,我们可以通过插件来生成,这样只需要设计一个就可以了。
flutter_launcher_icons
地址 https://github.com/fluttercommunity/flutter_launcher_icons/

使用
在 pubspec.yaml 添加配置

flutter_launcher_icons:
  android: true # 是否为 Android 生成图标
  ios: true              # 是否为 iOS 生成图标
  image_path: "assets/images/logo.jpg" # 你的图标文件路径
  remove_alpha_ios: true 


执行如下命令,生成不同规格的icon图片

dart run flutter_launcher_icon

结语
通过以上的技术栈,我们足以支撑大部分场景的flutter开发需求。

flutter

关于作者

落雁沙
非典型码农
获得点赞
文章被阅读