likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 33

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

  1. 完整且可投入生产的Flutter项目架构;
  2. mvvm状态管理库GetX的全家桶式体验;

前言

在本专栏前面的几篇文章中,我们对桌面应用实现了可定制的窗口化适配了多种分辨率的屏幕,并且实现了小组件 “灵动岛”。前面的文章算是一些基础建设的搭建,这篇文章我将基于状态管理库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桌面实践

转载自:https://juejin.cn/post/7159958059006033950
评论
请登录