经过此篇文章,你将了解到:
- 完整且可投入生产的Flutter项目架构;
- mvvm状况办理库GetX的全家桶式体会;
⚠️本文为稀土技能社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言
在本专栏前面的几篇文章中,咱们对桌面运用完成了可定制的窗口化
,适配了多种分辨率
的屏幕,而且完成了小组件 “灵动岛”。前面的文章算是一些根底建设的树立,这篇文章我将根据状况办理库GetX
,树立一个老练完善的,可投入生产开发的项目架构。这也是咱们后边继续开发桌面运用的根底。
树立准则
此次项目结构的树立,彻底根据GetX库。虽说之前我也分析过GetX的优势和坏处,但对于咱们一个开源的项目,GetX这种“全家桶”库再合适不过啦。一起还会供给在Windows开发过程中一些区别于移动端开发的小技巧。
GetX全家桶的树立
为啥说GetX是全家桶,由于它不仅能够满足MVVM的状况办理,还能满足:国际化、路由装备、网络恳求等等,着实便利,而且亲测牢靠!GetX
1. 国际化
GetX供给了运用的顶层入口GetMaterialApp
,这个控件封装了Flutter的MaterialApp
,咱们只需求依照GetX给定的规则传入多言语的装备即可。装备也是十分简单的,只需求在类中供给get声明的map目标即可。Map的key由言语的代码和国家地区组成,无需处理体系言语环境改变等事情。
import 'package:get/get.dart';
class Internationalization extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'en_US': {
'appName': 'Flutter Windows',
'hello':'Hello World!'
},
'zh_CN': {
'appName': 'Flutter桌面运用',
'hello':'你好,国际!'
},
'zh_HK': {
'appName': 'Flutter桌面應用',
'hello':'你好,国际!'
},
};
}
2. 路由装备
如果没有运用GetX,路由办理很大情况是运用Fluro,大量的define、setting、handle真的装备的很枯燥。在GetX中,你只需求装备路由称号和对应的Widget即可。
class RouteConfig {
/// home模块
static const String home = "/home/homePage";
/// 我的模块
static const String mine = "/mine/myPage";
static final List<GetPage> getPages = [
GetPage(name: home, page: () => HomePage()),
GetPage(name: mine, page: () => MinePage()),
];
}
至于参数,能够直接像web端的url一样,运用?、&
传递。
一起GetX也供给了路由跳转的办法,相比Flutter Navigator2供给的api,GetX的路由跳转显着愈加便利,能够脱离context进行跳转,咱们能够在VM层随意处理路由,这点真的很爽。
// 跳转到我的页面
Get.toNamed('${RouteConfig.mine}?userId=123&userName=karl');
// 我的页面接纳参数
String? userName = Get.parameters['userName'];
3. GetX状况办理
状况办理才是GetX的重头戏,GetX中完成的Obx机制,能十分轻量级的帮咱们定点改写。Obx是经过创立定向的Stream,来部分setState的。而且作者还供给了ide的插件,咱们来创立一个GetX的页面。
经过插件快捷创立之后咱们能够得到:logic、state、view的分层结构,经过logic绑定数据和视图,而且完成数据驱动UI改写。
当然,经过Obx的办法会触发创立较多的Stream,有时运用update()来主动改写也是能够的。
关于GetX的状况办理,有个细节要提示下:
- 如果listview.build下的item都有自己的状况办理,那么每个item需求向logic传递自己的tag才干产生各自的Obx stream;
Get.put(SwiperItemLogic(), tag: model.key);
GetX相对其他的状况办理,最重点是根据Stream完成了真正的跨组件通讯,包括兄弟组件;只需求确保logic层Put一次,其余组件去Find即可直接更新logic的值,完成视图改写。
4. 网络恳求
在网络恳求上,GetX的封装其实并没有dio来的好,Get_connect插件集成了REST API恳求和GraphQL规范,咱们开发过程中其实不会两者都用。尽管GraphQL进步了健壮性,但在界说恳求目标的时候,往往会增加一些工作量,特别是对于小项目。
- 咱们能够先创立一个根底内容供给,完成通用装备;
/// 网络恳求基类,装备公共属性
class BaseProvider extends GetConnect {
@override
void onInit() {
super.onInit();
httpClient.baseUrl = Api.baseUrl;
// 恳求阻拦
httpClient.addRequestModifier<void>((request) {
request.headers['accept'] = 'application/json';
request.headers['content-type'] = 'application/json';
return request;
});
// 响应阻拦;甚至已经把http status都帮咱们区分好了
httpClient.addResponseModifier((request, response) {
if (response.isOk) {
return response;
} else if (response.unauthorized) {
// 账户权限失效
}
return response;
});
}
}
- 然后依照模块化去装备恳求,进步可维护性。
import 'package:get/get.dart';
import 'base_provider.dart';
/// 依照模块去制定网络恳求,数据源模块化
class HomeProvider extends BaseProvider {
// get会带上baseUrl
Future<Response> getHomeSwiper(int id) => get('home/swiper');
}
日志记载
日志咱们采用Logger进行记载,桌面端一般运用txt文件格式。以时刻命名,天为单位树立日志文件即可。如果有需求,也能够加一些守时清理的逻辑。
咱们需求重写下LogOutput
的办法,把颜色和表情都去掉,避免编码过错,然后完成下单例。
Logger? logger;
Logger get appLogger => logger ??= Logger(
filter: CustomerFilter(),
printer: PrettyPrinter(
printEmojis: false,
colors: false,
methodCount: 0,
noBoxingByDefault: true),
output: LogStorage(),
);
class LogStorage extends LogOutput {
// 默认的日志文件过期时刻,以小时为单位
static const _logExpiredTime = 72;
/// 日志文件操作目标
File? _file;
/// 日志目录
String? logDir;
/// 日志称号
String? logName;
LogStorage({this.logDir, this.logName});
@override
void destroy() {
deleteExpiredLogs(_logExpiredTime);
}
@override
void init() async {
deleteExpiredLogs(_logExpiredTime);
}
@override
void output(OutputEvent event) async {
_file ??= await createFile(logDir, logName);
String now = CommonUtils.formatDateTime(DateTime.now());
String version = packageInfo.version;
_file!.writeAsStringSync('>>>> $version $now [${event.level.name}]\n',
mode: FileMode.writeOnlyAppend);
for (var line in event.lines) {
_file!.writeAsStringSync('${line.toString()}\n',
mode: FileMode.writeOnlyAppend);
debugPrint(line);
}
}
Future<File> createFile(String? logDir, String? logName) async {
logDir = logDir;
logName = logName;
if (logDir == null) {
Directory documentsDirectory = await getApplicationSupportDirectory();
logDir =
"${documentsDirectory.path}${Platform.pathSeparator}${Constants.logDir}";
}
logName ??=
"${CommonUtils.formatDateTime(DateTime.now(), format: 'yyyy-MM-dd')}.txt";
String path = '$logDir${Platform.pathSeparator}$logName';
debugPrint('>>>>日志存储途径:$path');
File file = File(path);
if (!file.existsSync()) {
file = await File(path).create(recursive: true);
}
return file;
}
吐司提示
吐司用的仍是fluttertoast的办法。但是windows的完成比较不一样,在windows上的完成toast提示只能显示在运用窗体内。
static FToast fToast = FToast().init(Get.overlayContext!);
static void showToast(String text, {int? timeInSeconds}) {
// 桌面版必须运用带context的FToast
if (Platform.isWindows || Platform.isMacOS) {
cancelToastForDesktop();
fToast.showToast(
toastDuration: Duration(seconds: timeInSeconds ?? 3),
gravity: ToastGravity.BOTTOM,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25.0),
color: const Color(0xff323334),
),
child: Text(
text,
style: const TextStyle(
fontSize: 16,
color: Colors.white,
),
),
),
);
} else {
cancelToast();
Fluttertoast.showToast(
msg: text,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: timeInSeconds ?? 3,
backgroundColor: const Color(0xff323334),
textColor: Colors.white,
fontSize: 16,
);
}
}
一些的小技巧
代码注入,更简练的完成单例和结构引证
在开发过程中,我还会运用get_it
和injectable
来生成主动单例、工厂结构函数等类。优点是让代码更为简练牢靠,便于维护。下面举个萌友上报的例子,初始装备只需求在create中写入即可,然后事务方调用只需求运用GetIt.get<YouMengReport>().report()
上报就行了。这便是一个十分完整的单例,运用维护都很便利。
/// 声明单例,而且主动初始化
@singleton(signalsReady: true)
class YouMengReport {
/// 声明工厂结构函数,主动初始化的时候会主动自行create办法
@factoryMethod
create() {
// 这儿能够做一些初始化工作
}
report() {}
}
json生成器
由于不支持反射,导致Flutter的json解析一向为人诟病。因此运用json_serializable
会是一个不错的挑选,其原理是经过AOP注解,帮咱们生成json编码和解析。经过插件Json2json_serializable能够帮咱们主动生成dart文件,如下图:
其他
还有很多运用窗口化、单例、窗口作用交互等的细节,也是属于windows项目结构必须的,进步其可维护性也是很重要的。具体不再赘述,可看本专栏之前文章:Flutter桌面实践。