Desarrollo de escritorio Flutter: construcción del marco de ingeniería de proyectos

A través de este artículo, aprenderá:

  1. Una arquitectura de proyecto Flutter completa y lista para producción;
  2. Experiencia de grupo familiar de la biblioteca de administración de estado mvvm GetX;

⚠️Este artículo es el primer artículo firmado por la comunidad tecnológica de pepitas de tierras raras. Se prohíbe la reimpresión dentro de los 14 días. La reimpresión sin autorización está prohibida después de los 14 días. ¡Se debe investigar la infracción!

prefacio

En los artículos anteriores de esta columna, implementamos la pantalla de la aplicación de escritorio 可定制的窗口化e 适配了多种分辨率implementamos el widget "Smart Island" . Los artículos anteriores pueden considerarse como una construcción de infraestructura. En este artículo, construiré GetXuna arquitectura de proyecto madura y completa que se puede poner en producción y desarrollo basada en la biblioteca de gestión estatal . Esta es también la base para que sigamos desarrollando aplicaciones de escritorio más adelante.

Principios de construcción

La construcción del marco del proyecto se basa completamente en la biblioteca GetX. Aunque ya analicé las ventajas y desventajas de GetX antes, para nuestro proyecto de código abierto, la biblioteca "cubo familiar" de GetX es perfecta . Al mismo tiempo, también brindará algunos consejos en el proceso de desarrollo de Windows que son diferentes del desarrollo móvil.

Construcción del cubo de la familia GetX

¿Por qué dice que GetX es un cubo familiar, porque no solo puede cumplir con la gestión de estado de MVVM, sino también con: internacionalización, configuración de enrutamiento, solicitudes de red, etc., es realmente conveniente y confiable en pruebas personales? ! ObtenerX

1. Internacionalización

GetX proporciona la entrada de nivel superior de la aplicación GetMaterialApp, este control encapsula Flutter MaterialApp, solo necesitamos pasar la configuración multilingüe de acuerdo con las reglas dadas por GetX. La configuración también es muy simple, solo necesita proporcionar el objeto de mapa declarado por get en la clase. La clave del Mapa está compuesta por el código de idioma y el país y la región , y no hay necesidad de lidiar con eventos como cambios de configuración regional del sistema.

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. Configuración de enrutamiento

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

其他

También hay muchos detalles de aplicación de ventanas, singleton, interacción de efectos de ventana, etc., que también son necesarios para el marco del proyecto de Windows, y también es muy importante mejorar su mantenibilidad. Para obtener más información, consulte el artículo anterior en esta columna: Práctica de escritorio de Flutter .

Supongo que te gusta

Origin juejin.im/post/7159958059006033950
Recomendado
Clasificación