经过此篇文章,你将了解到:

  1. 完整且可投入生产的Flutter项目架构
  2. 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的页面。

Flutter桌面开发-项目工程化框架搭建
经过插件快捷创立之后咱们能够得到:logic、state、view的分层结构,经过logic绑定数据和视图,而且完成数据驱动UI改写。
Flutter桌面开发-项目工程化框架搭建
当然,经过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进步了健壮性,但在界说恳求目标的时候,往往会增加一些工作量,特别是对于小项目。

  1. 咱们能够先创立一个根底内容供给,完成通用装备;
/// 网络恳求基类,装备公共属性
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;
    });
  }
}
  1. 然后依照模块化去装备恳求,进步可维护性。
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_itinjectable来生成主动单例、工厂结构函数等类。优点是让代码更为简练牢靠,便于维护。下面举个萌友上报的例子,初始装备只需求在create中写入即可,然后事务方调用只需求运用GetIt.get<YouMengReport>().report()上报就行了。这便是一个十分完整的单例,运用维护都很便利。

/// 声明单例,而且主动初始化
@singleton(signalsReady: true)
class YouMengReport {
  /// 声明工厂结构函数,主动初始化的时候会主动自行create办法
  @factoryMethod
  create() {
    // 这儿能够做一些初始化工作
  }
  report() {}
}

json生成器

由于不支持反射,导致Flutter的json解析一向为人诟病。因此运用json_serializable会是一个不错的挑选,其原理是经过AOP注解,帮咱们生成json编码和解析。经过插件Json2json_serializable能够帮咱们主动生成dart文件,如下图:

Flutter桌面开发-项目工程化框架搭建

其他

还有很多运用窗口化、单例、窗口作用交互等的细节,也是属于windows项目结构必须的,进步其可维护性也是很重要的。具体不再赘述,可看本专栏之前文章:Flutter桌面实践。