1 Star 0 Fork 0

lrq/flutter-interview

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
lrq update 8fd5af4 3个月前
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

Flutter-Interview

QuestionList

  • TabBarView + AutomaticKeepAliveClientMixin 的结合:
  • TabBarView 仍然是原生的切换逻辑,保留了滑动交互的体验。
  • 在 QuestionList 中使用 AutomaticKeepAliveClientMixin,确保每个页面只初始化一次,并在切换时保持其状态。
  • 这样既避免了重复初始化组件,也确保了组件状态的持久性。
  • 分页加载逻辑:
  • 使用了 _questionList 来保存已加载的数据,并通过 _currentPage 和 _pageTotal 控制分页逻辑。
  • 在 _onRefresh 和 _onLoading 中调用 fetchQuestions,实现了下拉刷新和上拉加载更多的功能。
  • 自动触发首次加载:
  • 在 initState 中通过 WidgetsBinding.instance.addPostFrameCallback 确保第一次加载在页面初始化完成后触发。
  • 页面重用与性能优化:
  • ListView.builder 和分页的结合确保了大数据列表的渲染性能。
  • SmartRefresher 用于提供下拉刷新和加载更多的体验,封装了控件的交互逻辑。
特性 Flutter (AutomaticKeepAliveClientMixin) Vue (keep-alive)
缓存粒度 页面级别,每个 StatefulWidget 独立缓存 组件级别
实现方式 使用 Mixin 来告知框架保留 State 指令 管理缓存
生命周期管理 页面的生命周期完全由开发者控制 提供 activated 和 deactivated 钩子
使用场景 TabBarView 等需要频繁切换的场景 动态组件切换和性能优化场景
性能消耗 较低,State 的持久性直接由框架控制 缓存的虚拟 DOM 占用更多内存

PageView + WebView 的手势冲突

1. GestureDetector 与 PageView 手势冲突

  • GestureDetector 的行为:
    • GestureDetector 会直接 拦截手势事件,这意味着它会优先处理用户的手势,而不是将手势事件传递给其他组件。
    • 在 PageView 默认的手势切换逻辑中,如果手势被 GestureDetector 拦截,那么 PageView 的滑动逻辑就不会被触发。
    • 同样地,当 PageView 自己处理了手势事件时,会优先触发它的翻页操作,而不是把手势传递到 WebView。
  • 手势冲突的本质: GestureDetector 的拦截特性加上 PageView 的内部手势监听机制,导致 WebView 的手势被“抢占”,无法生效。

2. 使用 Listener 的优势

  • Listener 的特点:

    • Listener 不拦截手势事件,而是监听原始的手势事件(如 PointerDownPointerMovePointerUp )。
    • 它不像 GestureDetector 那样会打断事件传递链,因此可以让 PageViewWebView 都有机会处理事件。
  • 结合 PageView 的方案:

    • 禁用 PageView 的默认切换事件(通过 physics: NeverScrollableScrollPhysics()),确保 PageView 不会“抢占”手势。
    • 通过 Listener 来监听用户的滑动方向(水平还是垂直),根据滑动方向手动触发 PageView 翻页或将事件交给 WebView。

3. 改造的逻辑分析

  • 最终的改造逻辑是:

    • 禁用 PageView 的默认滑动切换行为。
    • 使用 Listener 来捕获手势事件,而不是 GestureDetector ,从而避免拦截。
    • 根据手势方向,判断事件的处理方式:
    • 如果是水平滑动( deltaX 大于 deltaY ),手动触发 PageView 的翻页。
    • 如果是垂直滑动( deltaY 大于 deltaX ),允许 WebView 自然处理滑动事件。
    • 实现了一个统一的逻辑控制,避免 PageViewWebView 因手势冲突而影响用户体验。

4. 优势总结

  • 解决了冲突:
    • 通过禁用默认切换,手动监听手势实现了对手势事件的统一管理。
    • 避免了 GestureDetector 对手势事件的强拦截,确保 WebView 可以正常响应垂直滑动。
  • 扩展性强:
    • 使用 Listener 的方式更灵活,可以轻松拓展为处理其他类型的手势(如双指缩放等)。

5. 代码优化的方向

  • 手势判断逻辑:对于复杂的滑动场景(如 WebView 内部缩放),可以进一步细化方向判断逻辑。
  • 性能优化:Listener 是一个较低级的组件,建议在监听逻辑中做更多的优化,避免误触。 自定义动画:当手动切换 PageView 时,可以加入一些切换动画效果(例如使用 PageController 的 animateToPage 方法)。

一句话总结

GestureDetector 会直接进行拦截手势事件 而在PageView默认的切换事件时 PageView的控件会把手势事件拦截处理 从而导致webview的手势无法触发 要解决PageView + WebView 目前使用方案是 禁用默认的Pageview切换事件 使用 Listener 对整体进行监听手势事件 而不是进行拦截 webview的滑动和pageview的切换逻辑全部改用手写 这样就可以实现功能

响应式设计

setState

getx

在 GetX 中,响应式系统 是通过 Rx 类型(如 RxInt、RxMap 等)来实现数据的自动更新的,但其工作机制与具体的 数据引用 和 响应式对象的通知规则 有紧密的关系。

三个关键点

  • 响应式数据的通知机制
  • 直接修改 Rx 对象中的属性值 和 整体赋值更新的区别
  • 复杂数据类型(如 RxMap 或对象数组)如何触发 GetX 的响应式更新

数据通知机制

在 GetX 中,响应式系统的核心原理是通过 观察者模式 监听数据变化。当你修改了 Rx 包裹的数据时,GetX 会自动通知使用了这个数据的 UI 更新。

  • Rx 对象 如 RxInt、RxMap、RxList 等,本身就是响应式的。
  • 赋值 或者 调用 .refresh() 会触发观察者通知,使依赖于该数据的 UI 自动更新。

特殊情况 RxMap

  • RxMap 和引用型数据的行为
  • RxMap 是响应式的,但当你 直接修改其内部对象的属性 时,GetX 不会自动检测到变化。
  • 这是因为 GetX 只监控对 RxMap 整体引用 的修改,而不会深入监听 RxMap 中每个对象的属性变化。
  • 内部对象的属性改变无法直接触发通知,必须手动调用 refresh()

复杂数据类型的响应式设计

  • 对于复杂数据类型(如对象数组或 RxMap),GetX 的响应式系统有以下特点:
    • RxMap 和 RxList 本身是响应式的:
    • 对整个 RxMap 或 RxList 进行赋值,会触发 UI 更新。
  • 示例:
questionDetails['123'] = QuestionDetail(...); // 触发响应式更新
  • 修改内部对象的属性不会触发响应式更新:
    • GetX 默认不会递归监听 RxMap 或 RxList 内部对象的属性变更。
    • 手动调用 .refresh():
  • 当你修改 RxMap 内部对象的属性时,需要调用 refresh() 来触发整体更新。
  • 示例:
questionDetails['123']!.likeCount.value += 1;
questionDetails.refresh(); // 强制触发响应式更新
  • 使用 RxList 的操作方法触发更新:

    • RxList 提供了一些操作方法(如 .assignAll、.remove 等)来直接触发更新。
  • 最佳实践

    • 将内部属性也使用 Rx 包裹:
    • 确保每个需要监听的属性都是 Rx 类型。 例如:
class QuestionDetail {
  final RxInt likeCount;
  final RxInt likeFlag;

  QuestionDetail({required this.likeCount, required this.likeFlag});
}

对于对象数组或 RxMap,使用手动 refresh():

修改对象内部属性后,调用 refresh() 触发整体更新。

  • 统一管理数据变更逻辑:

封装方法修改数据并调用 refresh(),避免在多个地方重复手动刷新。

持久化存储

permanent: true(GetX 内存持久化实例)

  • Get.put(controller, permanent: true) 会将 Controller 或数据实例保存在内存中,且在应用生命周期内不会被销毁。
  • 优点:
    • 速度最快:数据存储在内存中,读写速度极快,因为内存是访问延迟最小的存储介质
    • 简单易用:无需额外配置,即可通过 Get.find() 访问同一实例。
    • 适合状态管理:适用于全局数据、临时状态共享,如页面间通信、用户状态等。
  • 缺点
    • 如果使用频繁,可能造成内存泄漏,内存过大,影响页面性能等
    • 占用内存:数据量大时会持续占用内存,无法自动释放,可能导致内存占用过高
    • 非持久化:数据存储在内存中,应用重启或崩溃后数据会丢失。
    • 生命周期受限:仅在应用运行期间有效。
  • 场景
    • 页面状态管理
    • 用户会话、临时缓存。
    • 数据量不大,且对持久化没有要求等需要频繁读取的状态

GetStorage (GetX 内置本地存储)

await GetStorage.init("question_details_box");
final GetStorage box = GetStorage("user_box");
  • 优点:
    • 持久化存储:数据会存储到本地文件系统,应用重启后仍能保持数据。
    • 轻量级:非常适合存储小型数据(键值对),类似于 SharedPreferences。
    • 速度快:通过内存缓存加速访问,同时支持文件存储,性能较高。
  • 缺点
    • 不适合大数据:主要用于键值对存储,处理复杂数据结构时可能需要手动序列化。
    • 依赖本地存储:虽然访问速度较快,但不如内存直接操作快。

SharedPreferences

  • SharedPreferences 是原生 Android/iOS 提供的本地键值对存储方式,Flutter 中通过 shared_preferences 插件封装使用。
  • 优点
    • 稳定性高:作为原生 API 封装,广泛被使用,成熟稳定。
    • 持久化存储:数据存储到本地文件系统,应用重启后数据不会丢失。
    • 简单轻量:适合存储简单的键值对数据,如布尔值、字符串、数字等。
  • 缺点
    • 性能较低:读取和写入速度比内存慢,尤其在数据量大时。
    • 功能有限:不支持复杂数据结构存储,需要手动序列化和反序列化(如 JSON)。
    • 平台依赖:依赖平台实现,可能会有平台特定的 bug。
指标 permanent: true GetStorage SharedPreferences
速度 ⭐⭐⭐⭐(最快,内存操作 ⭐⭐⭐(本地缓存+文件) ⭐⭐(文件存储,速度较慢)
持久化能力 无,数据重启后丢失 有,应用重启数据不丢失 有,应用重启数据不丢失
存储容量 受限于内存 适合存储小数据(KB 级) 适合存储小数据(KB 级)
适合数据类型 任何数据结构 键值对(支持简单和序列化数据 键值对(支持简单和序列化数据
生命周期 应用运行期间 持久化到文件,随时可读取 持久化到文件,随时可读取
依赖性 依赖 GetX 依赖 GetX 依赖平台和第三方插件
内存占用 高,数据在内存中 中,依赖文件缓存 中,依赖文件缓存

总结分析

  • 频繁读取、运行时状态共享 permanent: true 内存存储速度快,适合状态管理。
  • 小型持久化数据存储 GetStorage 简单易用,支持本地文件持久化。
  • 稳定的键值对存储 SharedPreferences 成熟稳定,但速度略低。
  • 复杂对象/大数据存储 自定义存储方案 以上三种方案均不适合处理大规模数据。

输入框+长列表布局

场景:

TextField聚焦时弹出键盘,会导致ExpandedSmartRefresher布局错乱,错乱表现是

  • 键盘弹出导致布局变化 flutter布局会重新计算
  • SmartRefresher 的滚动监听受到键盘干扰
  • 输入框可能引发多次状态刷新
  • 输入框与列表在同一页面竞争焦点 原因是弹出键盘时flutter布局会重新计算 (尤其是 MediaQuery.of(context).viewInsets.bottom 的值发生变化),直接影响 SmartRefresher 的触底机制(底部位置计算发生偏移)

解决方案1

  • TextFieldExpanded+SmartRefresher拆分开页面,单独用一个SearchResult结果页去单独展示结果列表,避免这两个组件在一个页面展示,是最安全的解决方案。
  • 鉴于项目的controller设计,可以把controller初始化的时机放在 TextFieldSearchResult专注于结果展示和触发下拉刷新,上拉加载更多。

解决方案2

  • 降低触发搜索频率,将onChange事件的value记录下来,而不是直接对value进行搜索,保证有输入框时,不渲染SmartRefresher
  • 在触发onSearch事件前手动关闭键盘,这样最大程度上保证布局不会被键盘弹出所影响。

性能优化

渲染层面

首屏渲染

  • 在应用首页需要初始化请前唤起进度条弹框并使用单独的私有变量_isloading进行标记(默认是true,请求完成且客户端处理完成后变成false),在build函数进行判断,若_isLoading为true时,展示一个空容器(空容器SizedBox.shrink() 基本不占用任何渲染资源;也可以使用骨架屏,但就不需要进度条弹框,两种方案选一种),待客户端处理接口完成后,展示真正需要渲染的Widget组件。
  • 首页一些需要展示的静态组件,(轮播图、banner等),在进入Home 前就开始使用预加载,不管是静态图还是网络图片,都最大程度上保证加载速度。

长列表

  • 如果有分类或者其他标记点,父级组件使用TabBar+TabBarView;反之使用PageView,列表组件统一用SmartRefresher ;
  • 首先数据在GetxController进行维护(不需要getxStorage),如果是分类那么用RxMap(满足列表响应式),对每个分类的数据列表进行处理。每个Map 是由分类的唯一标识 + State维护,State包括list(需要渲染的数据源)、isLoading(是否在加载中的标记)、isFinished(当前分类是否加载完毕的标记)、currentPage(当前页)、pageTotal(总页数或总数量,最好要求服务端返回)。
  • 长列表数据的请求方法有两个 refresh刷新和loadMore加载,区别:loadMore专注于加载更多,触发时在isLoadingisFinishedtrue时都需要return,避免触发过于频繁,之后发起网络请求前让isLoadingtrue,请求完成后isLoadingfalserefresh专注于刷新,执行前先把list请空、currentPage改为0,isFinished改为false,之前再发起网络请求;每次网络请求发起后,都去把对应Statelist补充上,判断当前页是否是最后一页,如果不是让当前页++,如果是让isFinished变成true
  • 渲染时,使用AutomaticKeepAliveClientMixin 缓存页面状态,初始化时先去加载当前的State,然后渲染当前的Statelist,遇到isLoading那么展示加载中的动画,同时处理_refreshController控制器,对加载完成、加载完毕,执行对应的方法,展示完全定制的HeaderFooter

多图片、多视频

  • 多图片场景:
    • 使用 CachedNetworkImage 实现图片的懒加载与缓存处理。
    • 对大规模图片列表,研究使用 flutter_staggered_grid_view 等库以优化图片的布局和加载性能。
  • 多视频场景:
    • 视频播放器推荐使用 video_player 或基于其封装的库(如 chewie),可以支持预加载、暂停、恢复等功能。
    • 如果是长视频列表,可以结合 PreloadPageView 实现预加载,同时动态释放不需要的视频资源,避免内存占用过高。

应用动画

  • 统一应用的动画风格:页面路由动画,弹框、提示框的显示隐藏动画,删除列表项的动画
  • 封装一个或多个通用的动画类,content参数对外暴露,可传入Widget, 将需要统一的风格全部统一

应用反馈(loading、弹框等)

  • 可分为flutter原生和通过Getx衍生出的弹框,现主要讨论flutter原生。
  • loading分为圆形、和进度条形有不同的使用场景。进度条用OverlayEntry搭建(有蒙层,展示时不允许点击其他区域),用于初进应用、初进某个页面时进行展示,短时间内不会再出现;圆形的LoadingCupertinoActivityIndicator搭建(也可以写Lottie动画),主要是Widget组件,是页面展示的一部分,主要用在加载列表、刷新页面时,可以点击其他任一区域。
  • ConFirm弹框也进行封装,Title Content ConfirmText CancalText等都是可配置,按钮的点击回调,弹框的整体样式都可定制。
  • Toast轻提示也进行统一封装,是否展示Icon Message position duration都对外进行暴露
  • 各类按钮和按钮的点击、双击、长按、按下的反馈都封装到RippleButton,统一对应用所有的可点击Widget进行包裹,并对外暴露点击、双击、长按、按下事件

应用刷新机制

  • 应用中使用的场景是:token已经过期,用户仍请求了某些请求后,服务端返回状态码为401,该接口的数据未正常返回。
  • 处理方式是将这些请求记录下来,重新登录后,再次请求。
  • 还有一种方式是,通过providergetx做一个刷新的标记,将其注入到build函数中,将这个标记的值变为true时,重新请求一次接口,注意一定要把接口单独写成一个函数。

WebView

  • 加载本地html,使用evaluateJavascript渲染接口返回的js代码,同时通过 InAppWebView 提供的回调监听 JS 执行结果
  • 加载远程url
  • 混合开发WebViewFlutter应用通信,可以使用 WebView 提供的 JS Bridge(如 JavascriptChannel)实现通信。
// JS 发送消息给 Flutter
window.flutter_inappwebview.callHandler('methodName', arg1, arg2);

// Flutter 调用 JS
controller.evaluateJavascript(source: "yourFunction()");

网络请求

封装统一的Http

  • 请求拦截器、响应拦截器
  • 特殊的状态码(401)、特殊的网络请求(无须校验token)...,都统一在Http类中做出封装,避免在业务代码中写
  • 对于请求量大的接口(上传接口或下载大文件)时,如果后端支持Content-Length 那么在客户端的进度条展示,如果服务端没有该字段,客户端可以用Steam自行模拟。

对报错进行统一处理

  • 遇到和服务端约定好的状态码(401:token过期),在客户端展示的逻辑抽离到一个公共方法类中。
  • 如果是用户登录凭证过期,那么重新登录后,把之前401的请求记录下来,用户重新登录后,立刻重新调用401的请求。

多接口并发请求

  • 使用 Future.wait 同时触发多个接口请求,也可以监听每个请求的状态
  • 在并发请求中,使用错误捕获组(如 try-catchrunZonedGuarded)防止接口失败导致整体崩溃。

数据模型

数据响应式

  • Flutter是单向数据流,不像RN Uniapp ,所以有些重新需要手动去实现一个数据响应式;场景是父子组件如何数据同步、祖先组件和后代组件的数据同步。比较难实现的是复杂的数据类型,如通过RxMap维护的长列表,里面的List中的某个对象里的views字段在子组件修改了,父组件没有更新。
  • 实现的步骤需要在父组件和子组件的controller中分别都暴露出一个updateViews的方法,需要父子组件的controller中有一个独特的唯一标识如typeId,进行父子组件内修改views后同步到另一方。

跨组件事件通信

  • 两个组件、页面之间没有任何关系,完全是独立的,但是需要在一方修改了状态后,另一方需要更新或触发某方法。
  • 目前用到的是getx,独特的标记controller,通过修改该标记的值,触发某方法。
  • ... 可能也有事件总线的机制

本地存储

  • 将一些数据量大、更新频率不会非常高的组件如PageView依赖的数据源、搜索历史、动态添加的tabbar、用户信息、用户的独特配置(主题、语言)等用GetxStoage进行缓存,从而提高用户的体验。
  • GetxStoage只能存储键值对,大小应当是kb,所以需要对存储的数据进行json化,如果是可修改、可删除、可添加等,还需要对RxList进行转义处理。
  • 所有存储的列表类数据都做了条数限制(例如搜索历史累计20条后,会覆盖最先添加的数据)
  • sqlLite暂时未用到

性能监测

  • 对应用启动的各项指标进行标记,计算每个阶段的耗时(ms),以便开发时调试、优化。断点的阶段有入口文件——加载config——配置主题、语言——渲染第一帧等
  • 如进入入口文件到flutter 框架初始化 。mainStart to Flutter-Initialized: 133ms
  • 还可以对每个页面、每个时刻的gpu等进行监测

日志上报

  • 封装统一的日志工具Logger,提供四个等级的日志,在输出时添加Tag 当前时间 日志等级 以及对四个等级进行颜色区分 。(Info warn error debug
  • 其中Error等级日志添加stackTrace ,提供上下文和详细调用路径。
  • 开发环境不做多余处理,生成环境时,过滤掉所有的日志,除Error等级日志,全部过滤,并将Error等级日志全部强制上传到服务端。
  • 一般的埋点调试都是对用户行为分析,如支付页面停留时间、哪些页面浏览时间长、哪些关键页面打开多少次,这些统计在客户端进行GetxStoage进行缓存,累计到一定条数(20)再进行上报服务端,同时这个方法也提供强制上报的逻辑,用来排查线上用户遇到问题,即使处理。

框架对比

比较指标 vue2/3(uniapp) react(react-native) flutter
架构模式 单向数据流 单向数据流 单向数据流
响应式原理 vue2通过Object.defineProperty实现,拦截每一个对象,通过观察者模式,监听每个属性。vue3通过proxy监听所有的对象,深层次监听,无论是子改父还是父改子都会驱动UI更新 没有内建属性监听机制,依赖于状态更新,一般是父子组件通过自定义事件的传递修改状态来驱动UI更新。修改对象时,需要改变对象的引用,而不仅仅是内部属性。setState(()=>{...data,k="key"}) 通过Widget树构建UI,状态的变化触发 UI 重建,和react类似,不能仅修改对象的内部属性,要修改对象的引用,才会驱动UI更新。
状态管理工具 vuex pinia redux jotai getx
组件通信 自定义事件、自定义属性; props传递; 自定义事件传递;
架构设计 通过vuexpinia 通过redux jotai 使用getx建议每个数据模型的controller,统一数据控制层,所有用到该数据的页面都用同一套数据来统一管理,使用Obx Rx 实现响应式更新

react / flutter

1. React 的机制:状态驱动 UI

在 React 中,UI 是由状态驱动的。无论状态如何发生变化,只要调用 useState 的 setter 方法(函数组件),React 会:

  1. 将状态的改变标记为“脏”。
  2. 根据新的状态重新渲染组件,生成新的虚拟 DOM。
  3. 比较新旧虚拟 DOM(Diff 算法),只更新真正变化的部分到真实 DOM 中。

具体流程:

  • 父组件定义状态并将其传递给子组件,同时提供 setState 方法。
  • 子组件通过调用传递下来的 setState 方法更新父组件的状态。
  • 状态更新后,React 检测到状态变化,重新渲染父组件(以及子组件)。
  • UI 根据新的状态更新。

2. Flutter 的机制:Widget 树与状态驱动 UI

在 Flutter 中,UI 由 Widget 树构建,状态变化通过 StatefulWidget 中的 setState 方法触发。和 React 类似,Flutter 也使用“状态驱动 UI”的思想,但它和 React 的根本区别在于:

  • Flutter 不会主动监听对象的变化(除非使用响应式工具)
    • React 会自动检测状态变化,且状态由引用的变化触发。
    • Flutter 的 setState 必须明确告知框架:某些状态已发生变化,重建 Widget 树。
  • Flutter 没有虚拟 DOM
    • Flutter 不需要 Diff 比较新旧状态,而是直接通过 Widget.build() 方法重建树。
    • 但只有在调用 setState 或类似的方法后,Flutter 才会重新执行 build

问题:为什么类似 React 的做法在 Flutter 中无效?

如果在 Flutter 中,父组件将某个方法(如 setState)传递给子组件,并在子组件中调用该方法,这种做法本质上只是调用了一个方法,而不会自动触发 UI 的更新,除非你显式地调用父组件的 setState

这是因为:

  1. Flutter 并不像 React 那样有状态变更的监听机制。
  2. Flutter 的状态更新完全依赖于显式调用 setState 来触发重建。

vue / flutter

  • 观察者模式:都通过观察数据的变化来驱动 UI 更新。
  • 双向绑定体验:GetX 的响应式对象(Rx 系列)在使用时表现得像 Vue 的双向绑定,你可以直接修改数据,UI 就会自动更新。

用GetX 实现 的 Vue 系列响应式

  1. 性能: GetX 的响应式更新机制更高效,它可以精准更新受影响的组件,而不是整个页面重绘。相比之下,uni-app 的响应式机制在性能优化上较弱(尤其是数据变化较频繁时)。
  2. 开发效率: GetX 使用简洁,避免了过多的模板约束,同时对复杂业务逻辑的状态管理更友好。而 uni-app 虽然有 Vue 的开发体验,但其生态和框架的灵活性较差。
  3. 原生能力: Flutter 的原生性能无疑比 uni-app(基于 WebView 和小程序容器)更强。尤其在复杂交互、高性能场景下,Flutter + GetX 是远胜 uni-app 的组合。

空文件

简介

flutter 版本 interview 展开 收起
取消

发行版

暂无发行版

贡献者

全部

近期动态

不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/lin-ruiqiang/flutter-interview.git
git@gitee.com:lin-ruiqiang/flutter-interview.git
lin-ruiqiang
flutter-interview
flutter-interview
main

搜索帮助