flutter
/

GetX Workers: ever, once, debounce, interval & More

Last Sync: Today

On this page

13
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX Workers: ever, once, debounce, interval & More

What are GetX Workers?

Workers in GetX are powerful tools that allow you to react to changes in reactive variables (Rx types). They execute side effects like logging, API calls, navigation, or any logic whenever the observed Rx variable changes. Workers are typically registered inside the onInit() method of a controller and are automatically disposed when the controller is destroyed.

  1. ever – React to Every Change

The ever worker triggers the callback every time the observed Rx variable changes. It's perfect for logging, saving data, or any action that should happen on every update.

DARTRead-only
1
class MyController extends GetxController {
  var count = 0.obs;

  @override
  void onInit() {
    super.onInit();
    ever(count, (_) => print('Count changed to $count'));
  }

  void increment() => count++;
}

  1. once – React Only Once

The once worker executes the callback only the first time the Rx variable changes. Subsequent changes are ignored. This is useful for one-time events like showing a tutorial or navigating after a condition is met.

DARTRead-only
1
class MyController extends GetxController {
  var isLoggedIn = false.obs;

  @override
  void onInit() {
    super.onInit();
    once(isLoggedIn, (_) => Get.snackbar('Welcome', 'You are now logged in!'));
  }

  void login() => isLoggedIn.value = true;
}

  1. debounce – Wait for a Pause

The debounce worker waits for a specified period of inactivity before executing the callback. It's ideal for search inputs, form validations, or any scenario where you want to react after the user stops typing.

DARTRead-only
1
class SearchController extends GetxController {
  var query = ''.obs;

  @override
  void onInit() {
    super.onInit();
    debounce(query, (_) => performSearch(), time: Duration(milliseconds: 500));
  }

  void performSearch() {
    print('Searching for: ${query.value}');
    // Call API, update results, etc.
  }
}

  1. interval – Trigger Periodically

The interval worker executes the callback at a fixed time interval while the Rx variable is changing. It's useful for throttling events, like tracking progress or limiting API calls during rapid changes.

DARTRead-only
1
class ProgressController extends GetxController {
  var progress = 0.obs;

  @override
  void onInit() {
    super.onInit();
    interval(progress, (_) => print('Progress: $progress%'), time: Duration(milliseconds: 200));
  }

  void updateProgress() {
    if (progress < 100) progress++;
  }
}

  1. everAll – React to Multiple Variables

The everAll worker listens to a list of Rx variables and executes the callback whenever any of them changes. This is useful when you need to react to combined state.

DARTRead-only
1
class FormController extends GetxController {
  var name = ''.obs;
  var email = ''.obs;
  var isFormValid = false.obs;

  @override
  void onInit() {
    super.onInit();
    everAll([name, email], (_) => validateForm());
  }

  void validateForm() {
    isFormValid.value = name.value.isNotEmpty && email.value.isNotEmpty;
  }
}

  1. onceAll – Trigger Once When Any Changes

The onceAll worker triggers the callback the first time any of the observed Rx variables change. After that, it stops listening. This is great for onboarding or initial setup that should happen after user interaction.

DARTRead-only
1
class OnboardingController extends GetxController {
  var step1Completed = false.obs;
  var step2Completed = false.obs;

  @override
  void onInit() {
    super.onInit();
    onceAll([step1Completed, step2Completed], (_) => showCongrats());
  }

  void showCongrats() => Get.snackbar('Congratulations', 'You completed your first action!');
}

Workers in Practice: Search with Debounce

DARTRead-only
1
class SearchController extends GetxController {
  var query = ''.obs;
  var results = <String>[].obs;
  var isLoading = false.obs;

  @override
  void onInit() {
    super.onInit();
    debounce(query, (_) => search(), time: Duration(milliseconds: 500));
  }

  Future<void> search() async {
    if (query.value.isEmpty) return;
    isLoading.value = true;
    // Simulate API call
    await Future.delayed(Duration(seconds: 1));
    results.assignAll(['Result for ${query.value}']);
    isLoading.value = false;
  }
}

Workers in Practice: Logging with ever

DARTRead-only
1
class AnalyticsController extends GetxController {
  var screenName = ''.obs;

  @override
  void onInit() {
    super.onInit();
    ever(screenName, (name) => logScreenView(name));
  }

  void logScreenView(String name) {
    // Send to analytics service
    print('Screen viewed: $name');
  }
}

Best Practices

  • Register workers in onInit – This ensures they are set up when the controller is created.
  • Keep callbacks lightweight – Avoid heavy computations in workers; delegate to separate methods if needed.
  • Use debounce for user input – Prevents excessive API calls during typing.
  • Dispose automatically – Workers are disposed when the controller is destroyed, no manual cleanup needed.
  • Avoid infinite loops – Don't modify the same Rx variable inside its own worker without a guard condition.
  • Combine with async/await – You can use async callbacks, but be mindful of cancelled requests.

Common Mistakes

  • ❌ Registering workers outside onInit – May cause memory leaks if not properly managed. ✅ Always register in onInit.
  • ❌ Using ever when debounce is appropriate – Triggers on every keystroke, causing performance issues. ✅ Use debounce for search inputs.
  • ❌ Not handling async operations properly – Workers don't automatically cancel previous async tasks. ✅ Use a cancel token or ignore results from stale requests.
  • ❌ Creating circular dependencies – Modifying the same Rx variable inside its worker can cause infinite loops. ✅ Use a flag to prevent recursion if needed.
  • ❌ Forgetting to check for disposed controller – After async operations, check Get.isRegistered or use if (mounted) pattern. ✅ Use if (Get.isRegistered<MyController>()) before accessing.

FAQ

  • Q: Do workers automatically stop when the controller is disposed?
    A: Yes, workers are tied to the controller's lifecycle and are automatically cleaned up when the controller is disposed.
  • Q: Can I use workers without a controller?
    A: You can, but it's not recommended. Workers are meant to be used inside controllers to leverage automatic disposal and lifecycle.
  • Q: How do I handle async operations in workers?
    A: You can use async callbacks, but be aware that if the Rx variable changes again before the async operation completes, you may get multiple overlapping calls. Consider using debounce or a cancel token.
  • Q: What's the difference between debounce and interval?
    A: debounce waits for a pause before executing; interval executes repeatedly at a fixed rate while changes are happening.
  • Q: Can I remove a worker manually?
    A: Workers are not directly removable, but since they're disposed with the controller, you can call Get.delete<MyController>() to remove the controller and its workers.
  • Q: Do workers work with GetBuilder?
    A: Workers listen to Rx variables, so they work regardless of whether you use Obx or GetBuilder. They are independent of the UI.
  • Q: Can I use everAll with a mix of Rx types?
    A: Yes, everAll accepts a list of any Rx variables, regardless of their type.

Conclusion

Workers are a powerful feature of GetX that allow you to react to state changes in a declarative and efficient way. By understanding ever, once, debounce, interval, and their all variants, you can build responsive applications with clean separation of concerns. Remember to register workers in onInit and keep side effects light for optimal performance.

Try it yourself

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: WorkersDemo(),
    );
  }
}

class DemoController extends GetxController {
  var count = 0.obs;
  var query = ''.obs;
  var status = 'Waiting'.obs;

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

    // ever: triggers on every change
    ever(count, (_) => print('Count changed to $count'));

    // once: triggers only once
    once(count, (_) => Get.snackbar('Once', 'Count changed for first time!'));

    // debounce: waits for 500ms of inactivity
    debounce(query, (_) => status.value = 'Searching for: ${query.value}', time: Duration(milliseconds: 500));

    // interval: triggers every second while changing
    interval(count, (_) => print('Interval: count is $count'), time: Duration(seconds: 1));
  }

  void increment() => count++;
  void updateQuery(String value) => query.value = value;
}

class WorkersDemo extends StatelessWidget {
  final controller = Get.put(DemoController());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Workers Demo')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12.0),
                child: Column(
                  children: [
                    Text('ever & once', style: TextStyle(fontWeight: FontWeight.bold)),
                    Obx(() => Text('Count: ${controller.count}', style: TextStyle(fontSize: 24))),
                    ElevatedButton(onPressed: controller.increment, child: Text('Increment')),
                  ],
                ),
              ),
            ),
            SizedBox(height: 16),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12.0),
                child: Column(
                  children: [
                    Text('debounce', style: TextStyle(fontWeight: FontWeight.bold)),
                    TextField(
                      onChanged: controller.updateQuery,
                      decoration: InputDecoration(labelText: 'Type something...'),
                    ),
                    Obx(() => Text('Status: ${controller.status}')),
                  ],
                ),
              ),
            ),
            SizedBox(height: 16),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12.0),
                child: Column(
                  children: [
                    Text('Check console for interval output', style: TextStyle(fontStyle: FontStyle.italic)),
                    Text('Every second while count changes, interval worker logs'),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 4

Which worker should you use to react to changes only once, the first time a variable changes?

A
ever
B
once
C
debounce
D
interval
Q2
of 4

Which worker is ideal for search inputs to avoid excessive API calls?

A
ever
B
once
C
debounce
D
interval
Q3
of 4

What method is used to register workers inside a GetX controller?

A
initState
B
onInit
C
onReady
D
onStart
Q4
of 4

Which worker executes the callback at fixed intervals while a variable is changing?

A
ever
B
once
C
debounce
D
interval

Previous

getx rx types

Next

getx controller

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.