Skip to main content

Pesquisa — Obx vs AppGetBuilder: reatividade no NewSellStepper

Data: 2026-05-27
Contexto: Task #19323 — RF-05 (NewSellStepper reativo)


Introdução

O projeto coezzion_vendas_app define regras explícitas de reatividade no README.md:

"Todos os widgets (telas inclusas) devem ser Stateless (exceções devem ser discutidas) e toda a gerência de estado deve ser feita pelo GetX, ou seja, sem setState" (linha 55)

"Atenção: não utilizar o GetBuilder, e sim o AppGetBuilder que é o que contém as regras oficiais do projeto" (linha 108)

Esta pesquisa compara os dois mecanismos disponíveis (Obx e AppGetBuilder) para decidir qual usar na reatividade do NewSellStepper.


Mecanismos

Obx (GetX nativo)

Fonte: get-4.7.3/lib/get_state_manager/src/rx_flutter/rx_obx_widget.dart

ObxState (StatefulWidget interno)
└── _observer = RxNotifier() ← sujeito/stream próprio
└── initState: _observer.listen(_updateTree) ← assina stream
└── _updateTree: setState(() {})
└── build: RxInterface.notifyChildren(_observer, widget.build)

Como detecta mudanças:

  1. Durante build(), RxInterface.proxy = _observer (global thread-local)
  2. O builder executa; qualquer acesso a .value de um .obsRxInterface.proxy?.addListener(subject) — vincula o stream da variável Rx ao observer
  3. Quando someObs.value = newValuesubject.add(_value) → propaga pelo stream → _observer notificado → _updateTreesetState()
  4. Se NENHUM .obs for acessado durante o build, Obx lança erro

Gatilho: someObs.value = x (automático, sem update() manual)

AppGetBuilder (custom do projeto)

Fonte: coezzion_vendas_app/lib/components/app_get_builder.dart

AppGetBuilderState (StatefulWidget interno)
└── initState: controller?.addListener(getUpdate)
└── getUpdate: if (mounted) setState(() {})
└── build: widget.builder()

Como detecta mudanças:

  1. GetxController estende ListNotifier — mantém lista _updaters de callbacks
  2. addListener(getUpdate) armazena getUpdate em _updaters
  3. Quando controller.update() é chamado → ListNotifier.refresh()_notifyUpdate() → itera _updaters → chama cada getUpdatesetState()
  4. dispose: remove listener via _remove?.call(); opcionalmente chama controller.onScreenClosed() (se implementar DefaultApiControllerInterface)

Gatilho: controller.update() (explícito, requer chamada manual)


Comparação lado a lado

AspectoObxAppGetBuilder
FonteGetX (pacote externo)Custom do projeto (lib/components/)
Widget baseStatefulWidget (interno)StatefulWidget (interno)
ReconstruçãosetState(() {})setState(() {})
Gatilho.obs value change (automático)controller.update() (explícito)
Controller necessário?Não — funciona com qualquer .obsSim — GetxController é obrigatório
GranularidadeSub-árvore dentro do builderToda a árvore do builder
Lifecycle hooksNenhumonScreenClosed() (se DefaultApiControllerInterface)
Uso no projeto149 ocorrências, sempre aninhado dentro de AppGetBuilderShell externo de telas/seções
README manda usar?Não mencionado explicitamenteSim — "não utilizar o GetBuilder, e sim o AppGetBuilder"

Cadeia de eventos: Obx

Controller: isActive.value = true

RxImpl.set value(val):
if (_value == val && !firstRebuild) return; ← evita rebuilds desnecessários
_value = val;
subject.add(_value); ← emite no stream

NotifyManager.addListener (registrado via proxy):
subs = rxGetx.listen((data) {
subject.add(data); ← propaga para o observer
});

ObxState._observer.listen(_updateTree):
_updateTree(_) => setState(() {}); ← reconstrói sub-árvore

build(): RxInterface.notifyChildren(_observer, widget.build)
→ proxy = _observer → executa builder → acessa isActive.value → registra listener novamente
→ se nenhum .obs acessado → ERRO

Cadeia de eventos: AppGetBuilder

Controller: update()

GetxController.update([ids], condition):
if (!condition) return;
refresh(); ← ListNotifier.refresh()

ListNotifierMixin.refresh():
_notifyUpdate();

ListNotifierMixin._notifyUpdate():
for (element in _updaters) { element!(); } ← itera callbacks registrados

AppGetBuilderState.getUpdate:
if (mounted) setState(() {}); ← reconstrói árvore inteira

build(): widget.builder() ← re-executa o builder completo

Cenário: AppGetBuilder aninhado (tela com cartController + stepper com upsellController)

Este é o cenário real: a tela CheckSellScreen (ou NewSellScreen) usa AppGetBuilder(init: cartController, ...) como shell externo. Dentro dela, o NewSellStepper usaria AppGetBuilder(init: upsellController, ...).

AppGetBuilder(init: cartController, builder: () { ← outer
Column(children: [
NewSellStepper(currentStep: SteppSell.xxx), ← contém AppGetBuilder interno
// ... outros widgets
])
})

O que acontece quando cartController.update() dispara:

  1. Outer AppGetBuilder é notificado → setState()widget.builder() re-executa
  2. NewSellStepper é recriado (novo StatelessWidget → novo build())
  3. Inner AppGetBuilder anterior é disposed:
    • _remove?.call() → remove listener do upsellController._updaters
    • controller = null
  4. Inner AppGetBuilder novo executa initState:
    • controller = widget.init (mesma instância lazySingleton)
    • _subscribeToController()controller.addListener(getUpdate) → adiciona callback em _updaters

Custo: A cada rebuild do outer, o inner AppGetBuilder é destruído e recriado. O listener é removido e readicionado. Se o cart muda frequentemente (ex: adicionar produtos), isso acontece múltiplas vezes por venda.

O que acontece quando upsellController.update() dispara:

  1. Inner AppGetBuilder é notificado → setState()widget.builder() re-executa
  2. Apenas o stepper é reconstruído — outer não é afetado
  3. Sem destruição/recriação do inner AppGetBuilder (não está sendo substituído por um novo widget)

Custo: Baixo. Rebuild apenas do stepper.

O mesmo cenário com Obx standalone:

AppGetBuilder(init: cartController, builder: () { ← outer
Column(children: [
NewSellStepper(currentStep: SteppSell.xxx), ← StatelessWidget com Obx interno
])
})

// Dentro de NewSellStepper.build():
Obx(() {
final isActive = controller.isActive.value; ← registro automático via proxy
return Row(children: [...]);
})
  1. Quando cart muda → outer reconstrói → stepper recriado → Obx recriado
  2. Obx antigo: dispose()_observer.close() → streams limpos
  3. Obx novo: initState → novo _observer_observer.listen(_updateTree)build()notifyChildren → registra proxy via isActive.value

Custo: Equivalente ao AppGetBuilder aninhado. Ambos têm overhead de destroy/recreate no outer rebuild.


Decisões tomadas

#QuestãoDecisão
Q1Obx standalone é suficiente para o stepper?Tecnicamente sim. O stepper só precisa reagir a isActive.value. Obx faz isso sem boilerplate de update().
Q2AppGetBuilder é obrigatório pelo README?O README manda usar AppGetBuilder "em vez de GetBuilder" — não proíbe Obx. Mas o padrão de uso (149 Obx sempre aninhados em AppGetBuilder) sugere que AppGetBuilder é o shell esperado.
Q3O controller precisa de .obs ou só update()?Com AppGetBuilder, o controller poderia usar apenas update() e um bool simples. Se quiser compatibilidade com Obx para outros consumidores, manter RxBool + chamar update() manualmente.
Q4Aninhamento de AppGetBuilder é problemático?Não quebra nada, mas cada rebuild do outer causa destroy/recreate do inner. É o mesmo overhead que Obx sofreria no mesmo cenário. O projeto já faz isso em check_sell_crm_values.dart.
Q5Recomendação finalAppGetBuilder. Alinha-se ao README, ao ecossistema de controllers do projeto (toda reatividade passa por GetxController), e o custo de aninhamento é aceitável (já praticado). O controller deve expor bool isActive (não RxBool) e chamar update() quando mudar — mais simples e explícito.