滴滴开源新项目Unify:聚焦Flutter与原生通信难题,助力跨端应用落地

引言

在移动开发领域,移动跨端技术因其提效收益,逐渐成为业界趋势之一。Flutter 作为近年来热门的跨端技术,以高性能、自渲染、泛跨端著称,得到广泛应用。在滴滴国际化业务中,我们大量应用 Flutter。目前已在滴滴国际化外卖、滴滴国际化出行司机端等业务中大规模落地,整体交付提效 50%+,收益显著。在大规模 Flutter 跨端场景下,存量的原生业务与增量 Flutter 业务间的双向通信成为痛点问题。

为此,滴滴国际化外卖自研 Unify 框架,旨在解决大规模跨端落地场景下,Flutter 与原生模块之间的通信问题。Unify 通过平台无关的模块抽象、灵活的实现注入、自动代码生成等特性,为开发者提供高效、灵活、易用的 Flutter 混合通信能力。

基于 Unify,滴滴国际化外卖成功将 16+ 个原生平台能力,26+ 个原生业务能力高效导入 Flutter,并沉淀出 UniFoundation、UniBusiness 两套业务架构模式,有效支撑外卖业务从零到一实现 Flutter 跨端落地。同时,Unify 也在滴滴国际化出行司机端中推广落地,有效支撑了兄弟业务的大规模跨端落地。

目前,Unify 已作为滴滴开源项目,正式开源,欢迎大家试用、体验、star 支持!

背景

在跨端落地过程中,通常会保留原生实现,以迭代方式逐步试水跨端,先跑通模式,再逐渐扩大跨端落地规模。

在原生代码与 Flutter 代码并存前提下,面临一系列实际问题:

1. 大量原生 SDK 如何高效导入 Flutter?

2. 大量业务功能如何高效导入 Flutter?

3. Flutter 功能模块如何导出给原生?

此类 Flutter 与原生代码间的双向通信问题,我们统称为混合通信问题

针对这一问题,Flutter 官方提供了 Channel 通信方案,但在大规模落地场景下,该方案存在一系列不足:

  1. 手动解析参数引发异常:使用 Channel 需要手动解析调用参数,极易出错。当接口发生变化时,需重新适配也极易引入 Bug。该问题在线上经常出现,并且难以根治。

  1. 大规模导出难以维护:在大规模能力导出场景下,需要编写大量分支语句和硬编码,难以维护。

  2. 代码封装繁琐:Channel API 较为底层,开发者还需二次封装,才能提供给业务方便调用,这一过程较为繁琐。

除 Channel 外,Pigeon 是一个更加强大的解决方案。Pigeon 由 Google 推出,该方案基于代码生成技术,有效提升了工程质量,降低了接入成本。但通过实际使用,我们发现在大规模模块导出场景下,Pigeon 的开发效率还有进一步提升的空间。

基于这一背景,Unify 通过批量接口声明、批量模块生成,简化了工程复杂度,进一步提升了开发效率。同时,Unify 也逐渐演化出自身特色,比如更加符合开发者习惯的多工程文件组织方式。

Unify 介绍

Unify 由滴滴出行国际化外卖团队自研,目前已经广泛应用于滴滴国际化外卖及国际化出行业务,有力支撑了业务的 Flutter 化进程。

Unify 的亮点特性包括:

  • 平台无关的模块抽象: 允许开发者使用 Dart 语言声明与平台无关的模块接口与实体。

  • 灵活的实现注入: 开发者可以灵活地选择注入原生实现(Android/iOS)或 Flutter 实现。

  • 自动代码生成: 借助强大的代码生成引擎,Unify 可以自动生成 Flutter、Android、iOS 多平台下统一调用的 SDK。

下面是一个使用 Unify 声明原生模块的示例:

@UniNativeModule()
abstract class DeviceInfoService {Future<DeviceInfoModel> getDeviceInfo();
}

通过 Unify,上面的 Dart 接口可以自动映射到 Android、iOS、Flutter 平台,开发者只需在各平台下填入具体实现即可。在 Flutter 中使用时,调用方式就像普通的 Flutter 模块一样简单、直观:

DeviceInfoService.getDeviceInfo().then((deviceInfoModel) {print("${deviceInfoModel.encode()}");
});

Unify 的整体架构如下:

outside_default.png

Unify 核心概念

在进行混合通信开发时,典型场景包括

  • 将原生模块导出至 Flutter 调用

  • 将 Flutter 模块导出至原生调用

  • 在接口中传递复杂实体类

在 Unify 中定义了一系列核心概念,能够高效满足上述场景。以上场景分别对应于 UniNativeModule、UniFlutterModule、UniModel。

在具体使用时,开发者首先声明模块接口,接口声明使用 Dart 语言,以抽象类形式编写。接下来执行 Unify 代码生成器,生成器会分析接口声明,并通过代码生成技术,生成两部分实现:

  1. 实现注入接口:用于开发者注入实现逻辑

  1. 对于原生模块导出至 Flutter 场景,使用 UniNativeModule 声明模块,Unify 会在原生侧 (Android、iOS)生成注入接口。

  2. 对于 Flutter 模块导出至原生场景,使用 UniFlutterModule 声明模块,Unify 会在 Flutter 侧生成注入接口。

  1. 三端统一调用接口

  1. Unify 会在 Android(Java)、iOS(Objective-C)、Flutter(Dart)生成三端统一调用接口。

  2. 在任意一端,都能在对应语言下,使用同样的模块接口签名,调用导出能力。

  3. 值得一提的是,Unify 支持使用 UniModel 声明可嵌套实体类,在三端下也会生成对应实体类,开发者在任意一技术栈下都可操作实体类,由 Unify 抹平底层序列化、反序列化通信,大幅提升开发体验与质量。

整体流程如下图所示:

outside_default.png

具体来说:

概念描述举例

UniNativeModule

声明一个模块,该模块的实现在原生(Android/iOS)注入。

通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。

outside_default.png

UniFlutterModule

声明一个模块,该模块的实现在 Flutter 注入。

通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。

outside_default.png

UniModel

Unify 提供的模板注解之一,主要作用:

  • 创建自定义实体(Model/Entity)。

跨端传输时,可以把它的对象实体作为参数,

直接跨端发送。

outside_default.png


Getting Start

前面的介绍有些抽象,在本节中,我们将通过实际案例,看是如何将原生模块是导入 Flutter中,来进行介绍的。

在本节中,假设有一个系统信息 SDK,在 Android、iOS 下分别实现。现在我们需要对两端进行封装,向 Flutter 侧提供统一能力。基于 Unify,这一任务能够快速、简单、高效、高质量完成。

注:完整代码实现可于文末点击「阅读原文」查看。

Step1:模块声明

第一步,开发者需要对模块接口进行声明。在 Flutter 工程根目录下创建一个 interface 目录,所有 Unify 的模块声明均位于该目录中。

interface 下包含两个声明文件,均以 Dart 抽象类方式编写。

device_info_service.dart

声明原生模块

// device_info_service.dart
@UniNativeModule()
abstract class DeviceInfoService {/// 获取设备信息Future<DeviceInfoModel> getDeviceInfo();
}

@UniNativeModule 注解表示该模块的实现由原生侧提供。

device_info_model.dart

声明返回值 Model

// device_info_model.dart
@UniModel()
class DeviceInfoModel {/// 系统版本String? osVersion;/// 内存信息String? memory;/// 手机型号String? plaform;
}

@UniModel 注解表示这是一个跨平台的数据模型。

值得一提的是:

  1. Unify 并不限制接口参数的数量,并且参数支持基本类型、List/Map 容器(支持范型)以及实体类。

  1. 在 Unify 中,实体类支持任意嵌套。

  2. 通过 Unify 生成器,interface  中声明的实体类(UniModel)将同时生成 Android(Java)、iOS(Objective-C)、Flutter(Dart)实现代码,在任意一端下,开发者都以同样方式使用实体类,由 Unify 实现底层序列化、反序列化及透传。

Step2:执行 Unify 生成器

接口声明完成后,执行如下命令生成跨平台代码:

flutter pub run unify api\--input=`pwd`/interface \--dart_out=`pwd`/lib \--java_out=`pwd`/android/src/main/java/com/example/uninativemodule_demo \--java_package=com.example.uninativemodule_demo \--oc_out=`pwd`/ios/Classes \--dart_null_safety=true \--uniapi_prefix=UD

在命令中,指定了 interface 接口目录,Android、iOS 输出位置等配置信息。

在 2.1 节中说到,对于 UniNativeModule,将会生成两部分代码:

  1. 实现注入接口:

  1. Android:DeviceInfoService.java、DeviceInfoServiceRegister.java

  2. iOS:DeviceInfoService.h、DeviceInfoService.m

  1. 三端统一调用接口:

  1. Flutter:main.dart

  2. Android:MainActivity.java

  3. iOS:AppDelegate.m

注:代码文件源自 Unify/example/01_uninativemodule_demo

值得一提的是:

  1. 除了 Flutter 调用接口外,Unify 也会在 Android 和 iOS 工程内分别以 Java、Objective-C 生成双端调用接口。供开发者在任何一端下,都可以用同样的方法、同样的实体类进行调用。这对于跨端场景下的代码一致性来说,意义是巨大的,避免了跨端多技术栈下,模块抽象不一致的问题。

  2. 本例是将原生模块导入 Flutter,使用 UniNativeModule,在原生侧提供实现注入接口。如果是将 Flutter 模块导入原生,则使用 UniFlutterModule,将在 Flutter 侧提供注入接口。不论是 UniNativeModule 还是 UniFlutterModule,除了注入接口有区别外,上层的三端统一调用接口是完全一致的,这也体现了 Unify 平台无关的模块抽象的思想,这对于混合栈下的架构分层至关重要。

Step3:注入原生实现

有了实现注入接口,开发者根据接口分别补充 Android、iOS 端实现。关键代码如下:

Android 实现

public class DeviceInfoServiceImpl implements DeviceInfoService {@Overridepublic void getDeviceInfo(Result<DeviceInfoModel> result) {DeviceInfoModel model = new DeviceInfoModel();......result.success(model);}
}

iOS 实现

// DeviceInfoServiceVendor.h
@interface DeviceInfoServiceVendor : NSObject<DeviceInfoService>
@end// DeviceInfoServiceVendor.m
@implementation DeviceInfoServiceVendor
UNI_EXPORT(DeviceInfoServiceVendor)
......
#pragma mark - DeviceInfoService协议 实现
- (void)getDeviceInfo:(void(^)(DeviceInfoModel* result))success fail:(void(^)(FlutterError* error))fail {DeviceInfoModel *model = [DeviceInfoModel new];......success(model);
}
@end

对于完整代码,可参见文末「阅读原文」:

  • Android 平台实现:DeviceInfoServiceImpl.java

  • Android 平台注册实现:MainActivity.java

  • iOS 平台实现类:DeviceInfoServiceVendor.h、DeviceInfoServiceVendor.m

  • iOS 平台注册实现:AppDelegate.m

注:代码文件源自 Unify/example/01_uninativemodule_demo

Step4:在 Flutter 中调用

一切就绪! 在 Flutter 代码中,现在可以直接调用 Unify 封装的原生模块了:

模块调用

OutlinedButton(child: const Text("获取设备信息"),onPressed: () {DeviceInfoService.getDeviceInfo().then((deviceInfoModel) {setState(() {_platformVersion = "\n${deviceInfoModel.encode()}";});});},
),

效果截图

outside_default.png

至此,你已经成功通过 Unify 将一个原生模块导入并在 Flutter 中使用。就像调用 Flutter 模块一样简单、直观!

小结

通过这个示例,我们体验了 Unify 带来的价值:

  1. 统一模块声明: 在任何平台下,统一的模块接口声明,避免实现不一致

  1. UniModel: 支持跨平台透明传输的数据模型

  1. 相比 Flutter 原生 Channel 方式:

  1. 避免手动解析参数易出错

  2. Android、iOS 双端自动对齐

  3. 大量 Channel 自动生成,易于维护

  4. 复杂实体无缝序列化,降低管理成本

我们总结了如下决策流程,方便大家根据场景需要,选择 UniNativeModule、UniFlutterModule:

outside_default.png

Unify 核心原理

Unify 之所以能提升跨端通信的开发效率,关键在于 Unify 实现了一套多语言代码生成器,通过该生成器,能够自动解析开发者声明的 Dart 抽象接口,并自动生成三端注入、调用代码,将开发者从繁重的胶水代码中解脱出来。在本节中,介绍 Unify 底层代码生成原理,并介绍与同类方案的对比。

Dart 代码静态分析

我们选择 Dart 语言作为模块接口声明语言,并基于 Dart Analyzer 库,实现对接口声明的静态分析,将 Dart 源代码转换为 Dart AST。在 Unify 中,我们基于 Dart AST 定义了 Unify AST,这是一套适用于模块导出场景的简化 AST,特色为内置了对多语言(Java、Dart、Objective-C)代码生成的映射关系,保证了后续多语言代码生成器实现的简洁。

从开发者接口声明,通过 Dart Analyzer 库静态分析,到产出 Unify AST 的整体流程如下:

outside_default.png

Unify 多语言代码生成器

基于这套 Unify AST,Unify 自研了一套多语言代码生成器,能够基于一套 AST 同时生成多端、多语言代码(Java、Dart、Objective-C),这也是 Unify 高效开发的关键。

在 Unify AST 中,我们抽象了多种抽象语法节点,每种节点中,都包含对多种语言的生成映射关系:

Unify AST

outside_default.png

Unify AST 节点多语言映射

outside_default.png

基于 Unify AST,以 UniModel 为例,开发者声明的 UniModel 将被转换为 Model AST 实例:

outside_default.png

有了 Model AST,Unify 声明了 UniModel 在多端下的生成代码模板。在 Unify 中,我们自研了一套类似于 Flutter 组件化的代码生成模板语法,相较于其它框架手动拼接字符串的方式,Unify 代码生成模板结合 Unify AST 具备更高的模版编写效率,同时代码质量和可维护性更高。以 UniModel 为例,部分模版如图:

outside_default.png

Unify 代码生成器的作用是将 UniModel 的 Model AST 与各技术栈下的生成模版相结合,从而生成 UniModel 在各平台下的多语言实现。最终的生成代码如图:

outside_default.png

同类方案对比

Unify 通过平台无关的模块抽象、灵活的实现注入、自动代码生成等特性,为开发者提供高效、灵活、易用的 Flutter 混合通信能力。同时,Unify 也逐渐演化出自身特色,比如参数支持任意嵌套的实体类、集合类范型,以及贴近 Flutter 开发者的纯 Dart 语言的接口声明方式。

Unify 还支持批量接口声明、批量模块生成,简化了工程复杂度,进一步提升了开发效率。外卖大规模 Flutter 落地之初,面临数10+基础能力的批量导出,如果逐个搭建 Git 库导出,维护成本和导出成本过高。基于 Unify 的批量导出能力,我们在短时间内完成了对平台能力的批量封装。

基于前文的使用介绍、原理介绍,相信大家对 Unify 有了深入的了解。在本节中,我们将 Unify 与其它同类框架对比,帮助大家选型、决策。

outside_default.png

通过对比可以看出,不同方案各有特色,适合于不同的场景。概括来说,如果业务中有大量封装导出场景,Unify 能够实现更高的批量导出效率,同时保持了较低的工程复杂度,易于维护。如果是对单模块进行封装导出,或者需要支持更多语言,尤其是 C++ 封装支持,Pigeon 则是较好的选择。

Unify 业务最佳实践

在滴滴国际化外卖业务 Flutter 大规模落地的初期,面临十余个公司平台能力 SDK 需要导出的 Flutter 侧,同时业务中存在大量混合通信,需要保证高可靠性。基于这一背景,在调研已有方案后,我们自研了 Unify,解决了大量模块的批量导出问题。并且在此过程中,我们沉淀出两套架构模式 UniFoundation 和 UniBusiness,成为业务混合通信最佳实践。

UniFoundation 是我们基于 Unify,高效完成公司16+ 个 SDK 批量导出,形成一套能够在 Android、iOS、Flutter 三端统一调用的基建能力。UniFoundation 是一套可复用基建,支撑了国际化外卖商家端、用户端、骑手端三端 Flutter 大规模落地。同时,作为通用基建,UniFoundation 成功推广到国际化出行司机端,助力兄弟业务的 Flutter 大规模落地,并实现跨团队合作共建。

在 UniFoundation 落地之后,在各端业务中,也存在大量业务模块与 Flutter 之间混合通信的场景,于是我们沿用 UniFoundation 的模式延伸出 UniBusiness。UniBusiness 是业务端内部,基于 Unify 批量抽象出的平台无关的业务模块,能够在三端,以统一的方式实现模块调用、复杂实体透传。随着 Flutter 落地规模的扩大,有越来越多业务模块由 Flutter 实现,并经过 Unify 封装,实现三端统一调用。

UniFoundation 和 UniBusiness 在业务中多端落地如图所示:

outside_default.png

落地收益

滴滴国际化外卖业务包含用户端、骑手端、商家端三端,目前均已实现 Flutter 大规模业务落地,并且 Flutter 均已覆盖各端核心主流程,实现跨端复用,整体交付提效 50%+,收益显著,并且是一项持续性提效的收益。其中,国际化外卖骑手端 90%+ 以上代码均为 Flutter 跨端实现,已线上稳定运行两年多时间。

目前,Unify 已成为滴滴 DiFlutter 技术体系的核心架构组件之一,稳定支撑着各端业务,并在业务中大量使用,解决了基础模块、业务模块的混合通信问题,彻底解决了由 Channel 通信导致的参数手动解析错误、Android/iOS 双端接口抽象不一致等问题。

滴滴国际化外卖 Flutter 部分业务落地场景展示:

outside_default.png

outside_default.png

总结与未来展望

滴滴国际化外卖在完成大规模 Flutter 跨端落地之后,我们意识到 Flutter 跨端仍然存在进一步提效空间,目前在向纯 Flutter 化方向演进。对于未来 Unify 的演进,我们希望将 Unify 打造成一套 Flutter 混合开发领域的标准化解决方案,帮助业务解决 Flutter 大规模落地过程中的痛点难点问题。

目前,Unify 已经完成混合通信能力的沉淀,未来我们将持续迭代,提供更多功能,让跨端混合通信开发更加高效、可靠。今年上半年,我们也调研了 Flutter PlatformView 嵌原生能力,目前 Unify 正在提供一套基于嵌原生的混合路由方案,解决大规模 Flutter 落地场景下的混合页面跳转问题。

新的混合路由相较于业界已有方案,更加轻量化,大幅降低复杂度。我们希望这套路由能够助力业务,向纯 Flutter 化方向演进、过渡。经过多年验证稳定后,我们也荣幸得将 Unify 作为滴滴官方开源项目,将这套实践分享给业内同行。欢迎大家试用、体验、star 支持!

国际化外卖技术团队正在招聘服务端高级开发工程师、高级数据研发工程师,感兴趣的小伙伴欢迎联系ginasun@didiglobal.com,期待你的加入!


更多项目开源信息,欢迎点击「阅读原文」了解!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/402612.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【大模型部署及其应用 】RAG检索技术和生成模型的应用程序架构:RAG 使用 Meta AI 的 Llama 3

目录 RAG检索技术和生成模型的应用程序架构1. **基本概念**2. **工作原理**3. **RAG的优势**4. **常见应用场景**5. **RAG的挑战**6. **技术实现**参考RAG 使用 Meta AI 的 Llama 3亲自尝试运行主笔记本与文档应用聊天关键架构组件1. 自定义知识库2. 分块3. 嵌入模型4. 矢量数据…

PHP多商家营销活动平台系统小程序源码

解锁营销新境界&#xff01;「多商家营销活动平台」让你的品牌火出圈✨ &#x1f680;【聚合力量&#xff0c;共创辉煌】&#x1f680; 在这个竞争激烈的市场中&#xff0c;单打独斗早已不是最佳选择&#xff01;「多商家营销活动平台」横空出世&#xff0c;它像一座桥梁&…

关于Python3项目中依赖包管理问题

背景&#xff1a;最近在使用Python3.11编写脚本来获取google play中app的用户评论&#xff0c;脚本中需要安装多个依赖包&#xff0c;在本地Pycharm调试通过以后&#xff0c;上传到github&#xff0c;然后在linux服务器拉取脚本来运行&#xff0c;发现存在几个问题。本文将面临…

Qt登录窗口设计

widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QIcon> //图标类 #include <QPushButton> #include <QLineEdit> //行编辑 #include <QLabel> #include <QTextEdit> #include <QMovie>class Widge…

django中的MESSAGE组件

文章目录 message组件1 使用配置2 设置值3 读取值4 源码分析 message组件 1 使用配置 INSTALLED_APPS [# django.contrib.admin,# django.contrib.auth,# django.contrib.contenttypes,# django.contrib.sessions,django.contrib.messages,django.contrib.staticfiles,"…

vuex的原理和使用方法

简介 Vuex 是 Vue.js 应用的状态管理模式&#xff0c;它为应用内的所有组件提供集中式的状态&#xff08;数据&#xff09;管理。可以帮我们管理 Vue 通用的数据 (多组件共享的数据)。 Vuex的构成 state&#xff1a;state 是 Vuex 的数据中心&#xff0c;也就是说state是用来…

修改系统启动环境变量

修改系统启动环境变量 查看uboot默认env 首先连接好开发板的串口终端&#xff0c;在开发板上后&#xff0c;一直快速短按 空格键 即可进入 uboot的 shell 交互命令行内。在命令行内输入 print 命令&#xff0c;可以看到当前系统的所有环境变量。 > print aw-ubi-spinand…

[DL]深度学习_针对图像恢复的高效扩散模型DiffIR

DiffIR: Efficient Diffusion Model for Image Restoration Abstract 扩散模型(DM)通过将图像合成过程建模为去噪网络的顺序应用&#xff0c;实现了SOTA的性能。然而&#xff0c;与图像合成不同的是&#xff0c;图像恢复(IR)对生成符合ground-truth的结果有很强的约束。因此&am…

【Linux基础】Linux中的开发工具(1)--yum和vim

目录 ✈️前言一&#xff0c;Linux 软件包管理器 yum1. 什么是软件包2. 如何安装软件3. 如何卸载软件 二&#xff0c;Linux编辑器-vim使用1. vim的基本概念1.1 命令/正常/普通模式1.2 插入模式1.3 底行模式 三&#xff0c;vim命令模式命令集1. 移动光标2. 删除字符3. 复制4. 替…

用python制作88键赛博钢琴(能用鼠标键盘进行弹奏)

用python制作88键赛博钢琴 前言 恭喜这位博主终于想起了自己的账号密码&#xff01; 时光荏苒&#xff0c;转眼间已逾一年未曾在此留下墨香。尽管这一年间&#xff0c;博主投身于无尽的忙碌与挑战之中&#xff0c;但令人欣慰的是&#xff0c;那份初心与热情似乎并未因岁月的流…

Django后台数据获取展示

​ 续接Django REST Framework&#xff0c;使用Vite构建Vue3的前端项目 1.跨域获取后台接口并展示 安装Axios npm install axios --save 前端查看后端所有定义的接口 // 访问后端定义的可视化Api接口文档 http://ip:8000/docs/ // 定义的学生类信息 http://ip:8000/api/v1…

Ubuntu下交叉编译器工具链的安装方法

本篇文章记录Ubuntu下交叉编译器工具链的安装方法。 目录 一、交叉编译器 1、交叉编译器简介 2、获取交叉编译器 3、安装交叉编译器 4、安装相关库 二、结语 一、交叉编译器 1、交叉编译器简介 交叉编译器是一种编译器&#xff0c;它在一种平台上运行&#xff0c;但生成…

如何获取VS Code扩展的版本更新信息

获取VS Code 扩展的版本更新的需求 因为企业内部有架设私有扩展管理器的要求&#xff0c;但是对于一些官方市场的插件&#xff0c;希望可以自动获取这些扩展的更新并上传至私有扩展管理器。于是就有了本篇介绍的需求&#xff1a; 通过API的方式获取VS Code 扩展的更新。 关于…

HarmonyOS Next 系列之列表下拉刷新和触底加载更多数据实现(十一)

系列文章目录 HarmonyOS Next 系列之省市区弹窗选择器实现&#xff08;一&#xff09; HarmonyOS Next 系列之验证码输入组件实现&#xff08;二&#xff09; HarmonyOS Next 系列之底部标签栏TabBar实现&#xff08;三&#xff09; HarmonyOS Next 系列之HTTP请求封装和Token…

STM32入门开发操作记录(九)——外部时钟定时器

目录 一、项目准备1. 工程模板2. 器件接线 二、外部时钟1. 端口复用2. 流程示意 三、定时器模块Timer.cTimer.h 四、遮光计数 一、项目准备 1. 工程模板 本篇项目所用模板包含以下模块&#xff0c;声明函数见头文件&#xff0c;模块添加和函数功能详见往期记录。   2. 器件…

Python之格式化输出

格式化输出 方法一&#xff1a;用%方法二&#xff1a;用format()函数设置输出的内容的宽度和小数位数 方法一&#xff1a;用% 直接用print()函数对字符串进行输出&#xff0c;是没有进行格式化控制的。 格式化&#xff0c;是对输出内容的显示方式进行设置。 首先&#xff0c;要…

小程序滑动单元格

项目场景&#xff1a;小程序用户管理列表&#xff0c;通过单元格滑动实现“密码重置”、“删除”功能。 技术框架&#xff1a;uniapp、uview3、ts 效果如下&#xff1a; 前端页面&#xff1a; <template><view class"fui-wrap"><view class"f…

Spring Boot 的Web开发

Spring Boot 的Web开发 一、 静态资源映射规则 总结&#xff1a; 只要静态资源放在类路径下&#xff1a; called /static (or /public or /resources or /METAINF/resources 访问 &#xff1a; 当前项目根路径/ 静态资源名 二、 enjoy模板引擎 Enjoy模板引擎是一个轻量级的…

数据结构-常见的七大排序

上节中我们学习了七大排序中的五种(插入排序、希尔排序、堆排序、选择排序、交换排序) 数据结构-常见的七大排序-CSDN博客 这节我们将要学习快速排序(hoare、指针法、挖洞法(快排的延伸)、快速排序非递归(栈)) 1.快速排序 1.1 hoare法 1.1思路 1.选出一个key&#xff0c;一…

浅看MySQL数据库

有这么一句话&#xff1a;“一个不会数据库的程序员不是合格的程序员”。有点夸张&#xff0c;但是确是如此。透彻学习数据库是要学习好多知识&#xff0c;需要学的东西也是偏难的。我们今天来看数据库MySQL的一些简单基础东西&#xff0c;跟着小编一起来看一下吧。 什么是数据…