Ao desenvolver um aplicativo Flutter, o gerenciamento de estado e a reatividade são aspectos centrais que afetam diretamente a manutenção do código e a experiência do usuário. Embora existam soluções como MobX, Riverpod e Provider para atender a essas necessidades, muitas vezes é desejável manter o desenvolvimento dentro das ferramentas básicas do Flutter, garantindo reatividade sem a necessidade de pacotes externos. Neste artigo, exploraremos como o uso de ValueNotifier e ValueListenableBuilder pode fornecer uma abordagem nativa, simples e eficaz para reatividade granular no Flutter.

O Cenário Comum: MobX e Computed Values

O MobX é uma biblioteca popular no ecossistema Flutter, amplamente utilizada para fornecer reatividade automática e controle sobre valores derivados por meio dos @observable e @computed. Com @computed, você pode derivar valores que são recalculados automaticamente sempre que suas dependências mudam. Vamos ver um exemplo de como isso funciona:

import 'package:mobx/mobx.dart';

part 'store.g.dart';

class Cart = _Cart with _$Cart;

abstract class _Cart with Store {
  @observable
  double price = 0.0;

  @observable
  int quantity = 0;

  @computed
  double get total => price * quantity;
}

void main() {
  final cart = Cart();

  autorun((_) {
    print('Total updated: \${cart.total}');
  });

  cart.price = 5.0;     // Saída: Total updated: 5.0
  cart.quantity = 3;    // Saída: Total updated: 15.0
}

Neste exemplo, @computed garante que o valor total do carrinho seja recalculado automaticamente quando price ou quantity mudam, fornecendo uma experiência reativa com mínimo esforço do desenvolvedor.

Substituindo o MobX com ValueNotifier e Listenable.merge

Apesar da simplicidade que o MobX oferece, ele traz um overhead em termos de dependências, pré-processamento (build_runner) e uma estrutura de código que não é tão direta. Com ValueNotifier, é possível obter a mesma reatividade, mantendo o código dentro das ferramentas nativas do Flutter.

Vamos ver um exemplo similar usando ValueNotifier:

import 'package:flutter/widgets.dart';

class Cart {
  ValueNotifier<double> price = ValueNotifier<double>(0.0);
  ValueNotifier<int> quantity = ValueNotifier<int>(0);
  ValueNotifier<double> total = ValueNotifier<double>(0.0);

  late final Listenable mergedListenable;

  Cart() {
    // Inicializa 'mergedListenable' combinando os ValueNotifiers.
    mergedListenable = Listenable.merge([price, quantity]);
    mergedListenable.addListener(_updateTotal);
  }

  void _updateTotal() {
    total.value = price.value * quantity.value;
  }
}

void main() {
  final cart = Cart();

  cart.total.addListener(() {
    print('Total updated: \${cart.total.value}');
  });

  // Atualizando valores
  cart.price.value = 5.0;     // Saída: Total updated: 5.0
  cart.quantity.value = 3;    // Saída: Total updated: 15.0
}

Análise da Abordagem com ValueNotifier

Nesta abordagem, o ValueNotifier é utilizado para manter a reatividade dos valores price, quantity e total. Vamos entender algumas vantagens dessa abordagem:

  1. Reatividade Granular e Controle Explícito:
    • Usando ValueNotifier, temos controle direto sobre cada propriedade que deve ser observada. A reatividade é feita de maneira granular, apenas onde realmente importa. Isso significa que é possível ter uma notificação precisa, sem atualizações desnecessárias.
  2. Uso do Listenable.merge:
    • O Listenable.merge nos permite combinar price e quantity em um único Listenable, tornando a estrutura mais organizada e evitando a necessidade de adicionar listeners individualmente a cada ValueNotifier. Assim, conseguimos manter o total sempre atualizado, sem a necessidade de uma biblioteca externa.
  3. Evita Pré-Processamento:
    • Diferente do MobX, não precisamos usar o build_runner para gerar código automaticamente. Isso simplifica o fluxo de desenvolvimento, deixando-o mais direto e sem dependências adicionais, o que é particularmente útil para quem quer manter o projeto mais leve.

Integrando com a Interface do Usuário usando ValueListenableBuilder

Um dos benefícios de ValueNotifier é que ele funciona perfeitamente com widgets nativos do Flutter, como o ValueListenableBuilder. Podemos usar este widget para atualizar a interface automaticamente quando o valor de total mudar, garantindo uma UI reativa.

Veja um exemplo:

import 'package:flutter/material.dart';

class Cart {
  ValueNotifier<double> price = ValueNotifier<double>(15.25);
  ValueNotifier<int> quantity = ValueNotifier<int>(0);
  ValueNotifier<double> total = ValueNotifier<double>(0);

  late final Listenable mergedListenable;

  Cart() {
    mergedListenable = Listenable.merge([price, quantity]);
    mergedListenable.addListener(_updateTotal);
  }

  void _updateTotal() {
    total.value = price.value * quantity.value;
  }
}

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

class MyApp extends StatelessWidget {
  final cart = Cart();

  MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    const font24 = TextStyle(fontSize: 24);
    const font20 = TextStyle(fontSize: 20);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Cart Total Example'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  IconButton.filledTonal(
                    onPressed: () => cart.quantity.value++,
                    icon: const Icon(Icons.add),
                  ),
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 20),
                    child: ValueListenableBuilder(
                      valueListenable: cart.quantity,
                      builder: (context, value, _) => Text(
                        'Qt: $value',
                        style: font20,
                      ),
                    ),
                  ),
                  IconButton.filledTonal(
                    onPressed: () => cart.quantity.value--,
                    icon: const Icon(Icons.remove),
                  ),
                ],
              ),
              const SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  IconButton.filledTonal(
                    onPressed: () => cart.price.value += 10,
                    icon: const Icon(Icons.add),
                  ),
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 20),
                    child: ValueListenableBuilder(
                      valueListenable: cart.price,
                      builder: (context, value, _) => Text(
                        'Price: ${value.toStringAsFixed(2)}',
                        style: font20,
                      ),
                    ),
                  ),
                  IconButton.filledTonal(
                    onPressed: () => cart.price.value -= 10,
                    icon: const Icon(Icons.remove),
                  ),
                ],
              ),
              const SizedBox(height: 20),
              ValueListenableBuilder(
                valueListenable: cart.total,
                builder: (context, value, _) => Text(
                  'Total: ${value.toStringAsFixed(2)}',
                  style: font24,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Conclusão

A abordagem com ValueNotifier e ValueListenableBuilder oferece uma alternativa extremamente eficiente para quem deseja evitar dependências externas e manter o código simples e direto. Embora exija algumas linhas adicionais para configurar a reatividade, essa solução proporciona controle granular e elimina o overhead de soluções como MobX e build_runner.

Manter o código dentro das ferramentas básicas do Flutter traz simplicidade, desempenho e uma curva de aprendizado menor, sem sacrificar a reatividade necessária para uma boa experiência de usuário. Essa abordagem é ideal para projetos que valorizam a manutenção, leveza e o uso de ferramentas nativas. Com ValueNotifier e ValueListenableBuilder, é possível controlar a reatividade de forma explícita e evitar notificações desnecessárias, mantendo o código eficiente e simplificado. Evitando dependências externas e pré-processamentos, também reduz-se o risco de problemas em futuras atualizações de pacotes. Essa solução é especialmente interessante para projetos menores ou médios, onde a simplicidade e o controle são fundamentais.

Em contrapartida, o MobX é vantajoso em projetos maiores e complexos, onde múltiplas dependências entre estados exigem sincronização constante. O uso de @computed facilita o gerenciamento desses estados, permitindo o recalculo automático de valores derivados. Além disso, a automação de código com build_runner poupa tempo em projetos grandes ao evitar configurações manuais de Listeners. O MobX mantém um fluxo de dados consistente e facilita a escalabilidade, tornando-o ideal para aplicações que requerem reatividade avançada em múltiplas partes da interface.

Em resumo, para quem busca uma solução que equilibre simplicidade e controle, o ValueNotifier com Listenable.merge é uma excelente escolha para uma reatividade robusta e flexível no Flutter.