skip to content

In GetX, how does GetBuilder with update() differ from Obx with .obs values, and when would you choose each?

level: middleimportance: should knowfreq 40%

answer

  1. manual versus automatic notification
  2. plain fields plus update()
  3. ids target groups of builders
  4. init, global and autoRemove
  5. streams per Rx versus one listener list

basics

~20 s

GetBuilder rebuilds when the controller calls update(), optionally only builders with a given id, and works with plain fields. Obx rebuilds automatically when an Rx value read in its builder changes. GetBuilder is explicit and cheaper; Obx is less code.

solid answer

~40 s

GetX has two state styles. With **GetBuilder**, controller fields are plain Dart values and nothing happens until the controller calls `update()`; that rebuilds every `GetBuilder<T>` registered **without** an `id`, while `update(['row-3'])` rebuilds only the builders whose `id` is `'row-3'`. `GetBuilder` can also create the controller through `init:` and, with `autoRemove: true` (the default), delete it when the builder that created it is disposed. With **Obx**, fields are `.obs` observables and every assignment notifies, so the UI follows automatically. Choose GetBuilder when you want explicit, batched rebuilds and no stream per field; choose Obx when many independent small values change and the convenience outweighs implicit dependencies.

code

dart · 42 lines
dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class HabitListController extends GetxController {
  final List<String> habits = ['run', 'read'];

  void add(String habit) {
    habits.add(habit);
    update(); // builders WITHOUT an id rebuild
  }

  void rename(int index, String name) {
    habits[index] = name;
    update(['row-$index']); // only builders with this id rebuild
  }
}

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

  @override
  Widget build(BuildContext context) {
    return GetBuilder<HabitListController>(
      init: HabitListController(), // registered here; removed when this builder is disposed
      builder: (c) => Text('${c.habits.length} habits'),
    );
  }
}

class HabitRow extends StatelessWidget {
  const HabitRow({super.key, required this.index});

  final int index;

  @override
  Widget build(BuildContext context) {
    return GetBuilder<HabitListController>(
      id: 'row-$index',
      builder: (c) => Text(c.habits[index]),
    );
  }
}

go deeper

for a junior

Recall that GetBuilder needs update() while Obx rebuilds on its own when an observable changes.

for a middle

Explain id groups, the init/autoRemove creator rule and why a builder with an id ignores a plain update().

for a senior

Choose per screen: batched explicit updates for coarse state, Obx for many independent values, and review for forgotten update() calls or oversized builders.

for a principal

Set one house style per codebase so reviewers know where rebuilds are decided, and document when the other style is allowed.

## Two ways to trigger a rebuild GetX ships a **simple** state manager and a **reactive** one, and both hang off a `GetxController`. | | `GetBuilder<T>` | `Obx` | |---|---|---| | Field type | plain Dart values | `.obs` observables (`RxInt`, `RxList`, ...) | | What triggers a rebuild | the controller calls `update()` | an Rx read in the builder emits | | Granularity | all non-id builders, or an id group | exactly the Obx that read the value | | Controller creation | optional `init:` registers one | none; find or put it yourself | | Cleanup | `autoRemove: true` deletes a controller it created | none of its own | | Mechanism | a listener list on the controller | a stream per observable | ## GetBuilder in detail `GetBuilder<HabitListController>(builder: (c) => ...)` looks the controller up in GetX's registry when it mounts. If none is registered and `init:` was given, it registers that instance with `Get.put` and marks itself as the **creator**. When the creator is disposed and `autoRemove` is `true` (the default), it deletes the controller, which runs `onClose`. The controller decides when to redraw: - `update()` notifies every `GetBuilder` subscribed **without** an `id`. - `update(['row-3'])` notifies only builders created with `id: 'row-3'`; builders without an id are not touched. - `update(null, false)` does nothing; the second parameter is a condition, handy for `update(null, changed)`. - `filter: (c) => c.habits.length` on a `GetBuilder` makes it rebuild only when that derived value changed. The subtle part is the first two bullets: a builder with an `id` listens **only** to its id group, so a plain `update()` will not refresh it. ## Obx in detail `Obx(() => Text('${c.count.value}'))` subscribes to every observable whose `.value` it reads while its builder runs. Each `.obs` field owns a stream; assigning a new value emits, and the Obx rebuilds. There is no `update()` call to forget, but also no single place where the rebuild is decided. ## Choosing between them Choose **GetBuilder** when: 1. you want rebuilds to happen at explicit points, for example once after a batch of changes; 2. the state is a few coarse objects rather than many independent values; 3. you prefer plain fields that are easy to inspect and test. Choose **Obx** when: 1. many small, independent values each drive their own widget; 2. you want the UI to follow assignments without remembering to notify; 3. the team accepts that dependencies are implicit in what each builder reads. The two mix freely: one controller may hold plain fields for a `GetBuilder` list and a few `.obs` flags for small `Obx` widgets. ## Common mistakes - Forgetting `update()` after changing a plain field: the GetBuilder shows stale data. - Calling `update()` and expecting an id'd builder to refresh: it only listens to its id. - Two `GetBuilder`s with `init:` for the same type: only the first registers; the second finds the existing controller and is not its creator, so it will not remove it. - Wrapping a whole screen in one GetBuilder and calling `update()` on every keystroke, which rebuilds the whole screen each time. ## Mixing both in one controller Nothing forces a choice per app. A common pattern keeps a large list as a plain field refreshed with `update()` and a few flags as observables: ```dart class HabitScreenController extends GetxController { final List<String> habits = []; final isSaving = false.obs; Future<void> save(String habit) async { isSaving.value = true; // Obx spinner shows await Future<void>.delayed(const Duration(milliseconds: 300)); // stands in for a repository call habits.add(habit); update(); // GetBuilder list refreshes once isSaving.value = false; } } ``` The list rebuilds exactly once per save, while the spinner reacts to each flag change without an explicit call. The risk is inconsistency: reviewers must know which fields are observable and which need `update()`, so name or group them clearly. ## Cost `GetBuilder` keeps one list of callbacks per controller. Each `.obs` field keeps its own stream and each Obx its own subscription. For typical screens the difference is small; it matters when a list creates thousands of observables.

  • A GetBuilder has id: 'header' and the controller calls update() with no arguments; does the header rebuild?
    No. In GetX 4.6.1 a GetBuilder with an id subscribes only to that id group, and `update()` without ids notifies only builders that have no id. Call `update(['header'])`, or drop the id if the header should follow every update.
  • What does GetBuilder's filter parameter add?
    It takes a function of the controller, such as `(c) => c.habits.length`, stores its result, and on each notification rebuilds only if the new result differs. It narrows rebuilds when a coarse `update()` is called for changes this builder does not display.

saying these in an interview costs you the question

  • update() rebuilds every GetBuilder of that controller, ids or not
  • GetBuilder rebuilds automatically when a plain field changes
  • Obx requires calling update() after changing an observable
  • GetBuilder's init creates a new controller for every builder
  • GetBuilder and Obx cannot be used with the same controller