flutter
/

GetX Workers Advanced: Complex Reactive Patterns & Use Cases

Last Sync: Today

On this page

15
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX Workers Advanced: Complex Reactive Patterns & Use Cases

Introduction

Workers in GetX are a powerful way to react to state changes. Beyond the basics (ever, once, debounce, interval), you can create complex reactive flows by combining workers, conditionally triggering side effects, and using everAll and onceAll. This guide covers advanced worker patterns to handle intricate business logic with minimal code.

  1. Recap: Basic Workers

  • ever – Executes on every change of a reactive variable.
  • once – Executes only on the first change.
  • debounce – Waits for a pause before executing (ideal for search inputs).
  • interval – Executes at a fixed rate while changes occur.
  • everAll – Executes when any of a list of reactive variables changes.
  • onceAll – Executes once when any of a list of reactive variables changes.

  1. Chaining Workers

You can chain workers to create multi‑step reactive flows. For example, after a debounced search, call another worker to log the result.

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

  @override
  void onInit() {
    super.onInit();
    // Step 1: debounce search input
    debounce(query, (_) => performSearch(), time: Duration(milliseconds: 500));

    // Step 2: when results change, log them
    ever(results, (list) {
      print('Results updated: ${list.length} items');
    });
  }

  Future<void> performSearch() async {
    if (query.value.isEmpty) {
      results.clear();
      return;
    }
    // simulate API
    await Future.delayed(Duration(milliseconds: 200));
    results.assignAll(['Result for ${query.value}']);
  }
}

  1. Conditional Triggers

Sometimes you only want to react when a variable reaches a certain value. You can wrap the worker callback with a condition, or use a custom Rx with a filter.

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

  @override
  void onInit() {
    super.onInit();
    ever(count, (value) {
      if (value == 10) {
        Get.snackbar('Milestone', 'Reached 10!');
      }
    });
  }
}

  1. Combining Multiple Rx with everAll

everAll listens to a list of reactive variables and triggers when any of them change. It provides the list of new values. This is perfect for validating forms or combining multiple state pieces.

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

  @override
  void onInit() {
    super.onInit();
    everAll([username, email, password], (_) {
      final valid = username.value.isNotEmpty &&
          email.value.contains('@') &&
          password.value.length >= 6;
      isFormValid.value = valid;
    });
  }
}

  1. onceAll – One‑Time Reaction

onceAll executes only once when any of the observed variables change. It's useful for onboarding flows or one‑time setups triggered by 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], (_) {
      Get.snackbar('Congratulations', 'You finished onboarding!');
    });
  }
}

  1. Custom Worker Logic: Debounce with Immediate First Call

Sometimes you need a debounce that also triggers immediately on the first change. You can implement a custom worker or combine once and debounce.

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

  @override
  void onInit() {
    super.onInit();
    // Immediate trigger on first change, then debounce
    once(query, (_) => search());
    debounce(query, (_) => search(), time: Duration(milliseconds: 500));
  }

  void search() {
    print('Searching for: ${query.value}');
  }
}

  1. Throttling with interval

interval executes at a fixed interval while the variable changes. This can be used to throttle high‑frequency events like scroll or mouse movements.

DARTRead-only
1
class ScrollController extends GetxController {
  var scrollY = 0.obs;

  @override
  void onInit() {
    super.onInit();
    interval(scrollY, (value) {
      print('Scroll position: $value');
    }, time: Duration(milliseconds: 200));
  }
}

  1. Worker Cleanup & Disposal

Workers are automatically disposed when the controller is disposed. However, if you need to stop a worker manually (e.g., conditionally), you can use a flag inside the worker callback. There's no direct API to remove a worker, but you can set a condition that prevents execution.

DARTRead-only
1
class MyController extends GetxController {
  var active = true.obs;
  var data = ''.obs;

  @override
  void onInit() {
    super.onInit();
    ever(data, (value) {
      if (active.value) {
        print('Data changed: $value');
      }
    });
  }
}

  1. Asynchronous Workers

Workers can perform async operations, but you need to be careful about overlapping calls. Use a flag to prevent multiple executions or cancel previous requests.

DARTRead-only
1
class AsyncController extends GetxController {
  var query = ''.obs;
  var isLoading = false.obs;
  CancelToken? _cancelToken;

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

  Future<void> search() async {
    _cancelToken?.cancel();
    _cancelToken = CancelToken();
    isLoading.value = true;
    try {
      await api.search(query.value, cancelToken: _cancelToken);
    } catch (e) {
      if (e is CancelledException) return;
      // handle error
    } finally {
      isLoading.value = false;
    }
  }
}

  1. Performance Considerations

  • Avoid heavy computations inside worker callbacks – Move heavy work to separate isolates or use compute.
  • Use debounce for frequent changes – Prevents excessive executions.
  • Unsubscribe from expensive workers when not needed – Use a conditional flag or dispose controller early.
  • Prefer ever over polling – It’s event‑driven and more efficient.

Best Practices

  • Keep workers focused – One worker per side effect.
  • Use everAll for dependent state – Reduces duplicate logic.
  • Combine debounce with once for immediate + delayed actions – Improves UX.
  • Always handle errors in async workers – Prevent uncaught exceptions.
  • Test workers – Use Get.reset and simulate state changes to verify behavior.

Common Mistakes

  • ❌ Creating workers that cause infinite loops – Modifying the same Rx variable inside its own worker. ✅ Avoid modifying the observed variable; use a flag or different variable.
  • ❌ Forgetting to cancel previous async calls – Results from old requests may overwrite new ones. ✅ Use a cancel token or ignore stale responses.
  • ❌ Using ever when debounce is more appropriate – Unnecessary CPU usage. ✅ Choose the right worker type.
  • ❌ Not handling null in workers – The value may be null if the Rx is cleared. ✅ Check for null.

FAQ

  • Q: Can I use workers with GetBuilder?
    A: Workers are independent of UI; they react to Rx variables. They work regardless of whether the UI uses Obx or GetBuilder.
  • Q: How do I stop a worker?
    A: Workers are tied to the controller's lifecycle. To stop it early, you can use a conditional flag inside the callback, or delete the controller with Get.delete.
  • Q: Can workers listen to multiple variables with different debounce times?
    A: Not directly; you would need separate workers or combine them with a custom Rx that aggregates changes.
  • Q: What's the difference between everAll and ever with a list?
    A: everAll triggers when any of the observed variables change, and you get all their values. You can also use ever on a computed getter that depends on them, but that runs on every access.
  • Q: Can I use workers inside a service (GetxService)?
    A: Yes, services have the same lifecycle and can use workers.

Conclusion

Advanced worker patterns allow you to build sophisticated reactive flows with minimal code. By combining workers, handling async operations, and leveraging everAll and onceAll, you can orchestrate complex side effects cleanly. Remember to test your workers and consider performance for heavy operations.

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: WorkerDemo(),
    );
  }
}

class DemoController extends GetxController {
  var username = ''.obs;
  var email = ''.obs;
  var password = ''.obs;
  var isFormValid = false.obs;
  var searchQuery = ''.obs;
  var searchResults = <String>[].obs;

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

    // everAll – validate form
    everAll([username, email, password], (_) {
      isFormValid.value = username.value.isNotEmpty &&
          email.value.contains('@') &&
          password.value.length >= 6;
    });

    // debounce search
    debounce(searchQuery, (_) => performSearch(), time: Duration(milliseconds: 500));
  }

  Future<void> performSearch() async {
    if (searchQuery.value.isEmpty) {
      searchResults.clear();
      return;
    }
    await Future.delayed(Duration(milliseconds: 300));
    searchResults.assignAll(['Result for "${searchQuery.value}"']);
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Advanced Workers')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              onChanged: (v) => controller.username.value = v,
              decoration: InputDecoration(labelText: 'Username'),
            ),
            TextField(
              onChanged: (v) => controller.email.value = v,
              decoration: InputDecoration(labelText: 'Email'),
            ),
            TextField(
              onChanged: (v) => controller.password.value = v,
              obscureText: true,
              decoration: InputDecoration(labelText: 'Password'),
            ),
            Obx(() => Text(
              controller.isFormValid.value ? 'Form valid' : 'Form invalid',
              style: TextStyle(color: controller.isFormValid.value ? Colors.green : Colors.red),
            )),
            Divider(),
            TextField(
              onChanged: (v) => controller.searchQuery.value = v,
              decoration: InputDecoration(labelText: 'Search (debounced)'),
            ),
            Obx(() => Column(
              children: controller.searchResults.map((r) => Text(r)).toList(),
            )),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which worker should you use to react when any of a list of reactive variables changes?

A
ever
B
once
C
everAll
D
interval
Q2
of 3

How can you implement a debounce that also triggers immediately on the first change?

A
Use only debounce
B
Use only once
C
Combine once and debounce
D
Use interval with a short duration
Q3
of 3

What is a common pitfall when using async workers without cancellation?

A
Memory leaks
B
Out‑of‑order responses overwriting newer data
C
UI freezes
D
Workers stop working

Previous

getx custom rx model

Next

getx feature modular architecture

Related Content

Need help?

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