Getx:防抖Debounce与节流interval

防抖Debounce

什么是防抖(Debounce)

防抖是一种 控制函数调用频率的技术。典型场景是:

  • 用户输入搜索关键词,你不想每敲一个字就发送请求,而是在用户停止输入一定时间后再发送。
  • 滑动条频繁变化时,不想每次变化都触发计算或请求。

原理

当事件被触发时,延迟执行函数。

如果在延迟时间内再次触发事件,前一次延迟取消,重新计时。

只有在最后一次触发后经过设定时间没有再次触发,函数才真正执行。

GetX 的 debounce 方法

在 GetX 中,debounce 用来对 Rx 变量(响应式变量)的变化进行防抖处理。

void debounce<T>(
  Rx<T> rx,
  VoidCallback callback, {
  Duration time = const Duration(milliseconds: 500),
  bool? leading,
});
参数 说明
rx 要监听的响应式变量(如 RxString, RxInt, RxList 等)
callback 当防抖条件满足时执行的回调
time 防抖时间间隔,默认为 500ms
leading 是否在首次触发立即执行回调(默认为 false)

场景一:搜索输入防抖

import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() {
  runApp(const MyApp());
}

/// 主程序
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(title: 'GetX Debounce 搜索 Demo', home: SearchPage());
  }
}

/// 控制器
class SearchController extends GetxController {
  // 响应式字符串变量,用来存储搜索关键词
  var query = ''.obs;

  @override
  void onInit() {
    super.onInit();

    // 防抖监听 query 变量
    // 当 query 停止变化超过 800ms,才会执行 fetchSearchResults
    debounce<String>(
      query,
      (_) => fetchSearchResults(query.value),
      time: const Duration(milliseconds: 800),
    );
  }

  // 模拟网络搜索
  void fetchSearchResults(String keyword) {
    print('搜索关键词: $keyword');
  }
}

/// 页面
class SearchPage extends StatelessWidget {
  final SearchController controller = Get.put(SearchController());

  SearchPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('搜索防抖示例')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // 输入框
            TextField(
              decoration: const InputDecoration(
                labelText: '输入搜索关键词',
                border: OutlineInputBorder(),
              ),
              onChanged: (value) {
                // 每次输入都会触发 query 的变化
                controller.query.value = value;
              },
            ),
            const SizedBox(height: 20),
            const Text('打开控制台,输入时不会立即触发搜索,停止输入 800ms 后打印搜索关键词'),
          ],
        ),
      ),
    );
  }
}
// 以下是日志
I/flutter (15544): 搜索关键词: a
I/flutter (15544): 搜索关键词: aa
I/flutter (15544): 搜索关键词: aac
I/flutter (15544): 搜索关键词: aacdd
I/flutter (15544): 搜索关键词: aacddfeserwcewr

为什么debounce(或 interval)放在onInit里面

在 中使用 debounce(或 interval)时,我们通常会把它们写在 onInit() 里,这是有明确原因的:

  1. debounce 是「监听 Rx 变量变化」的注册函数

    • debounce(rx, callback, …) 本质是:订阅(监听)这个 Rx 变量的变化

    • 它不会立刻执行 callback,而是等到这个 Rx 值发生变化时才会判断是否要触发回调。

    • 所以它必须先注册监听,等后续值变化时才能起作用。

  2. onInit() 是注册监听逻辑的最佳时机

    • onInit()GetxController 的生命周期方法,在控制器第一次被创建时调用,只执行一次。在这里放 debounce,可以确保:

      • 只注册一次监听,不会重复添加。
      • 生命周期清晰,便于维护和释放资源(对应的 onClose() 会自动清理订阅)。
    • 所以把 debounce 放到 onInit() 里,是标准做法

  3. 如果你放在 build 或 onChanged 里会有什么问题?

    • build() 会多次执行(每次界面刷新都会重建 UI),如果在里面调用 debounce:会不断重复注册监听,导致回调被触发多次(打印多次、发多次请求等)

    • onChanged() 是事件回调,每次输入/滑动都会调用一次,如果在这里注册 debounce:也会重复绑定监听,导致防抖逻辑完全失效

场景二:滑动条值变化防抖

import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'GetX Debounce Slider Demo',
      home: SliderPage(),
    );
  }
}

/// 控制器
class SliderController extends GetxController {
  // 响应式 double 变量,表示滑动条值
  var sliderValue = 0.0.obs;

  @override
  void onInit() {
    super.onInit();

    // 防抖监听 sliderValue
    // 用户滑动时,只有停止滑动超过 300ms 才打印最终值
    debounce<double>(
      sliderValue,
      (val) => print('最终滑动值: $val'),
      time: const Duration(milliseconds: 300),
    );
  }
}

/// 页面
class SliderPage extends StatelessWidget {
  final SliderController controller = Get.put(SliderController());

  SliderPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('滑动条防抖示例')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Obx(
              // 实时显示滑动条的值
              () => Text(
                '当前值: ${controller.sliderValue.value.toStringAsFixed(2)}',
              ),
            ),
            const SizedBox(height: 20),
            Obx(
              () => Slider(
                min: 0,
                max: 100,
                value: controller.sliderValue.value,
                onChanged: (value) {
                  // 每次滑动都会触发 sliderValue 的变化
                  controller.sliderValue.value = value;
                },
              ),
            ),
            const SizedBox(height: 20),
            const Text('打开控制台,滑动条变化不会立即打印,停止滑动 300ms 后打印最终值'),
          ],
        ),
      ),
    );
  }
}
// 以下是日志:
I/flutter (15971): 最终滑动值: 14.13532125538793
I/flutter (15971): 最终滑动值: 36.28982017780172
I/flutter (15971): 最终滑动值: 57.580566406250014
I/flutter (15971): 最终滑动值: 100.0
I/flutter (15971): 最终滑动值: 0.0
I/flutter (15971): 最终滑动值: 36.032209725215516
I/flutter (15971): 最终滑动值: 41.72237001616378

节流interval

什么是节流(interval)

节流是指:让高频率触发的操作,在固定时间间隔内最多只执行一次

interval 就是用于 限制高频变化的调用频率,也叫节流(throttle)。

  • 当被监听的 Rx 变量短时间内频繁变化时,interval固定时间间隔触发一次回调
  • 不同于 debounce 等待“停止变化”,interval按固定间隔触发

典型场景:

  • 滑动条连续拖动时,每隔 300ms 更新一次显示或发送网络请求。
  • 高频按钮点击或动画值变化时,只想限制触发频率。

interval方法

void interval<T>(
  Rx<T> rx,
  VoidCallback callback, {
  Duration time = const Duration(milliseconds: 300),
});
参数 说明
rx 要监听的响应式变量(如 RxInt, RxDouble, RxString 等)
callback 在固定时间间隔触发的回调函数
time 时间间隔(Duration),默认 300ms

场景:滑动条节流

import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(home: ThrottleSliderPage());
  }
}

/// 控制器
class SliderController extends GetxController {
  var sliderValue = 0.0.obs;

  @override
  void onInit() {
    super.onInit();

    // interval: 每隔 300ms 触发一次回调
    interval<double>(
      sliderValue,
      (val) => print('节流打印滑动值: ${sliderValue.value.toStringAsFixed(2)}'),
      time: const Duration(milliseconds: 300),
    );
  }
}

/// 页面
class ThrottleSliderPage extends StatelessWidget {
  final SliderController controller = Get.put(SliderController());

  ThrottleSliderPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('滑动条节流示例')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Obx(
              () => Text(
                '当前值: ${controller.sliderValue.value.toStringAsFixed(2)}',
              ),
            ),
            Obx(
              () => Slider(
                min: 0,
                max: 100,
                value: controller.sliderValue.value,
                onChanged: (value) {
                  controller.sliderValue.value = value;
                },
              ),
            ),
            const SizedBox(height: 20),
            const Text('拖动滑块时,每隔 300ms 才会打印一次值,即使滑块频繁变化'),
          ],
        ),
      ),
    );
  }
}
// 以下是日志
I/flutter (16136): 节流打印滑动值: 31.55
I/flutter (16136): 节流打印滑动值: 31.89
I/flutter (16136): 节流打印滑动值: 69.91
I/flutter (16136): 节流打印滑动值: 95.34
I/flutter (16136): 节流打印滑动值: 96.55
I/flutter (16136): 节流打印滑动值: 94.39
I/flutter (16136): 节流打印滑动值: 90.86
I/flutter (16136): 节流打印滑动值: 38.19
I/flutter (16136): 节流打印滑动值: 34.39
I/flutter (16136): 节流打印滑动值: 41.03
I/flutter (16136): 节流打印滑动值: 41.38
I/flutter (16136): 节流打印滑动值: 96.55
I/flutter (16136): 节流打印滑动值: 100.00
I/flutter (16136): 节流打印滑动值: 0.00
I/flutter (16136): 节流打印滑动值: 38.19
I/flutter (16136): 节流打印滑动值: 48.36
I/flutter (16136): 节流打印滑动值: 53.79

对比

特性 debounce interval
触发时机 停止变化后延迟触发 按固定时间间隔触发
场景 输入搜索、停止拖动后触发操作 高频滑动、动画、按钮点击限制
调用频率 最后一次变化触发一次 在间隔时间内重复触发一次
参数 timeleading time

×

喜欢就点赞,疼爱就打赏