前言
我们在选择一种技术方案的时候,除了考虑技术方案本身的优势之外,相关的配套生态也是必不可少的,否则如果所有的功能都完全自己去开发,那么工作量是很庞大的,从企业的角度成本也是不可接受的。这里我们就从实际开发的角度梳理出一份技术栈方案。
场景
我们整理出相关的应用场景,并找出对于的组件进行支撑。
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开发需求。