Mac应用第三方渠道发布指南

1.申请证书

Mac 应用有多种证书,主要包含以下几种:

Apple证书类型
Apple证书类型

  • Apple Development: Xcode 11之后Apple全平台开发证书
  • Apple Distribution: Xcode 11之后Apple全平台发布
  • Mac Development:用来开发和Debug的证书
  • Mac App Distribution:用来给APP文件签名后上传到Mac App Store
  • Mac Installer Distribution:用来给PKG文件签名后上传到Mac App Store
  • Developer ID Installer:用于给PKG文件签名,在非Mac App Store场景使用
  • Developer ID Application:用于给APP文件签名,在非Mac App Store场景使用

这里直接申请 Developer ID Application 证书,申请过程中需要使用 Mac 本地证书助理来创建证书签名请求,按照指引一步步完成即可。

证书申请完成
证书申请完成

申请完成后下载证书并在本地安装。

2.申请 App ID

也就是平常大家所说的 Bundle ID,这里跟普通 iOS App 是完全一致的,选择 APP IDs 即可,正常填写相关内容。

申请App ID
申请App ID

3.生成描述文件

这里同样选择生成 Developer ID 描述文件

生成描述文件
生成描述文件

然后选择 Mac 应用,并填写刚才申请的 App ID

4.本地工程配置

本地安装证书,描述文件,并在工程配置里的 Signing & Capabilities 填写刚才申请的 App ID(Bundle ID)和描述文件

本地工程配置
本地工程配置

5.对App文件进行签名和公证

开发完成后使用 Xcode 自带工具对 App 文件进行签名和公证。

首先选择 Product->Archive 生成 App 文件,注意这里的步骤要登陆证书对应的开发者账号。

生产App文件
生产App文件

之后使用 Distribute App 选项对 App 进行公证,这里并不是 App 审核,是苹果推出的一个针对 Mac 第三方 App安全机制 ,在 Mac OS 10.15 之后,没有公证的 APP 打开会出现错误信息。

对App进行公证
对App进行公证

这里同样选择 Developer ID ,之后选择 UploadExport 即可导出 App 文件,其中 Upload 会由 App Store 进行公证,速度还是很快的,几分钟之内就公证完毕了。

公证过程
公证过程

6.打包dmg文件

App 文件进行签名公证后最后一步就是打包生成 dmg 文件,这样打开后可以直接拖动 .app 文件到 Application 文件夹中,这里使用 create—dmg工具

具体选项可以参考 create-dmgGitHub 主页,需要注意的是 App 文件要放进 source_folder 文件夹里。

最后就得到一个可以任意安装的 dmg 文件啦,Mac 应用打包完成!

7.FAQ

如果在执行公证的时候:

公证过程
公证过程

出现如下报错:

Hardened Runtime(强化运行时)是 ApplemacOS 中引入的一项 安全机制,用于加强 App 的保护能力,防止被恶意注入、调试或篡改。

它是 App Notarization(公证)和沙盒(App Sandbox)安全体系的基础之一。凡是要进行公证或上架 App StoremacOS App,目前(2025/08/23)都必须开启 Hardened Runtime

如果你的项目中没有添加 Hardened Runtime,则需要点击项目名 – TARGETS – 应用名称 – Signing & Capabilities,点击“+ Capability”按钮,选择 Hardened Runtime

如果点安装包报错不能启动。使用命令行启动报错如下:

点击 “显示包内容” 进入应用程序目录,然后查看 Contents/MacOS/ 下的执行程序,发现是白色的,如下图:

而不是黑色的左上角带 exec 执行标记的样子,如下:

此问题大概率是由于安装包里的程序没有可执行权限导致的,执行如下命令授予执行权限即可。

命令参考如下:

参考链接


Flutter编写HTTPS服务器

从磁盘加载证书:

从内存加载证书:

参考链接


Java环境下 SslSocket 服务端及客户端简单例子

客户端代码如下:

服务端代码如下:

1 SSL单向认证概念

当客户端(服务请求方)向服务端(服务提供方)发起请求时,服务器端需要向客户端提供认证。服务端需要生成一个keystore和一个服务器密钥对儿(公钥和私钥),客户端需要生成一个truststore,然后导入服务端的公钥证书。

2 keystore以及服务器密钥对儿的生成 

这条命令会在生成 keystore 后接着生成一个密钥对儿。RSA是非对称密钥算法,也可以改为 keytool支持的其他密钥算法,365 代表的是证书的有效期,可以自己指定,shfqkeystore.jkskeystroe 的名称,也可以自己指定。打开 cmd 命令行,输入:

会提示输入 keystore 的密码,接着会提示输入名字等信息,如下图:

补充:输入<certificatekey>的主密码,是指生成服务端证书的私钥。服务端私钥如果和keystore的相同的话,直接按回车。建议直接按回车,即服务端私钥和keystore的密码相同。如果两者的密码不相同的话在服务端tomcat server.xml中配置完毕以后启动tomcat会报一个 UnrecoverableKeyException: Cannot recover key 的异常(后面会介绍服务端 tomcat server.xml 的配置的)。

keytool会把生成的keystore文件默认保存到C:\Users\lenovo路径下(用户目录下的计算机名称下)接下来生成的所有文件也都保存到此处。

3 验证新生成的keystor文件以及证书信息

可以执行下面的命令:

会显示出以下信息,如图:

4 导出公钥证书

下面的命令可以导出自签公钥证书:

其中shfqcert.cer是导出证书的名称,可以随便起个名字,shfqkeystore.jks是2中生成的keystore 文件。

执行上面的命令会要求输入shfqkeystore的密码,会显示以下信息,如下图。

5 Truststore的生成以及公钥证书的导入

把4生成的公钥证书shfqcert.cer导入到truststore中

shfqcert.cer是4导出的公钥证书,shfqtruststore.jks可以随便起,是生成的truststore的文件名。这条命令首先会生成一个truststore,然后导入4生成的公钥证书shfqcert.cer。

执行

后,首先会提示输入 truststore 的密码,如下图:

6 验证5生成的truststore文件

shfqtruststore.jks是5生成的truststore文件名。

 到此为止,keystore、truststore、公钥证书都已生成完毕。

参考链接


Java环境下 SslSocket 服务端及客户端简单例子

网页检测 微信登录状态

现在很多网站、应用平台在登录的时候,都支持直接通过微信扫码登录。

最近我发现一个现象:以前需要扫二维码才能登录,而现在,如果你的电脑上已经运行了微信,它能直接检测到,然后点击一个按钮就可以实现登录了。

比如知识星球以前是微信扫码登录,而现在,它会自动检测到展示下面的画面:

然后,程序员的敏感神经触发,让我开始思考:它是咋知道我电脑上已经运行了微信,并且登录的是这个账号呢?

要知道,知识星球只是一个运行在 Chrome 浏览器的网页应用,它是不可能拥有 native 权限,去做什么进程扫描检测之类的事儿的。

直接祭出 F12 大法,来看看到底发生了什么。

在网络连接这里,我发现了一堆的 check-login 请求:

这一堆请求里面只有一个是成功的,其他都失败了。仔细一看,请求的域名都是 localhost.weixin.qq.com,只不过端口在变化,最后在 13013 端口请求成功了。

一看这个域名,localhost.weixin.qq.com,而且解析的 IP 地址是 127.0.0.1

难道 hosts 里面被下药了?

打开 hosts 文件一看,没啥异常啊。然后 nslookup 看了一下,还真是 DNS 返回的 IP 地址它就是 127.0.0.1,有点意思。

请求 127.0.0.113013 端口,那本地又是哪个进程在配合监听这个端口呢?我估计你已经猜到了,就是微信的进程。

最后看看知识星球网页里面,发起这个请求的 JS 代码是啥?

至此,真相大白了:

像知识星球这样的第三方应用通过微信的官方接口接入微信登录功能,然后接入的 JS 代码里面会去请求 localhost.weixin.qq.com 这个域名的 13013 等多个端口,来检查本地是否有微信账户已经登录了。

而腾讯把这个域名映射到了本机的 127.0.0.1。再在客户端这边监听一下 13013 这个端口,就能实现在网页里面和本地的微信进程进行通信了。

参考链接


Flutter 的生命周期

概述

生命周期是一个 widget 组件加载到卸载的整个周期,熟悉生命周期可以让我们在合适的时机做该做的事情。

Flutter 开发中,everything is widget,但我们一般都不用直接继承 Widget 类来实现一个新组件,我们通常会通过继承 StatelessWidgetStatefulWidget 来间接继承 Widget 类来实现。StatelessWidgetStatefulWidget 都是直接继承自 Widget 类,而这两个类也正是 Flutter 中非常重要的两个抽象类,它们引入了两种 Widget 模型。此文主要介绍这两种 widget 的生命周期。

StatelessWidget

StatelessWidget 是无状态的 Widget,一旦创建就不会发生变化,所以无法提供 setState 修改组件的状态,它内部属性应声明为 final,防止意外发生改变。所以 StatelessWidget 的生命周期只有一个,就是 buildbuild 是用来创建 Widget 的,但因为 build 在每次界面刷新的时候都会调用,所以不要在 build 里写业务逻辑,可以把业务逻辑写到你的 StatelessWidget 的构造函数里。其生命周期如下图:

StatefulWidget

StatefulWidget 是有状态的 Widget,它的 state 在发生变化时会重新渲染 UI,提供 setState 方法修改组件的状态,它的生命周期主要在 State 这块,其生命周期如下图:

下面解释下各个函数:

  • initState当 Widget 第一次插入到 Widget 树时会被调用;对于每一个 State 对象,Flutter Framework 只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等 。不能在该回调中调用 BuildContext.dependOnInheritedWidgetOfExactType(该方法用于在 Widget 树上获取离当前 widget 最近的一个父级 InheritFromWidget),原因是在初始化完成后,Widget 树中的 InheritFromWidget 也可能会发生变化,所以正确的做法应该在在 build 方法或 didChangeDependencies 中调用它。

  • didChangeDependencies()State 对象的依赖发生变化时会被调用;比如其所依赖的 InheritedWidget 发生变化时, Framework 会调用此方法通知组件发生变化。典型的场景是当系统语言 Locale 或应用主题改变时,Flutter framework 会通知 widget 调用此回调。

注意:didChangeDependencies 方法调用后,组件的状态变为 dirty,立即调用 build 方法。

  • build用于构建 Widget 的,会在如下场景被调用:

    1. 在调用 initState() 之后。
    2. 在调用 didUpdateWidget() 之后。
    3. 在调用 setState() 之后。
    4. 在调用 didChangeDependencies() 之后。
    5. State 对象从树中一个位置移除后(会调用 deactivate )又重新插入到树的其它位置之后。

注意:此方法中应该只包含构建组件的代码,不应该包含其他额外的功能,尤其是耗时任务。

  • reassemble()此回调是专门为了开发调试而提供的,在热重载( hot reload )时会被调用,此回调在 Release 模式下永远不会被调用。

  • didUpdateWidget()在父 widget 重新构建子  widget  时,子 widgetdidUpdateWidget 可能会被调用。 之所以说可能,是因为在父 widget 重新构建时,Flutter Framework 会调用 Widget.canUpdate 来检测Widget 树中同一位置的新旧节点的 keyruntimeType 是否同时相等,新旧 widgetkeyruntimeType 同时相等时会返回 true,此时 didUpdateWidget() 就会被调用,如果新旧节点的 keyruntimeType 没有同时相等,didUpdateWidget() 不会被调用。

注意:Framework 调用完此方法后,会将组件设置为 dirty 状态,然后调用 build 方法。

  • deactivate()State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter Framework 会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过 GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用 dispose() 方法。

  • dispose()State 对象从树中被永久移除时调用;通常在此回调中释放资源。

实例展示

展示随机数的一个 demo,点击随机按钮,更新随机数,打印此 demo 的生命周期函数的调用流程,代码如下:

点击run首次启动,打印log如下:

点击切换随机数按钮,打印 log 如下:

点击切换随机数按钮,会调用 setState()setState 会调用 build 重新构建 widget

点击 hot reload 按钮,打印 log 如下:

App生命周期

通过 WidgetsBindingObserverdidChangeAppLifecycleState 可以获取 App 的生命周期状态。生命周期在 AppLifecycleState 类中。常用状态包含如下几个:

  • resumed:处于可见并能响应用户的输入
  • inactive:处于不活跃状态且无法处理用户的响应
  • paused:处于不可见且不能响应用户的输入,但是在后台继续活跃

举例如下:

记得注册和移除监听。

生命周期相关的一些注意点

  • 页面渲染完毕回调 addPostFrameCallbackStatefulWidget 渲染结束的回调,只会被调用一次,之后 StatefulWidget 刷新 UI 也不会被调用。在 initState 不能调用BuildContext.dependOnInheritedWidgetOfExactType,可以使用 addPostFrameCallback 规避此限制:

  • mounted

mountedState 对象中的一个属性,此属性表示当前组件是否在树中(在创建 State 之后,调用 initState 之前,Framework 会将 StateBuildContext 进行关联),当 Framework 调用 dispose 时,mounted 被设置为 false,表示当前组件已经不在树中。

createState 函数执行完毕后表示当前组件已经在组件树中,属性 mountedFramework 设置为 true,平时写代码时或者看其他开源代码时经常看到如下代码:

强烈建议:在调用 setState 时加上 mounted 判断。

为什么要加上如此判断?因为如果当前组件未插入到树中或者已经从树中移除时,调用 setState 会抛出异常,加上 mounted 判断,则表示当前组件在树中。

  • dirtyclean

dirty 表示组件当前的状态为 脏状态,下一帧时将会执行 build 函数,调用 setState 方法或者 执行 didUpdateWidget 方法后,组件的状态为 dirty

cleandirty 相对应,clean 表示组件当前的状态为干净状态,clean 状态下组件不会执行 build 函数。

参考链接


Flutter 的生命周期

Flutter手势冲突难题怎么破?几种解决方式大揭秘!

Flutter 应用开发中,手势处理是构建交互式界面的核心环节。然而当多个手势识别器或可滚动组件嵌套使用时,经常会出现手势冲突问题。

本文将深入探讨 Flutter 中解决手势冲突的各种方法,并分析其适用场景,帮助您掌握高效的手势管理策略。

1 手势冲突的根源

在理解如何解决手势冲突之前,我们需要先了解 Flutter 中手势系统的基本原理 :

Flutter 的手势识别基于 GestureRecognizer 竞争机制。当用户触发指针事件(PointerDown)时,多个手势识别器会进入 gesture arena 竞争,最终只有一个胜出。竞争的过程主要有两个阶段:

  • 命中测试阶段(Hit Test Phase):确定哪些 Widget 接收了手势事件。
  • 手势识别阶段(Gesture Recognition Phase):多个 Widget 可能会识别相同的手势,从而产生冲突。

开发过程中比较常见的手势冲突场景包括:

  • 嵌套滚动组件(如 PageView 中的 ListView
  • 多层手势检测器(如 InkWell 内部的 GestureDetector
  • 父子组件都监听相同类型手势事件
  • 多个手势识别器同时竞争同一区域

2 解决手势冲突的方式

针对不同的应用场景,Flutter 提供了不同的冲突的处理方式

2.1 HitTestBehavior (最轻量级)

通过控制 GestureDetectorbehavior 参数控制手势事件如何在Widget 树中传递。常见的 behavior 值有:

  • HitTestBehavior.translucent : 自身和子组件都能接收事件
  • HitTestBehavior.opaque: 拦截所有事件(即使透明区域)
  • HitTestBehavior.deferToChild: 默认值,优先传给子组件

有兴趣的话,可以尝试改变内外两个 GestureDetectorbehavior 的值来加深理解对这个处理方式的理解

适用场景

  • 父子组件都需要响应手势
  • 需要控制手势事件的传递层次(通过 behavior )
  • 简单按钮嵌套
2.2 AbsorbPointer/IgnorePointer组件

这两个组件在使用时,会完成拦截或忽略掉所有的手势:

  • IgnorePointer:使子 Widget 忽略所有手势事件,但仍会参与布局和绘制;
  • AbsorbPointer:拦截并消耗所有手势事件,子 Widget 无法接收手势,自身可以接收并处理事件。

适用场景

  • 临时禁用某个区域的手势
  • 阻止下层 Widget 接收手势事件;
  • 复杂UI中动态切换交互状态;

2.3 RawGestureDetector与手势竞技场

Flutter 的手势竞技场( GestureArena )机制允许自定义手势识别的竞争规则,通过使用底层的 RawGestureDetector 注册多个手势识别器,由手势竞技场决定胜出者,整个竞技的过程会经历

  1. 当指针按下时,所有识别器进入竞技场
  2. 通过 addPointer() 处理事件流
  3. 识别器声明是否"准备好"处理事件
  4. 竞技场选择获胜者(首个声明准备就绪的识别器)
  5. 胜出者接收后续事件,其他被拒绝

适用场景

  • 需要同时识别多种手势
  • 复杂手势组合(缩放+旋转+平移)
  • 自定义手势识别逻辑
2.4 Listener组件处理原始指针事件

直接通过 Listener 去处理最原始的指针事件,自由控制:

适用场景

  • 需要底层事件控制的场景
  • 高度定制化的交互需求
  • 性能关键型的手势处理
2.5 自定义 ScrollPhysics

通过继承 ScrollPhysics 类,可以自定义滚动行为,控制滚动事件的传递和处理。

适用场景

  • 嵌套滚动组件(如 PageViewListView 的冲突)
  • 需要精确控制滚动阈值和边界条件
2.6 使用 NotificationListener

通过监听滚动通知(如 ScrollNotification),可以在父组件中捕获并处理滚动事件,从而控制子组件的行为。

适用场景

  • 监听滚动状态并作出响应
  • 协调多层级滚动组件的行为

3 手势冲突的例子

项目的某一个场景中,会有一个 PageView 里面嵌套 ScrollView 的场景,而且这两个的滚动方向是一致的(都是竖向的滚动)。

这种情况下要怎样保证当 ScrollView 滑动到最底(最顶)时,能触发 PageView的 翻页呢?结合前面的介绍的处理手势冲突的几种方式,大家会选择哪个呢?

最开始笔者是选择了自定义 ScrollPhysics 的方式来尝试处理的,但发现最终也只能有一个组件能滚动(中间尝试不同解决方案的痛苦就一一细说了),这里最终是通过NotificationListener 来协调这两者的滚动的。(这个方案不一定是最好的)

4 总结

本文主要介绍了 Flutter 中手势冲突几种解决方案,从简单高效的 AbsorbPointer 到底层强大的 Listener,可以应对不同复杂度的交互场景。

可以根据不同的业务场景选择不同的解决方案,这里给几点在选择解决方案时可参考的点:

  • 准确识别冲突来源和类型
  • 评估交互的复杂程度
  • 考虑性能和维护成本

参考链接


Flutter手势冲突难题怎么破?几种解决方式大揭秘!

OpenBB的介绍以及如何使用OpenBB助力A股港股的金融数据分析

OpenBB 是一个开源的金融数据平台,旨在为投资者、分析师、研究人员和开发者提供免费、透明且易于使用的金融与宏观经济数据访问接口。它曾被认为是类似于彭博终端(Bloomberg Terminal)的功能性替代品,但完全开放源码,用户可以自由定制和扩展。

在一些文章中将 OpenBB 解释为 Open Bloomberg,这是个误解。尽管它常被视为“开源版彭博终端”,但其名称中的“BB”实际上源自黑莓公司的股票代码,而 OpenBB 的创始人此前曾在黑莓股票上亏损。

📜 OpenBB 的历史背景

OpenBB 的前身是由 Didier Lopes 在 2021 年创建的开源项目 Gamestonk Terminal。2021 年 3 月,Gamestonk Terminal 1.0 版本正式发布,凭借其独特的功能和开源特性,迅速在市场上崭露头角,并于同年成功获得第一笔风险投资。基于该项目的良好发展态势,新公司于 2022 年正式成立,并将公司和项目更名为 OpenBB。

请参考创始人Didier Lopes的这篇文章 GME didn’t take me to the moon, but Gamestonk terminal did

OpenBB的发展时间线如下:

  • 2020 年第四季度:Didier 开启 Gamestonk Terminal 的开发之旅
  • 2021 年第一季度:Gamestonk Terminal 正式上线
  • 2021 年第二季度:Joeseph Jacks 与开发团队展开合作洽谈
  • 2021 年第三季度:OpenBB 项目宣告成立
  • 2021 年第四季度:获得 850 万美元的初始投资
  • 2022 年第一季度:OpenBB 正式发布

从 Gamestonk Terminal 到 OpenBB Terminal,其产品开发思路与传统金融终端类似,是一款集成众多数据源的开源金融终端产品。和著名的 Bloomberg Terminal 一样,OpenBB Terminal 对不同数据源的数据进行了抽象化和标准化处理,这种特性显著提升了金融分析从业人员的工作效率,也使得 OpenBB Terminal 在短时间内吸引了大量用户。

OpenBB Platform

随着 OpenBB Terminal 的用户规模不断扩大,项目团队面临着巨大的维护压力。正如创始人 Didier Lopes 在文章中提到,维护这个免费开源终端,包括添加数据集、处理 500 多个 Python 包依赖项以及应对数据源端点更新等工作,所投入的资源对于初创公司来说难以持续。

关于停止 OpenBB Terminal 的开发决定,请参考文章 Sunsetting OpenBB Terminal: Why, How, and What now?。

在实际金融数据分析中,投资者往往面临复杂的数据获取问题。例如,多元化投资者的资产可能涉及多个币种和股票市场,需要从不同数据源收集数据并进行计算,这一过程不仅耗时,而且重复性高。虽然可以通过编写 Python 脚本来自动拉取数据,但随着时间推移,会遇到数据源接口变更和数据需求多样化的难题。

基于这些问题,OpenBB 开发团队重新审视产品价值,决定在 2024 年一季度发布 OpenBB Platform 取代 OpenBB Terminal。OpenBB Platform 专注于金融数据的抽象化和标准化,开发者能够自由添加所需数据源。以查询历史股价为例,无论选择哪种数据源,都可使用统一接口:

用户可以根据自身需求,灵活选择默认或指定的数据源,无论是免费还是付费的数据源都能适配。

随着产品升级,OpenBB 的用户界面也进化为 OpenBB Platform CLI,它结合了命令行工具和 WebView,为调用 OpenBB API 提供了便捷环境。在命令行中查询股价历史的示例命令如下:

执行该命令后,WebView 会展示查询结果。

OpenBB Workspace

OpenBB Platform CLI 更适合开发者和具备编程能力的金融分析人员,而对于企业用户,OpenBB 团队推出了 OpenBB Workspace 解决方案,也被称为 OpenBB Terminal Pro。它虽然并非全开源产品,但有望为团队带来收益,目前是 OpenBB 团队的主要开发方向。

通过参考 OpenBB 官方文章中的架构图(整合了 2024 年 3 月及后续关于私有化部署和 AI 集成的示意图),可以清晰了解其产品定义。

关于这个架构图,参考下面这篇文章 Exploring the architecture behind the OpenBB Platform

开源的 OpenBB Platform 是金融分析应用的基础,在此之上,前端部分不断发展。从 2025 年 6 月起,OpenBB Bot 转由 Unusual Whales 运营,OpenBB Workspace 也转变为基于 AI 的可定制方案。企业用户能够基于 OpenBB Platform 和自身数据,将 OpenBB Workspace 定制为兼具金融终端功能和 AI 能力的专属金融分析平台,相比传统昂贵的金融终端,具有显著优势。为增强灵活性和可定制性,OpenBB 开源了 OpenBB Workspace 的后端和 AI Agent 的集成部分,除用户界面组件外,数据集成方案和 OpenBB Copilot 都支持定制。

  • OpenBB Workspace Backend
    https://github.com/OpenBB-finance/backends-for-openbb
  • OpenBB Agents
    https://github.com/OpenBB-finance/agents-for-openbb
OpenBB与AI的集成

OpenBB 针对企业用户和开发者,分别提供了不同的 AI 使用方式,其与 AI 的集成主要体现在以下两个层面:

OpenBB Platform - LLM Friendly Mode:专为开发者设计的集成方法
OpenBB Workspace - OpenBB Copilot:主要面向企业用户的使用方式

LLM Friendly Mode

在 API 层面,OpenBB Platform 的 LLM 友好模式,极大地降低了开发者将 OpenBB 数据模型集成到 AI 应用中的难度。OpenBB 开放的函数接口能够轻松转换为 LLM 的函数调用,官方提供的示例代码如下:

上述代码创建了一段 LLM 对话,并将 OpenBB 的股价查询函数obb.equity.price.quote作为 LLM 的函数调用,方便快捷地实现数据获取。

OpenBB Copilot

对于企业用户而言,OpenBB Workspace 提供的 OpenBB Copilot 功能十分实用。用户可以依据当前仪表板内容,借助 AI 进行深度分析。例如,在查看辽港股份资料后,通过 OpenBB Copilot 获取 2025 年投资建议,其分析结果和行业对比总结具有较高的参考价值。

上图中,使用的 Equity Template 模板在查询中国股市数据时存在部分空白项,如当前股票新闻、收入分析等。从辽港股份股票代码601880.SS的查询情况来看,中国股市数据主要依赖 Yahoo Finance,但该数据源对于中国股市数据分析存在局限性,OpenBB 若能支持更多本地化主流数据源,将更贴合中国市场需求。

中国市场的金融数据源

在中国金融市场,主流数据源大多需要付费使用,其中 Wind、东方财富 Choice 和同花顺 iFind 是行业内常用的数据源。除付费数据源外,也有一些可靠的开源数据源可供选择,如 AKShare 和 TuShare:

AKShare - https://github.com/akfamily/akshare

TuShare - https://github.com/waditu/tushare

通过开发 OpenBB Platform 的数据源扩展,接入中国市场专有的数据源,能够进一步提升 OpenBB 在中国市场金融分析领域的实用性和竞争力。

📝 总结

OpenBB 是一个功能强大、开源免费、面向未来金融数据需求的综合平台。无论你是想了解市场动态、构建投资组合,还是进行深度数据分析,OpenBB 都能为你提供强有力的支持。随着其生态系统的不断发展,OpenBB 正逐渐成为新一代金融科技爱好者的首选工具。

参考链接


Flutter手势冲突与手势竞争

手势冲突的原因

当多个手势同时出现在同一控件上时,就会发生手势冲突。这可能会导致系统无法准确识别用户的意图,从而导致错误的操作。iOS 上的 UIScrollView 是手势冲突的主要来源,因为它可以响应滚动、缩放和点击等各种手势。当 Flutter 视图与 UIScrollView 同时存在时,很容易发生手势冲突。

常见的冲突场景

最常见的场景是将 Flutter 视图嵌套在 UIScrollView 中。在这种情况下,UIScrollView 的滚动手势和 Flutter 视图的手势很容易发生冲突。例如,当用户在 Flutter 视图上拖动时,UIScrollView 可能将其误认为是滚动操作,从而导致 Flutter 视图无法响应拖动手势。

解决方案:手势竞技场

解决手势冲突的最有效方法之一是使用 Flutter 的手势竞技场。这是一个负责协调手势的系统,可确保同一时间只有一个手势被识别。要使用手势竞技场,需要在 Flutter 视图的最顶层添加一个 GestureRecognizer Widget,并将其作为竞技场的子控件。

手势竞技场的使用

以下是使用手势竞技场的示例代码:

通过使用这种方法,Flutter 视图的手势和 UIScrollView 的手势可以同时响应,互不干扰,从而完美解决 iOS 上的 Flutter 页面手势冲突问题。

常见问题解答

1. 为什么手势冲突在 iOS 上很常见?

因为 iOS 上的 UIScrollView 可以响应多种手势,并且它经常与 Flutter 视图一起使用,这可能导致手势冲突。

2. 手势竞技场是如何工作的?

手势竞技场是一个负责协调手势的系统,可确保同一时间只有一个手势被识别。它通过让手势识别器注册自己并协调它们的活动来实现此目的。

3. 如何在 Flutter 中使用手势竞技场?

在 Flutter 视图的最顶层添加一个 GestureRecognizer Widget,并将其作为竞技场的子控件。

4. 除了手势竞技场,还有其他解决手势冲突的方法吗?

是的,还有其他方法,例如使用 GestureDetector Widget 或重写控件的手势识别逻辑。

5. 为什么解决手势冲突很重要?

解决手势冲突很重要,因为它可以防止错误的操作,并确保用户获得流畅、无缝的体验。

参考链接


Flutter:使用Overlay展示浮动的Widget

想象一下:你编写出的迷人表单页面

你把它发给产品经理,他看了一眼说:“我一定要完整的输入国家名称吗,当我输入文字时难道你就不能给我展示些建议吗?”,你想了想:“好吧,他是对的”,因此,你决定开发一个‘自动补全‘的’预先输入’功能,随便你怎么称呼它:一个文本展示框 TextField ,当用户输入文字的时候展示一些建议选项。开始工作了..你知道怎么拿到建议数据,你知道怎么写逻辑,你知道所有要做的事情..除了不知道怎么将建议选项浮动展示在 Widgets 之上。

你想了想:打算重新设计代码结构,为了实现悬浮效果,决定将整个页面包装进一个 Stack 组件中,你需要准确的计算每个 Widget 显示的位置,非常侵入性、必须要严谨、容易出错,并且直觉告诉你这么做可能是错误的,有其他的实现方式吗?

方案就是:你可以使用 Flutter 已经提供好的 StackOverlay

在这片文章中,我将会介绍如何使用 Overlay ,来创建悬浮在其他 Widget 之上的 Widgets,并且并不需要重构你的整个页面。

你可以使用 Overlay 来展示自动匹配的建议选项,小提示,或着基本上所有的浮动的东西。

Overlay是什么?

官方文档这样定义

Stack of entries that can be managed independently.    // 一个可以独立管理的Stack子类

Overlays let independent child widgets “float” visual elements on top of other widgets by inserting them into the overlay’s Stack.    // 通过将可独立管理的子节点widgets加入到overlay的栈中,Overlays可以将这些widgets浮动展示到显现的elements节点的顶部

这看起来就是我们正在寻找的内容,当我们创建 MaterialApp 的时候,它会自动创建一个NavigatorNavigator 则又会创建一个 Overlay : 一个 Navigator 用来管理所展示的 Views 视图的 Stack 组件。

接下来,让我们一起看看怎么使用 Overlay 来解决我们的问题吧。

注意:这篇文章的核心是介绍如何显示浮动 Widgets,因此不会涉及太多如何实现自动补全文本输入框 TextField 的细节,如果你对一个编写良好、可高度自定义的自动补全 Widget 感兴趣的话,可以参考 flutter_typeahead

初始代码

我们以最初的代码开始吧:

*一个简单页面,包含了三个文本输入框:country、city、address。

然后我们就以 country 文本输入框为例吧,将它封装成一个我们自己的 StatefulWidget,命名为 CountriesField

接下来我们将要做的是,每次当选中文本输入框获取焦点 Focus 的时候,将一个浮动的 List 列表展示出来。当失去焦点 Focus 的时候,再将它隐藏起来,当然你可以按照自己需求来决定如何实现,你可能需要在用户输入了一些文字后展示它,或者当用户点击 Enter 按钮的时候再隐藏。无论怎样,让我们先看看如何展示这个悬浮的 Widget吧:

  • 我们给 TextFormField 绑定了一个 FocusNode,并且在 initState 里面给 FocusNode 添加了一个监听事件,通过监听事件来获取什么时候获得/失去焦点。
  • 每次当我们获取焦点 (_focusNode.hasFocus == true) 的时候, 我们通过 _createOverlayEntry 创建一个OverlayEntry 实例对象,然后通过使用 Overlay.of(context).insert,将它插入到最邻近的 Overlay Widget 中去。
  • 每次当我们失去焦点 (_focusNode.hasFocus == false) 的时候,我们通过使用 _overlayEntry.remove 来移除刚才添加的 Overlay 实例。
  • _createOverlayEntry 通过使用 context.findRenderObject 来获取我们的 Widget 所在的渲染对象 RenderBox ,渲染对象里包含位置 Position、大小 Size、和一些其他关于渲染的信息,有了这些信息能够帮助我们计算在哪里展示我们的悬浮列表。
  • _createOverlayEntry 通过渲染信息来获取当前 Widget 的大小,也可以使用 renderBox.localToGlobal 来获取当前 Widget 在屏幕上的坐标。我们将 localToGlobal 设置为 Offset.zero 这意味着我们将在渲染对象中使用(0,0)坐标,并且将他们转换为屏幕上相对应的坐标。
  • 接着我们创建了 OverlayEntry,这时一个用来将 Widgets 展示到 Overlay 中的 Widget
  • 当前创建的 OverlayEntry 的是一个 Positioned Widget。请牢记 Positioned Widgets 只能被插入到 Stack 中,当然 Overlay 其实也是一个 Stack
  • 我们设置 Positioned Widget 的坐标,给它和 TextField 相同的 X 轴坐标,相同的宽度,相同的 Y 轴坐标,当然为了不遮挡到 TextField,在底部进行了一些偏移。
  • Positioned 内部,我们设置了一个展示建议选项的 ListView(里面默写了例子中的一些国家)。注意到我把所有的内容都包在了 Material 中,关于这样写有两个原因: Overlay 默认不包含 Material Widget,并且很多 Widgets 如果没有有 Material 祖先节点的话不能展示,除此之外 Material 还提供了 elevation 属性,可以让我们给 Widget 设置阴影效果,看起来就像真正浮在上面一样。

以上,我们的建议选择项可以浮在所有 Widget 之上了!

彩蛋:跟随 Widget 滑动!

在我们离开之前,让我们在多学喜一点吧!假如我们的页面是可以滚动,我们可能注意到如下现象:

建议选择列表固定在了屏幕上!在某些场景下你可能的确需要固定的,但是在当前场景中,我们不想要它固定,我们想要它跟随我们的 TextField 一起滚动!

关键词滚动,Flutter给我们提供了两个widget:CompositedTransformFollowerCompositedTransformTarget,简单介绍就是,如果我们关联起一个 follower 和一个 target ,那么无论 target 滚动到哪里,这个 follower 将跟随它一起滚动!为了关联起一个 follower 和一个target,我们需要给他们设置相同的LayerLink.

因此我们需要将建议选择列表用 CompositedTransformFollower 包起来,将 TextFieldCompositedTransformTarget 包起来。然后我们将他们使用想用的 LayerLink 关联起来,这样就可以是建议选择列表跟随 TextField 一起滑动了:

  • 我们将 Material WidgetCompositedTransformFollower 包裹进 OverlayEntry 中,将TextFormField 包裹进 CompositedTransformTarget 中。
  • 我们使用相同的 LayerLink 示例,来关联 followertarget,这样 followertarget 将会在相同的坐标系中,高效的跟随 target 而动。
  • Positioned Widget 中移除了 topleft 属性,因为默认 followertarget 有相同的坐标,因此不在需要。然而我们保留了 width 属性,因为如果不设置的话,follower 将会无限的延伸。
  • 为了不遮挡 TextFormField ,我们给 CompositedTransformFollower 设置了一个offset(和之前一样的原因)。
  • 最后,我们将 showWhenUnlinked 属性设置为 false,当 TextFormField 在屏幕上不可见时,用来隐藏OverlayEntry(比如我们滑动出屏幕底部很远的时候)。

经过这些操作,我们的 OverlayEntry 现在可以跟随我们的 TextField 一起滚动啦!

重要提示:CompositedTransformFollower 仍然有一点问题,当 target 不可见时,即使 follower 已经从屏幕上隐藏了,这个 follower 仍然会响应点击事件,我已经给 Flutter Team 提了 issue ,此 issue 已经关闭,标记为解决。

Overlay 是一个强大的 Widget ,它给我们提供了一个简单易用的的用来展示浮动 Widget 的的 Stack 组件。

参考链接