flutter滚动视图之ScrollController源码解析(三)

ScrollControllerCallback

arduino 复制代码
// 示例假设:
// TrackingScrollController _trackingScrollController = TrackingScrollController();

/// 当 [ScrollController] 添加或移除一个 [ScrollPosition] 时的回调签名。
///
/// 由于 [ScrollPosition] 只有在 [Scrollable] 构建完成后才会创建并附加到控制器,
/// 因此可以用它来响应位置被附加到控制器的事件。
///
/// 通过直接访问该位置,可以对滚动位置的某些方面应用额外的监听器,
/// 比如 [ScrollPosition.isScrollingNotifier]。
///
/// 该回调由 [ScrollController.onAttach] 和 [ScrollController.onDetach] 使用。
typedef ScrollControllerCallback = void Function(ScrollPosition position);

这个注释解释了 ScrollControllerCallback 类型的作用,即当一个 ScrollPosition 被附加或从一个 ScrollController 移除时,触发的回调函数。

ScrollController

scala 复制代码
/// 控制可滚动的 Widget。
///
/// 滚动控制器通常作为成员变量存储在 [State] 对象中,并在每次 [State.build] 时重用。
/// 一个滚动控制器可以用于控制多个可滚动的 Widget,
/// 但是某些操作,比如读取滚动的 [offset],要求控制器只能与单个可滚动的 Widget 一起使用。
///
/// 滚动控制器会创建一个 [ScrollPosition] 来管理与单个 [Scrollable] Widget 相关的状态。
/// 如果想使用自定义的 [ScrollPosition],可以通过继承 [ScrollController] 并重写 [createScrollPosition] 来实现。
///
/// {@macro flutter.widgets.scrollPosition.listening}
///
/// 通常与 [ListView]、[GridView]、[CustomScrollView] 一起使用。
///
/// 另请参阅:
///
///  * [ListView]、[GridView]、[CustomScrollView],它们可以由 [ScrollController] 控制。
///  * [Scrollable],是一个更底层的 Widget,它会创建并将 [ScrollPosition] 对象与 [ScrollController] 对象关联。
///  * [PageController],一个类似的对象,用于控制 [PageView]。
///  * [ScrollPosition],管理单个滚动 Widget 的滚动偏移量。
///  * [ScrollNotification] 和 [NotificationListener],可以用来监听滚动事件,而不需要使用 [ScrollController]。
class ScrollController extends ChangeNotifier {
  /// 为可滚动的 Widget 创建一个控制器。
  ScrollController({
    double initialScrollOffset = 0.0,
    this.keepScrollOffset = true,
    this.debugLabel,
    this.onAttach,
    this.onDetach,
  }) : _initialScrollOffset = initialScrollOffset {
    if (kFlutterMemoryAllocationsEnabled) {
      ChangeNotifier.maybeDispatchObjectCreation(this);
    }
  }
}

initialScrollOffset

dart 复制代码
/// 用于 [offset] 的初始值。
///
/// 新创建并附加到此控制器的 [ScrollPosition] 对象将其偏移量初始化为此值,
/// 如果 [keepScrollOffset] 为 false 或者滚动偏移量尚未保存。
///
/// 默认为 0.0。
double get initialScrollOffset => _initialScrollOffset;
final double _initialScrollOffset;

描述了 initialScrollOffset 的作用,它是 ScrollController 的一个属性,表示初始的滚动偏移量。它会用于新创建并附加到控制器的 ScrollPosition 对象,只有在以下两种情况下,它才会被设置为这个初始值:

  1. keepScrollOffsetfalse
  2. 滚动偏移量尚未被保存。

默认值是 0.0,即初始时滚动视图的偏移量为 0。

keepScrollOffset

arduino 复制代码
/// 每次滚动完成时,将当前的滚动 [offset] 保存到 [PageStorage],
/// 并在该控制器的可滚动控件被重新创建时恢复该偏移量。
///
/// 如果此属性设置为 false,则滚动偏移量不会被保存,
/// 并且始终使用 [initialScrollOffset] 来初始化滚动偏移量。
/// 如果设置为 true(默认值),则初始滚动偏移量会在控制器的可滚动控件第一次创建时使用,
/// 因为那时还没有需要恢复的滚动偏移量。之后保存的偏移量会被恢复,
/// 此时 [initialScrollOffset] 会被忽略。
///
/// 另请参阅:
///
///  * [PageStorageKey],当同一路由中有多个可滚动控件时,应该使用它来区分用于保存滚动偏移量的 [PageStorage] 位置。
final bool keepScrollOffset;

它决定了是否将滚动偏移量保存到 PageStorage 中,并在重新创建可滚动控件时恢复该偏移量。如果该属性为 true(默认值),控件的滚动位置会在控件重建时恢复;如果为 false,则总是使用 initialScrollOffset 来初始化滚动位置,而不会保存偏移量。

onAttach

perl 复制代码
/// 当 [ScrollPosition] 附加到滚动控制器时调用。
///
/// 由于在 [Scrollable] 实际构建之前,滚动位置是不会被附加的,
/// 因此可以用它来响应新的滚动位置被附加的事件。
///
/// 在滚动位置附加时,像 [ScrollMetrics.maxScrollExtent] 这样的 [ScrollMetrics] 还不可用。
/// 这些信息直到 [Scrollable] 完成布局并计算出内容的完整范围后才会确定。
/// 可以使用 [ScrollPosition.hasContentDimensions] 来知道何时可以获取这些度量,
/// 或者可以使用 [ScrollMetricsNotification],下面会进一步讨论。
///
/// {@tool dartpad}
/// 此示例展示了如何使用 [ScrollController.onAttach] 为 [ScrollPosition.isScrollingNotifier] 添加监听器,
/// 用于在滚动发生时更改 [AppBar] 的颜色。
///
/// ** 请参阅 examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart 中的代码 **
/// {@end-tool}
final ScrollControllerCallback? onAttach;

描述了 onAttach 回调的作用,它会在一个 ScrollPosition 被附加到 ScrollController 时触发。这对于响应新附加的滚动位置很有用。

特别地,在 ScrollPosition 被附加时,ScrollMetrics 还未准备好(例如,maxScrollExtent),因为这些值在 Scrollable 完成布局并计算内容的完整范围后才会确定。可以通过 ScrollPosition.hasContentDimensions 来确认这些度量值是否已准备好,或者使用 ScrollMetricsNotification 来监听这些度量值。

onDetach

arduino 复制代码
/// 当 [ScrollPosition] 从滚动控制器中分离时调用。
///
/// {@tool dartpad}
/// 此示例展示了如何使用 [ScrollController.onAttach] 和 [ScrollController.onDetach] 为
/// [ScrollPosition.isScrollingNotifier] 添加监听器。
/// 该监听器用于在滚动发生时更改 [AppBar] 的颜色。
///
/// ** 请参阅 examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart 中的代码 **
/// {@end-tool}
final ScrollControllerCallback? onDetach;

描述了 onDetach 回调的作用,它会在一个 ScrollPositionScrollController 中分离时触发。这个回调通常用来响应滚动位置被分离的事件。

同样,注释中还给出了一个示例,展示了如何使用 onAttachonDetach 监听 ScrollPosition.isScrollingNotifier,在滚动发生时改变 AppBar 的颜色。

positions

ini 复制代码
/// 当前附加的滚动位置。
///
/// 不应直接修改此属性。[ScrollPosition] 对象可以通过 [attach] 和 [detach] 方法
/// 添加或移除。
Iterable<ScrollPosition> get positions => _positions;
final List<ScrollPosition> _positions = <ScrollPosition>[];

定义了一个 getter 方法 positions,它返回当前附加到控制器的所有 ScrollPosition 对象的列表。注释强调了不应该直接修改这个列表,而应该通过 attachdetach 方法来添加或移除 ScrollPosition

hasClients

ini 复制代码
/// 是否有任何 [ScrollPosition] 对象通过 [attach] 方法附加到
/// [ScrollController]。
///
/// 如果为 false,则必须避免调用与 [ScrollPosition] 交互的成员,
/// 例如 [position]、[offset]、[animateTo] 和 [jumpTo]。
bool get hasClients => _positions.isNotEmpty;

定义了一个 hasClients 属性,它检查是否有任何 ScrollPosition 对象已经通过 attach 方法附加到 ScrollController。如果没有附加任何 ScrollPosition(即 hasClientsfalse),则不应调用与滚动位置相关的功能,比如 positionoffsetanimateTojumpTo,因为这些操作依赖于有效的 ScrollPosition

position

dart 复制代码
/// 返回附加的 [ScrollPosition],通过它可以获取 [ScrollView] 的实际滚动偏移量。
///
/// 只有在仅附加了一个滚动位置时,调用此方法才是有效的。
ScrollPosition get position {
  assert(_positions.isNotEmpty, 'ScrollController 没有附加到任何滚动视图。');
  assert(_positions.length == 1, 'ScrollController 附加了多个滚动视图。');
  return _positions.single;
}

定义了一个 position 属性,它返回附加的 ScrollPosition 对象,进而可以获取滚动视图的实际滚动偏移量。注释说明:

  1. 只有当 仅有一个 滚动位置被附加时,这个属性才是有效的。
  2. 使用 assert 来确保在调用 position 时,_positions 列表中仅包含一个滚动位置,如果没有附加任何滚动位置或者附加了多个滚动位置,则会触发断言错误。

这个属性用于访问当前滚动视图的实际滚动位置。

offset(非常重要)

arduino 复制代码
/// 当前可滚动 Widget 的滚动偏移量。
///
/// 需要确保控制器正在控制一个且仅一个可滚动的 Widget。
double get offset => position.pixels;

定义了 offset 属性,它返回当前滚动视图的滚动偏移量(即 position.pixels)。需要注意的是,该属性要求控制器只能控制一个可滚动的 Widget。如果控制器控制了多个可滚动视图,调用该属性时会出现问题。

animateTo

dart 复制代码
/// 将当前位置从当前值动画到给定的值。
///
/// 任何活动的动画都会被取消。如果用户当前正在滚动,该操作也会被取消。
///
/// 返回的 [Future] 会在动画结束时完成,无论是成功完成还是被提前中断。
///
/// 动画会在以下情况下被中断:
/// - 用户尝试手动滚动时。
/// - 启动了另一个活动时。
/// - 动画到达视口边缘并尝试超出滚动范围时。  
///   (如果 [ScrollPosition] 不进行超滚动,而是允许滚动超出边界,
///    那么超出边界将不会中断动画。)
///
/// 动画不受视口或内容尺寸变化的影响。
///
/// 动画完成后,滚动位置将尝试开始一个弹性活动,以防其值不稳定
/// (例如,如果滚动超出了边界,通常会有回弹效果)。
///
/// 动画持续时间不能为零。如果需要跳转到特定值而不使用动画,
/// 请使用 [jumpTo]。
///
/// 在小部件测试中调用 [animateTo] 时,`await` 返回的 [Future] 可能会导致测试挂起并超时。  
/// 请改为使用 [WidgetTester.pumpAndSettle]。
Future<void> animateTo(double offset, {required Duration duration, required Curve curve}) async {
  assert(_positions.isNotEmpty, 'ScrollController 没有附加到任何滚动视图。');
  await Future.wait<void>(<Future<void>>[
    for (int i = 0; i < _positions.length; i += 1)
      _positions[i].animateTo(offset, duration: duration, curve: curve),
  ]);
}

代码描述了 animateTo 方法的功能,它用于将滚动位置动画地移动到指定的偏移量 offset。以下是其主要要点:

  1. 动画取消:任何正在进行的动画会被取消,用户滚动或其他活动也会中断当前动画。
  2. 返回 Future :动画结束时返回一个 Future,无论动画是否成功完成,都会触发 Future 的完成。
  3. 动画中断条件:动画会在几种情况下中断,例如手动滚动、启动其他活动或尝试超出滚动边界。
  4. 动画稳定性:动画结束后,如果滚动位置不稳定(如超出边界),会触发回弹动画。
  5. 动画时长duration 必须大于零。如果想要立即跳转到目标位置,可以使用 jumpTo 方法。
  6. 小部件测试 :在小部件测试中,直接 await 动画的 Future 可能导致超时错误。推荐使用 WidgetTester.pumpAndSettle 代替。

jumpTo

arduino 复制代码
/// 将滚动位置从当前值直接跳转到给定值,
/// 不使用动画,也不检查新值是否在范围内。
///
/// 任何活动的动画都会被取消。如果用户当前正在滚动,该操作也会被取消。
///
/// 如果此方法更改了滚动位置,将会触发一系列的开始/更新/结束滚动通知。 
/// 但无法通过此方法生成超滚动通知。
///
/// 跳转后,立即启动一个弹性活动,以防值超出了滚动范围。
void jumpTo(double value) {
  assert(_positions.isNotEmpty, 'ScrollController 没有附加到任何滚动视图。');
  for (final ScrollPosition position in List<ScrollPosition>.of(_positions)) {
    position.jumpTo(value);
  }
}

代码定义了 jumpTo 方法,用于将滚动位置 立即跳转 到指定的值,且不会有动画效果,也不会检查目标值是否在允许的滚动范围内。以下是关键点:

  1. 取消动画:如果有正在进行的动画,它会被取消。
  2. 取消滚动:如果用户正在手动滚动,也会取消当前的滚动操作。
  3. 滚动通知:如果滚动位置发生变化,会触发一系列滚动通知(开始、更新、结束),但不会生成超滚动通知。
  4. 弹性活动:跳转后会启动一个弹性活动,防止值超出范围时出现不稳定的滚动行为。

总之,jumpTo 允许直接跳转到目标位置,适用于需要立即定位的场景,但不会执行平滑过渡或边界检查。

attach

scss 复制代码
/// 将给定的滚动位置注册到此控制器。
///
/// 当此方法返回后,[animateTo] 和 [jumpTo] 方法将操作给定的滚动位置。
void attach(ScrollPosition position) {
  assert(!_positions.contains(position));
  _positions.add(position);
  position.addListener(notifyListeners);
  onAttach?.call(position);
}

定义了 attach 方法,它将一个 ScrollPosition 对象注册到 ScrollController 上。以下是其主要要点:

  1. 注册位置 :首先,它会确保该 ScrollPosition 尚未添加到控制器中(通过断言)。
  2. 将位置添加到列表 :将 ScrollPosition 添加到 _positions 列表中,意味着控制器开始管理这个位置。
  3. 添加监听器 :为该 ScrollPosition 添加一个监听器,以便控制器可以通知相关变化。
  4. 触发 onAttach 回调 :如果提供了 onAttach 回调,它将被调用,并传入当前的 ScrollPosition

总之,attach 方法用于将一个滚动位置与控制器关联,并为该位置添加必要的监听器,以便控制器可以通过方法如 animateTojumpTo 来操作它。

detach

scss 复制代码
/// 从此控制器中注销给定的滚动位置。
///
/// 当此方法返回后,[animateTo] 和 [jumpTo] 方法将不再操作给定的滚动位置。
void detach(ScrollPosition position) {
  assert(_positions.contains(position));
  onDetach?.call(position);
  position.removeListener(notifyListeners);
  _positions.remove(position);
}

定义了 detach 方法,用于将一个 ScrollPosition 对象从 ScrollController 中注销。以下是其主要要点:

  1. 断言检查 :首先,通过断言检查该 ScrollPosition 是否已经附加到控制器中。
  2. 触发 onDetach 回调 :如果提供了 onDetach 回调,它将被调用,并传入当前的 ScrollPosition
  3. 移除监听器 :将该 ScrollPosition 的监听器移除,停止控制器对该位置的监听。
  4. 从列表中移除位置 :将 ScrollPosition_positions 列表中移除,意味着控制器不再管理这个位置。

总之,detach 方法用于将一个滚动位置与控制器的关联解除,并停止控制器对该位置的操作和监听。

dispose

scss 复制代码
@override
void dispose() {
  for (final ScrollPosition position in _positions) {
    position.removeListener(notifyListeners);
  }
  super.dispose();
}

dispose 方法的重写,用于在销毁 ScrollController 时清理资源。以下是其主要步骤:

  1. 移除监听器 :遍历 _positions 列表中的每个 ScrollPosition 对象,调用 removeListener(notifyListeners) 移除对控制器的监听。这确保了控制器在销毁时不会继续监听滚动位置的变化。
  2. 调用父类的 dispose 方法 :调用 super.dispose(),以确保父类(ChangeNotifier)的 dispose 方法也会被调用,执行必要的资源释放。

dispose 方法用于释放 ScrollController 所持有的资源,尤其是移除所有 ScrollPosition 对象的监听器,以避免内存泄漏或不必要的操作

createScrollPosition

php 复制代码
/// 为 [Scrollable] 小部件创建一个 [ScrollPosition]。
///
/// 子类可以重写此函数来自定义其控制的可滚动小部件使用的 [ScrollPosition]。
/// 例如,[PageController] 重写了此函数,返回一个面向页面的滚动位置子类,
/// 它可以在可滚动小部件调整大小时保持相同的页面可见。
///
/// 默认情况下,返回一个 [ScrollPositionWithSingleContext]。
///
/// 参数通常会传递给正在创建的 [ScrollPosition]:
///
///  * `physics`:一个 [ScrollPhysics] 实例,决定了 [ScrollPosition] 应如何响应用户交互,
///    以及如何在释放或快速滑动时模拟滚动等。此值不会为 null。通常来自于创建
///    [Scrollable] 的 [ScrollView] 或其他小部件,或者,如果没有提供,则来自环境中的
///    [ScrollConfiguration]。
///  * `context`:一个 [ScrollContext],用于与将拥有此 [ScrollPosition] 对象的对象进行通信
///    (通常是 [Scrollable] 本身)。
///  * `oldPosition`:如果这是为此 [Scrollable] 创建的第一个 [ScrollPosition],则为 null;
///    否则,它将是上一个实例。当环境发生变化,需要重新创建 [ScrollPosition] 时使用。
///    如果这是第一次创建 [ScrollPosition],则为 null。
ScrollPosition createScrollPosition(
  ScrollPhysics physics,
  ScrollContext context,
  ScrollPosition? oldPosition,
) {
  return ScrollPositionWithSingleContext(
    physics: physics,
    context: context,
    initialPixels: initialScrollOffset,
    keepScrollOffset: keepScrollOffset,
    oldPosition: oldPosition,
    debugLabel: debugLabel,
  );
}

定义了 createScrollPosition 方法,用于为一个 Scrollable 小部件创建一个 ScrollPosition 对象。方法的注释提供了详细的说明,描述了它的行为和参数。

  1. 子类重写 :子类可以通过重写此方法来自定义其控制的滚动位置类型。例如,PageController 会重写此方法,以便为其使用的滚动视图提供一个特定的滚动位置子类。

  2. 默认实现 :如果没有重写,默认会返回一个 ScrollPositionWithSingleContext 实例,它是一个标准的滚动位置类型。

  3. 参数说明

    • physics:滚动物理学,控制如何响应用户的滚动交互。
    • context:用于传递上下文信息,通常是 Scrollable 自身的上下文。
    • oldPosition:如果这是为同一个 Scrollable 创建的新的滚动位置,oldPosition 是先前的滚动位置。

总结

createScrollPosition 是一个工厂方法,用来为 Scrollable 小部件创建适当的 ScrollPosition,它允许子类根据需要定制滚动行为和状态。

TrackingScrollController

scss 复制代码
/// 一个 [ScrollController],它的 [initialScrollOffset] 跟踪其最新更新的 [ScrollPosition]。
///
/// 这个类可以用于同步两个或更多懒加载的滚动视图的滚动偏移,它们共享一个 [TrackingScrollController]。
/// 它会跟踪最近更新的滚动位置,并将其作为 `initialScrollOffset` 返回。
///
/// {@tool snippet}
///
/// 在这个示例中,每个 [PageView] 页面都包含一个 [ListView],而所有三个 [ListView] 都共享同一个 [TrackingScrollController]。
/// 所有三个列表视图的滚动偏移将彼此同步,以尽可能考虑不同列表长度的差异。
///
/// ```dart
/// PageView(
///   children: <Widget>[
///     ListView(
///       controller: _trackingScrollController,
///       children: List<Widget>.generate(100, (int i) => Text('page 0 item $i')).toList(),
///     ),
///     ListView(
///       controller: _trackingScrollController,
///       children: List<Widget>.generate(200, (int i) => Text('page 1 item $i')).toList(),
///     ),
///     ListView(
///       controller: _trackingScrollController,
///       children: List<Widget>.generate(300, (int i) => Text('page 2 item $i')).toList(),
///     ),
///   ],
/// )
/// ```
/// {@end-tool}
///
/// 在这个示例中,`_trackingController` 会由构建小部件树的有状态小部件创建。
class TrackingScrollController extends ScrollController {
  /// 创建一个滚动控制器,持续更新其 [initialScrollOffset] 以匹配最后接收到的滚动通知。
  TrackingScrollController({
    super.initialScrollOffset,
    super.keepScrollOffset,
    super.debugLabel,
    super.onAttach,
    super.onDetach,
  });

  final Map<ScrollPosition, VoidCallback> _positionToListener = <ScrollPosition, VoidCallback>{};
  ScrollPosition? _lastUpdated;
  double? _lastUpdatedOffset;

  /// 最后更改的 [ScrollPosition]。如果没有附加的滚动位置,或者尚未发生任何滚动,
  /// 或者最后更改的 [ScrollPosition] 已被移除,则返回 null。
  ScrollPosition? get mostRecentlyUpdatedPosition => _lastUpdated;

  /// 返回 [mostRecentlyUpdatedPosition] 的滚动偏移,或者如果它为 null,则返回构造函数中提供的初始滚动偏移。
  ///
  /// 另请参见:
  ///
  ///  * [ScrollController.initialScrollOffset],这是被此方法重写的。
  @override
  double get initialScrollOffset => _lastUpdatedOffset ?? super.initialScrollOffset;

  @override
  void attach(ScrollPosition position) {
    super.attach(position);
    assert(!_positionToListener.containsKey(position));
    _positionToListener[position] = () {
      _lastUpdated = position;
      _lastUpdatedOffset = position.pixels;
    };
    position.addListener(_positionToListener[position]!);
  }

  @override
  void detach(ScrollPosition position) {
    super.detach(position);
    assert(_positionToListener.containsKey(position));
    position.removeListener(_positionToListener[position]!);
    _positionToListener.remove(position);
    if (_lastUpdated == position) {
      _lastUpdated = null;
    }
    if (_positionToListener.isEmpty) {
      _lastUpdatedOffset = null;
    }
  }

  @override
  void dispose() {
    for (final ScrollPosition position in positions) {
      assert(_positionToListener.containsKey(position));
      position.removeListener(_positionToListener[position]!);
    }
    super.dispose();
  }
}

TrackingScrollControllerScrollController 的一个子类,旨在使多个滚动视图的滚动偏移保持同步。它可以用于共享一个控制器的多个视图,尤其适用于需要多个视图滚动时保持一致的情况。此类通过跟踪最上次更新的 ScrollPosition 来更新 initialScrollOffset

关键功能
  1. 滚动同步 :通过共享一个 TrackingScrollController,多个滚动视图的偏移量将保持同步,确保它们的滚动状态相互一致。
  2. 追踪滚动位置_lastUpdated 变量用于存储最后一次更新的 ScrollPosition_lastUpdatedOffset 则存储该位置的滚动偏移。
  3. attach / detach :在 attach 方法中,每次添加新的 ScrollPosition 时,会为其设置监听器,更新 _lastUpdated_lastUpdatedOffset。在 detach 方法中,会移除监听器,并在没有剩余滚动位置时重置 initialScrollOffset
  4. 生命周期管理 :在 dispose 方法中,移除所有已附加位置的监听器,确保资源得到清理。
示例用法

在示例中,TrackingScrollController 被用来同步多个 ListView 的滚动位置。每个 PageView 页面都包含一个 ListView,而所有这些 ListView 共享同一个控制器。这样,用户滚动其中一个列表时,其他列表的滚动偏移也会同步更新。

相关推荐
跟橙姐学代码2 分钟前
Python 高手都偷偷用的 Lambda 函数,你还在傻傻写 def 吗?
前端·python
Eddy2 分钟前
useEffect最详细的用法
前端
一枚前端小能手7 分钟前
🎨 用户等不了3秒就跑了,你这时如何是好
前端
Eddy9 分钟前
什么时候应该用useCallback
前端
愿化为明月_随波逐流10 分钟前
关于uniapp开发安卓sdk的aar,用来控制pda的rfid的扫描
前端
探码科技12 分钟前
AI知识管理全面指南:助力企业高效协作与创新
前端
Eddy12 分钟前
react中什么时候应该用usecallback中代码优化
前端
Juchecar21 分钟前
Vue3 应用、组件概念详解 - 初学者完全指南
前端·vue.js
w_y_fan22 分钟前
双token机制:flutter_secure_storage 实现加密存储
前端·flutter
yvvvy23 分钟前
HTTP 从 0.9 到 3.0,一次穿越 30 年的网络进化之旅
前端·javascript