Flutter desktop development - project engineering framework construction

Through this article, you will learn:

  1. A complete and production-ready Flutter project architecture;
  2. Family bucket experience of mvvm state management library GetX;

⚠️This article is the first signed article of the Rare Earth Nuggets Technology Community. Reprinting is prohibited within 14 days. Reprinting without authorization is prohibited after 14 days. Infringement must be investigated!

foreword

In the previous articles of this column, we implemented the screen of the desktop application 可定制的窗口化, 适配了多种分辨率and implemented the widget "Smart Island" . The previous articles can be regarded as some infrastructure construction. In this article, I will GetXbuild a mature and complete project architecture that can be put into production and development based on the state management library . This is also the basis for us to continue to develop desktop applications later.

Construction principles

The construction of the project framework is completely based on the GetX library. Although I have analyzed the advantages and disadvantages of GetX before, for our open source project, the "family bucket" library of GetX is perfect . At the same time, it will also provide some tips in the Windows development process that are different from mobile development.

Construction of GetX Family Bucket

Why do you say that GetX is a family bucket, because it can not only meet the state management of MVVM, but also meet: internationalization, routing configuration, network requests, etc., it is really convenient, and it is reliable in personal testing! GetX

1. Internationalization

GetX provides the top-level entry of the application GetMaterialApp. This control encapsulates Flutter MaterialApp. We only need to pass in the multilingual configuration according to the rules given by GetX. The configuration is also very simple, you only need to provide the map object declared by get in the class. The key of the Map is composed of the language code and the country and region , and there is no need to deal with events such as system locale changes.

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. Routing configuration

如果没有使用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的页面。
image.png 通过插件快捷创建之后我们可以得到:logic、state、view的分层结构,通过logic绑定数据和视图,并且实现数据驱动UI刷新。 image.png 当然,通过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文件,如下图: image.png

其他

There are also many details of application windowing, singleton, window effect interaction, etc., which are also necessary for the windows project framework, and it is also very important to improve its maintainability. For details, please refer to the previous article in this column: Flutter desktop practice .

Guess you like

Origin juejin.im/post/7159958059006033950