Flutter下的一点实践

article/2025/8/23 2:40:49

目录

  • 1、背景
  • 2、refena创世纪代码
  • 3、localsend里refena的刷新
    • 3.1 初始状态
    • 3.2 发起设备扫描流程
    • 3.3 扫描过程
    • 3.3 刷新界面
  • 4.localsend的设备扫描流程
    • 4.1 UDP广播设备注册流程
    • 4.2 TCP/HTTP设备注册流程
    • 4.3 localsend的服务器初始化工作
    • 4.4总结

1、背景

在很久以前,我曾经经历了一小段时间的Flutter开发,当时Flutter的版本才迭代到1.0,在做一个短视频应用的开发中,我曾经产生了一个巨大的疑问,就是Flutter的状态刷新怎么才能简洁、高效,如果每次及每个地方都使用setstate()来刷新界面确实显得非常笨重。
这么多年过去了,就像一个回旋镖一样,我又进了一个Flutter/kotlin混合开发的项目,项目是在开源的localsend项目上做二次开发,localsend作为跨平台传输软件,可以实现在同一局域网的端到端设备之间共享文件。本篇博客将以最简单的方式介绍refena创世纪代码、localsend里refena的刷新、localsend的设备扫描流程。

2、refena创世纪代码

由于采用了Flutter作为开发框架,状态管理必不可少,相比于市面常见的Flutter状态管理框架async_redux,localsend采用了不太常用的状态管理框架refena。作为被async_redux启迪的状态管理框架,redux中常见的storestateactionsreducers同样适用。store保存了应用里所有的状态,而store被保存在各种provider里;action被保存在自身的reducers里,触发store状态发生变化的唯一办法是发送一个action。有关redux的工作流及基础概念可以参考这篇文章以及下面这张图:
在这里插入图片描述

refena官网有最小化的状态刷新介绍,先搬运介绍一下refena的创世纪代码:

final counterProvider = ReduxProvider<ReduxCounter, int>((ref) => Counter());//1.在refena中,notifier用以保存实际状态,并且可以触发监听它们的控件刷新
//2.init()方法可以定义Notifier初始化状态
//3.可以通过ref来获取定义的其他provider
//4.这里定义counter的初始状态:10
class ReduxCounter extends ReduxNotifier<int> {int init() => 10;
}//1.ReduxAction最重要的方法就是reduce()方法,用于向provider返回一个新的状态
//2.这里返回的状态为ReduxCounter现在的值加上传递过来的值
class AddAction extends ReduxAction<ReduxCounter, int> {final int amount;AddAction(this.amount);int reduce() => state + amount;
}class MyPage extends StatelessWidget {Widget build(BuildContext context) {int counterState = context.watch(counterProvider);return Scaffold(body: Center(child: Text('Counter state: $counterState'),),floatingActionButton: FloatingActionButton(//点击触发action dispatchonPressed: () => context.redux(counterProvider).dispatch(AddAction(2)),child: const Icon(Icons.add),),);}
}

总结起来,在refena下的工作流为:1、定义初始状态;2、重写reduce()方法,在ReduxAction或各个Action子类中定义要改变的状态;3、定义状态的触发条件,调用dispatch方法触发状态改变。总的来说,refena状态刷新其实和MVVM有许多相似之处。

3、localsend里refena的刷新

localsend典型的功能如下:两个接入同一个局域网的设备相互发送UDP广播或HTTP请求,确立连接之后通过HTTP协议来传输文件。下面将简单介绍localsend扫描到设备后的界面刷新流程。

3.1 初始状态

localsend的初始状态如下:
在这里插入图片描述
打开应用,进入发送页签,在“附件的设备”这个列表下开始扫描局域网内的设备。发送页签进行初始化工作,并且发送了一个全局异步的action——SendTabInitAction:

class SendTab extends StatelessWidget {const SendTab();Widget build(BuildContext context) {return ViewModelBuilder(provider: sendTabVmProvider,//依然是在init方法里分发初始化actioninit: (context) => context.global.dispatchAsync(SendTabInitAction(context)),......

这个SendTabInitAction的代码十分简单:

class SendTabInitAction extends AsyncGlobalAction {……Future<void> reduce() async {//从provider里边读取是否有扫描到的设备final devices = ref.read(nearbyDevicesProvider).devices;if (devices.isEmpty) {//如果没有设备触发设备扫描流程await dispatchAsync(StartSmartScan(forceLegacy: false));}}
}

根据refena工作流,发起一个action后,会直接调用它的reduce方法,在reduce方法里产生新的状态,并通过各种方式把这个新的状态同步给widget刷新界面。所以,设备是如何扫描出来的只需要跟进StartSmartScan这个action即可。

3.2 发起设备扫描流程

接下来就进入了localsend代码的核心——扫描设备流程,这个流程较为复杂,包括dart下高级网络编程及refena线程间通信等,这里只给出扫描开始及获取到扫描结果的伪代码:

//英文注释为原生代码注释
class StartSmartScan extends AsyncGlobalAction {static const maxInterfaces = 15;final bool forceLegacy;Future<void> reduce() async {// 1.Try performant Multicast/UDP method first//首先发起UDP广播,UDP广播性能比TCP/HTTP性能高。ref.redux(nearbyDevicesProvider).dispatch(StartMulticastScan());// At the same time, try to discover favorites//首先从文件里读取是否有收藏的设备final favorites = ref.read(favoritesProvider);final https = ref.read(settingsProvider).https;await ref.redux(nearbyDevicesProvider).dispatchAsync(StartFavoriteScan(devices: favorites, https: https));……// 2.If no devices has been found, then switch to legacy discovery mode// which is purely HTTP/TCP based.final stillEmpty = ref.read(nearbyDevicesProvider).devices.isEmpty;final stillInSendTab =ref.read(homePageControllerProvider).currentTab == HomeTab.send;if (forceLegacy || (stillEmpty && stillInSendTab)) {final networkInterfaces =//localIpProvider保存了从platformChannel里读取到的当前设备的IP//(这里解释一下为什么当前设备的IP需要单独用一个localIpProvider来保存,//在android设备里,根据底软实现可能有多个网卡,对应也有多个设备IP)//依然首先从provider(内存)里读取是否之前扫描到过设备ref.read(localIpProvider).localIps.take(maxInterfaces).toList();if (networkInterfaces.isNotEmpty) {//开始扫描当前设备所有IP所在局域网的设备await dispatchAsync(StartLegacySubnetScan(subnets: networkInterfaces));}} else {……}}
}

3.3 扫描过程

扫描代码主体流程主要分为两个步骤:1.通过UDP协议扫描局域网设备;2.通过TCP/HTTP协议扫描设备。扫描流程较为复杂,需要对网络编程有一定基础的了解。我们跳过具体的扫描流程,直接到获取扫描结果的部分。

//英文注释为localsend原生注释
/// HTTP based discovery on a fixed set of subnets.
class StartLegacySubnetScan extends AsyncGlobalAction {……Future<void> reduce() async {//读取配置信息,端口号、是否是HTTPS协议等final settings = ref.read(settingsProvider);final port = settings.port;final https = settings.https;// send announcement in parallel//发起设备扫描流程——UDP组播ref.redux(nearbyDevicesProvider).dispatch(StartMulticastScan());await Future.wait<void>([for (final subnet in subnets)ref.redux(nearbyDevicesProvider).dispatchAsync(//发起设备扫描流程——TCP/HTTP请求StartLegacyScan(port: port, localIp: subnet, https: https)),]);……}
}

/// It does not really "scan".
/// It just sends an announcement which will cause a response on every other LocalSend member of the network.
class StartMulticastScanextends ReduxAction<NearbyDevicesService, NearbyDevicesState> {NearbyDevicesState reduce() {external(notifier._isolateController)//开启线程发起UDP组播.dispatch(IsolateSendMulticastAnnouncementAction());return state;}
}
/// Scans one particular subnet with traditional HTTP/TCP discovery.
/// This method awaits until the scan is finished.
class StartLegacyScanextends AsyncReduxAction<NearbyDevicesService, NearbyDevicesState> {final int port;final String localIp;final bool https;……Future<NearbyDevicesState> reduce() async {……// 1.Scan all IP addresses on the WLANfinal stream = external(notifier._isolateController)//开启线程发起TCP/HTTP协议设备扫描流程,扫描到的设备保存在Stream里.dispatchTakeResult(IsolateInterfaceHttpDiscoveryAction(networkInterface: localIp,port: port,https: https,));// 2.Register the device to the RegisterDeviceActionawait for (final device in stream) {//将stream里的设备赋值RegisterDeviceAction//扫描过的设备保存到nearbyDevicesProviderawait dispatchAsync(RegisterDeviceAction(device));}……}
}

3.3 刷新界面

/// Registers a device in the state.
/// It will override any existing device with the same IP.
class RegisterDeviceActionextends AsyncReduxAction<NearbyDevicesService, NearbyDevicesState> {……Future<NearbyDevicesState> reduce() async {……//在RegisterDeviceAction的reduce方法里触发界面刷新var nearbyDevicesState = state.copyWith(devices: {...state.devices}..update(device.ip, (_) => device, ifAbsent: () => device),);……}
}

这样,通过扫描设备流程就将扫描到的设备更新到了界面上:
在这里插入图片描述

4.localsend的设备扫描流程

4.1 UDP广播设备注册流程

在前文里,我们已经提到设备扫描首先会以UDP广播来扫描设备,先来看看代码实现。

  //英文注释为原生代码注释/// Binds the UDP port and listen to UDP multicast packages/// It will automatically answer announcement messagesStream<Device> startListener() async* {……final sockets = await _getSockets(syncState.multicastGroup, syncState.port);//遍历UDP组播地址的所有Socketfor (final socket in sockets) {//开始监听是否有组播socket.socket.listen((_) {final datagram = socket.socket.receive();……try {//将Socket数据转换为对象final dto = MulticastDto.fromJson(jsonDecode(utf8.decode(datagram.data)));if (dto.fingerprint == syncState.securityContext.certificateHash) {return;}……if ((dto.announcement == true || dto.announce == true) && syncState.serverRunning) {// only respond when server is running//向UDP组播广播方返回应答消息//这里业务逻辑为UDP设备注册流程//广播发送方作为server//广播应答方作为client_answerAnnouncement(peer);}} catch (e) {……}});}// Tell everyone in the network that I am online//向UDP组播地址所有成员发送UDP组播,此举可以提供设备扫描成功率sendAnnouncement(); // ignore: unawaited_futuresyield* streamController.stream;}/// Responds to an announcement.Future<void> _answerAnnouncement(Device peer) async {try {// Answer with TCP//通过dio向广播发送方发起一路HTTP请求,这里的请求接口为设备注册接口await _ref.read(dioProvider).discovery.post(ApiRoute.register.target(peer),data: _getRegisterDto().toJson(),);} catch (e) {……}}/// Sends an announcement which triggers a response on every LocalSend member of the network.//发送一个广播,在网络的每个localSend成员上触发应答广播消息Future<void> sendAnnouncement() async {final syncState = _ref.read(syncProvider);final sockets = await _getSockets(syncState.multicastGroup);final dto = _getMulticastDto(announcement: true);//分别以100ms、500ms、2000ms向发送方应答组播消息for (final wait in [100, 500, 2000]) {……for (final socket in sockets) {try {socket.socket.send(dto, InternetAddress(syncState.multicastGroup), syncState.port);socket.socket.close();} catch (e) {……}}}……}Future<List<_SocketResult>> _getSockets(String multicastGroup, [int? port]) async {//通过各个平台的platformChannel获取当前设备的IP(android设备通常是SoftAP IP)final interfaces = await NetworkInterface.list();final sockets = <_SocketResult>[];for (final interface in interfaces) {try {//根据IP地址绑定到localsend预先定义的端口号上,返回一个Socket端点final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port ?? 0);//把这个Socket加入到UDP组播地址里socket.joinMulticast(InternetAddress(multicastGroup), interface);……} catch (e) {……}}return sockets;
}

4.2 TCP/HTTP设备注册流程

UDP设备流程结束之后,才会走到HTTP设备注册流程。HTTP设备注册流程资源占用率比UDP广播这种方式大得多。

class HttpScanDiscoveryService {……//参数名networkInterface代码当前设备的IP地址。如192.168.31.246Stream<Device> getStream({required String networkInterface, required int port, required bool https}) {//遍历197.168.31.0~197.168.31.255里的所有IP,尝试向这里面的每个IP地址发起一路HTTP请求final ipList = List.generate(256, (i) => '${networkInterface.split('.').take(3).join('.')}.$i').where((ip) => ip != networkInterface).toList();_runners[networkInterface]?.stop();_runners[networkInterface] = TaskRunner<Device?>(initialTasks: List.generate(ipList.length,//发起设备注册请求(index) => () async => _doRequest(ipList[index], port, https),),concurrency: 50,);return _runners[networkInterface]!.stream.where((device) => device != null).cast<Device>();}Future<Device?> _doRequest(String currentIp, int port, bool https) async {……final device = await _targetedDiscoveryService.state.discover(ip: currentIp,port: port,https: https,onError: null,);……}return device;}

4.3 localsend的服务器初始化工作

前面讲到了localsend的界面刷新、设备注册流程,还有个问题就是localsend到底如何处理这些广播、请求的?简单来说,localsend在进入应用的时候,跑了一个HTTP server来处理组播、HTTP请求。

/// Starts the server.Future<ServerState?> startServer({required String alias,required int port,required bool https,}) async{//1.检查用户给localsend客户端取的别名,例如:“美丽的芒果”alias = alias.trim();if (alias.isEmpty) {alias = generateRandomAlias();}……final router = SimpleServerRouteBuilder();final fingerprint = ref.read(securityProvider).certificateHash;_receiveController.installRoutes(router: router,alias: alias,port: port,https: https,fingerprint: fingerprint,showToken: ref.read(settingsProvider).showToken,);_sendController.installRoutes(router: router,alias: alias,fingerprint: fingerprint,);final HttpServer httpServer;//默认HTTPS协议,需要先安装默认证书if (https) {final securityContext = ref.read(securityProvider);httpServer = await HttpServer.bindSecure('0.0.0.0',port,SecurityContext()..usePrivateKeyBytes(securityContext.privateKey.codeUnits)..useCertificateChainBytes(securityContext.certificate.codeUnits),);} else {//HTTP协议无需证书httpServer = await HttpServer.bind('0.0.0.0',port,);}//启动服务final server = SimpleServer.start(server: httpServer, routes: router);final newServerState = ServerState(httpServer: server,alias: alias,port: port,https: https,session: null,webSendState: null,pinAttempts: {},);state = newServerState;return newServerState;}

最后一个问题,localsend作为服务器有哪些RESTful API?根据官方文档,localsend应该提供了以下这些接口:

enum ApiRoute {//早期的注册接口,现已废弃info('info'),//现在版本的注册接口,传递client端IP地址、名称等基础信息register('register'),//文件传输之前获取token的接口prepareUpload('prepare-upload', 'send-request'),//文件传输接口upload('upload', 'send'),//取消接口,包括发送取消、接收取消cancel('cancel'),……;

4.4总结

总结起来,localsend的关键原理:

  1. 建立一个HTTP Sever。监听相关端口接收UDP组播;初始化RESTful API接口,用于HTTP的设备注册、文件传输;
  2. UDP设备注册流程中,服务端监听UDP组播端口、客户端回复组播消息并在回复组播消息后发起HTTP注册流程,向服务端传输IP等关键信息;
  3. TCP设备注册流程中,主动作为客户端遍历当前网段的所有IP,发起一路HTTP请求,向服务端注册;
  4. 扫描到设备后,通过在服务端/客户端之间的HTTP协议来传输文件。传输过程中,文件发送方为client;文件接收方为server。client发起一路post请求到服务器即可完成文件传输。

http://www.hkcw.cn/article/wBVhaWEwDF.shtml

相关文章

英国利物浦汽车冲撞人群事件嫌疑人身份公布

英国利物浦汽车冲撞人群事件嫌疑人身份公布 警方通报调查进展英国默西赛德郡警方发言人当地时间5月29日说,日前在利物浦市中心驾车冲撞球迷的男子面临7项指控,将于30日在利物浦地方法院出庭。据英国媒体报道,默西-柴郡地区皇家检察署授权默西赛德郡警方正式对53岁的嫌疑人保…

「动态规划::状压DP」网格图递推 / AcWing 292|327(C++)

目录 概述 相邻行递推 思路 算法过程 优化方案 空间优化 返回值优化 Code 复杂度 相邻两行递推 思路 算法过程 Code 复杂度 特殊优化&#xff1a;编译期计算 总结 概述 如果我们有一张地图&#xff0c;要求是在符合某类条件的前提在地图上放置最优解&#xff…

深圳海关查获超18公斤摇头丸 科技助力精准打击

近日,深圳邮局海关在跨境转运货物的监管过程中查获了一批伪报为“有机蜡溶香草”的包裹,实际上是摇头丸,总重量达18433.3克。目前,该案件已正式移交至海关缉私部门,进行更深入的调查和追溯。事件起因是海关关员对一批申报品名为“有机蜡溶香草”的转运货物进行例行检查时,…

一根网线连接两台电脑组建局域网

用一根网线分别插在网络端口&#xff0c;修改网络IP地址&#xff0c;假如有A和B&#xff0c;设置A:IP地址192.168.1.1,B&#xff1a;192.168.1.2 接着把网络防火墙关闭&#xff0c;步骤如下&#xff1a; 后面接着右键点击我的电脑&#xff0c;选择属性&#xff0c;打开远程控制…

丰巢回应快递柜消失市民取件扑空!

丰巢回应快递柜消失市民取件扑空。5月27日,上海普陀。市民发帖称包裹被存放在丰巢快递柜,过去取件时快递柜却消失不见了。5月29日,该包裹快递员告诉《正在新闻》,昨天在该小区的66号楼快递柜发生同样事件。对于快递柜搬走的原因,他表示不清楚,快递柜管理人员并没有告知他…

树型表查询方法 —— SQL递归

目录 引言&#xff1a; 自链接查询&#xff1a; 递归查询&#xff1a; 编写service接口实现&#xff1a; 引言&#xff1a; 看下图&#xff0c;这是 course_category 课程分类表的结构&#xff1a; 这张表是一个树型结构&#xff0c;通过父结点id将各元素组成一个树。 我…

高校大数据采集平台产品特色

大数据采集平台是专为高校大数据相关专业打造的智能化数据采集教学与实训工具。平台具有以下核心优势&#xff1a;采用可视化图形界面&#xff0c;无需编程基础&#xff0c;通过简单配置即可快速抓取网页中的文本、链接、图片、视频及文档等全类型数据&#xff0c;并自动存储至…

石家庄铁道大学回应书记打人 学生直播见证冲突

5月28日,石家庄铁道大学一名学生在宿舍里被学院书记殴打,还见了血。当时学生正在直播,许多网友目睹了整个过程。当这件事上了热搜后,学校的回应令人气愤,称学院书记没有问题,是学生先动手的。网友们表示不信,质疑书记去学生宿舍是为了让学生打他。事件起因是石家庄铁道大…

PGSQL结合linux cron定期执行vacuum_full_analyze命令

‌VACUUM FULL ANALYZE 详解‌ 一、核心功能 ‌空间回收与重组‌ 完全重写表数据文件&#xff0c;将碎片化的存储空间合并并返还操作系统&#xff08;普通 VACUUM 仅标记空间可重用&#xff09;。彻底清理死元组&#xff08;已删除或更新的旧数据行&#xff09;&#xff0c;解…

吴艳妮摘铜哽咽鞠躬道歉 带伤参赛展现坚韧精神

5月29日,亚洲田径锦标赛女子100米栏决赛中,吴艳妮以13秒07的成绩获得铜牌。赛后,她走路时显得有些一瘸一拐。在接受采访时,吴艳妮哽咽着向大家道歉,表示很感谢现场观众的支持,但没能为中国队拿到冠军感到非常抱歉。她提到自己的伤还没有完全恢复,不想过多解释,但仍坚信…

XCVP1902-2MSEVSVA6865 Xilinx FPGA Versal Premium SoC/ASIC

XCVP1902-2MSEVSVA6865 Versal Premium SoC/ASIC 单片 FPGA&#xff0c;可提供大容量 FPGA 逻辑仿真和原型设计目标。VP1902的逻辑单元数量增加了 2.2 倍&#xff0c;达到 1850 万个。 VP1902 自适应 SoC 提供最大容量和连接能力&#xff0c;具有可随机存取的逻辑密度和 2.4 倍…

TripGenie:畅游济南旅行规划助手:个人工作纪实(二十一)

这次&#xff0c;我新增了一个济南公交线路的展示界面&#xff0c;济南的公交线路多&#xff0c;且经过的站点覆盖范围广&#xff0c;价格实惠&#xff0c;是出行旅游交通工具的不二之选&#xff0c;我基于此现实情况&#xff0c;觉得做一个新的页面全面展示济南交通。 我选择把…

激励电平与频差的微妙平衡:晶振选型不可忽视的细节

在电子设备的设计中&#xff0c;晶振作为提供稳定时钟信号的关键元件&#xff0c;其选型的正确性直接关系到整个系统的性能与稳定性。而在晶振选型过程中&#xff0c;激励电平与频差之间的微妙平衡常常被工程师们所忽视&#xff0c;然而这一细节却可能对电路的正常运行产生深远…

数字人引领政务新风尚:智能设备助力政务服务

在信息技术飞速发展的今天&#xff0c;政府机构不断探索提升服务效率和改善服务质量的新途径。实时交互数字人在政务服务中的应用正成为一大亮点&#xff0c;通过将“数字公务员”植入各种横屏智能设备中&#xff0c;为民众办理业务提供全程辅助。这种创新不仅优化了政务大厅的…

练习小项目9:打字效果文字展示(多段文字循环+删除+光标闪烁)

项目简介&#xff1a; 本文介绍如何用原生JavaScript实现一个简洁的打字效果&#xff0c;支持&#xff1a; 多段文字循环播放 打字完后暂停一会儿 逐字删除&#xff0c;形成打字机动画感 打字光标闪烁效果 项目适合用于首页欢迎语、提示语等动态文本展示&#xff0c;能提…

【从零开始超详细】Linux系统使用docker + docker-compose部署nacos以及SpringBoot+vue项目详细

Linux系统使用dockerdocker-compose部署nacos以及SpringBootvue项目详细文档 本文章Linux发行版为openEuler 22.03 (LTS-SP2), 多数命令与centos一致, 使用centos的小伙伴也可以参考 不知道自己的服务器是什么发行版的小伙伴可以执行如下命令查看: cat /etc/os-release执行结果…

利用Python制作环保志愿者招募海报

1. 文档概述 本研究文档详细论述了运用Python编程语言中的Pillow库&#xff08;PIL&#xff09;进行设计并制作一张专业环保志愿者招募海报的完整流程。该海报以“守护绿色家园”为主题&#xff0c;旨在激励社会公众积极参与森林保护的志愿活动。通过编程实现&#xff0c;海报中…

软考-系统架构设计师-第十五章 信息系统架构设计理论与实践

信息系统架构设计理论与实践 15.2 信息系统架构风格和分类15.3 信息系统常用的架构模型15.4 企业信息系统总体框架15.5 信息系统架构设计方法 15.2 信息系统架构风格和分类 信息系统架构风格 数据流体系结构风格&#xff1a;批处理、管道-过滤器调用/返回体系结构风格&#x…

德思特新闻 | 德思特与es:saar正式建立合作伙伴关系

德思特新闻 2025年5月9日&#xff0c;德思特科技有限公司&#xff08;以下简称“德思特”&#xff09;与德国嵌入式系统专家es:saar GmbH正式达成合作伙伴关系。此次合作旨在将 es:saar 的先进嵌入式开发与测试工具引入中国及亚太市场&#xff0c;助力本地客户提升产品开发效率…

【Simulink模型标准化开发】需求管理与基线测试--- Requirements ManagementSimulinkTest

前言&#xff1a;Simulink模型是嵌入于Matlab之中的一个模块化开发工具&#xff0c;它在嵌入式领域和应用层逻辑的搭建上享有声誉。并且&#xff0c;Simulink与C语言一样有着一套标准化的开发流程&#xff0c;因此它也具备安全性、可靠性、可移植性等优势。而在本篇文章中&…