Overlays Ancorados no Flutter: O Jeito Certo (Sim, Existe Um)

Eu estava tentando construir um tooltip (balão de dica) personalizado. Nada extravagante — apenas um pequeno balão que aparece acima de um componente, como qualquer interface normal teria. Parecia um problema já resolvido. O Flutter é maduro, Overlays existem, então isso deveria ser fácil. Peguei um OverlayEntry, posicionei-o acima do widget e segui em frente com confiança.

Para acertar a posição, fiz o que todo desenvolvedor Flutter acaba fazendo eventualmente: peguei o RenderBox do widget, usei o localToGlobal(), adicionei um pequeno deslocamento (offset) e ajustei alguns pixels. Rodei o app — e funcionou. O tooltip apareceu exatamente onde eu queria. Por um breve momento, me senti imparável.

final box = context.findRenderObject() as RenderBox;
    final offset = box.localToGlobal(Offset.zero);

    return OverlayEntry(
      builder: (_) => Positioned(
        left: offset.dx,
        top: offset.dy + box.size.height,
        child: _overlayBubble(
          color: Colors.redAccent,
          text: 'overlay 😵\nScroll me',
        ),
      ),
    );

À primeira vista, o código parecia certo. OverlayEntry, RenderBox, deslocamentos (offsets)… tudo parecia bem. Mas a realidade bateu à porta quando o componente foi parar em uma tela de verdade — não em uma demonstração, mas em uma ListView com rolagem, outros widgets e interação real do usuário. No momento em que eu rolei a tela, tudo quebrou. O widget se moveu, mas o tooltip não. Ele simplesmente ficou lá, congelado, enquanto o componente saía calmamente de cena… como aqueles companheiros de equipe fakes em um Battle Royale quando a situação aperta.

Tentei os consertos errados de costume — recalcular posições, adicionar listeners, ajustar deslocamentos. Às vezes funcionava, às vezes quebrava de novas maneiras. Foi quando caiu a ficha: se você está constantemente recalculando posições de overlay, você já está lutando contra o framework.

A solução real esteve no Flutter o tempo todo: CompositedTransformTarget e CompositedTransformFollower. Um widget se declara “seguível”, o outro o segue através de um LayerLink. Mudei para essa abordagem, rolei a tela novamente e tudo simplesmente funcionou. Sem trepidações, sem atrasos, sem matemática. Eu só me perguntei por que não tinha aprendido isso antes.

Conheça a Dupla de Widgets que Nunca se Separa.

Apresento a vocês a dupla dinâmica dos overlays no Flutter: CompositedTransformTarget e CompositedTransformFollower. Esses dois são os salvadores que nunca se desprendem, não importa a situação. O Target marca um widget como algo que pode ser rastreado, e o Follower se anexa a esse alvo, deixando o Flutter cuidar de todo o movimento. Eles são conectados usando um LayerLink — pense nisso como a cola que os mantém perfeitamente sincronizados.

Aqui está a primeira parte: envolver (fazer o wrap) o widget ao qual você deseja se ancorar.

final LayerLink _link = LayerLink();

 CompositedTransformTarget(
    link: _link,
    child: ElevatedButton(
       onPressed: _showCorrect,
       child: const Text('Show overlay'),
    ),
  )

A mágica acontece quando você cria o overlay:

OverlayEntry(
  builder: (_) => Positioned.fill(
    child: CompositedTransformFollower(
      link: _link,
      offset: const Offset(0, -40),
      showWhenUnlinked: false,
      child: TooltipBubble(),
    ),
  ),
);

Em vez de calcular as posições manualmente, o Follower se anexa ao Target. O deslocamento (offset) é relativo ao widget, não à tela, portanto, rolagens, mudanças de layout ou qualquer caos na interface (UI) não importam mais.

O Flutter agora mantém ambos os widgets perfeitamente sincronizados na camada de composição (compositing layer). Quando o alvo se move, o seguidor se move. Quando o layout muda, o overlay se atualiza automaticamente. Sem listeners, sem recalcular nada e sem precisar adivinhar pixels.

Quando usar isso

Se você estiver construindo qualquer coisa que se comporte como um tooltip, dropdown, menu de contexto ou popup flutuante, esta deve ser sua escolha padrão. No momento em que você vir um localToGlobal() em um código de overlay, pergunte-se se um CompositedTransformFollower não funcionaria melhor.

Depois que você usa esse padrão algumas vezes, é difícil voltar atrás. Seus overlays permanecem ancorados, seu código fica mais simples e sua interface finalmente para de agir como se tivesse vontade própria.

Portanto, na próxima vez que seu overlay sair flutuando por aí, lembre-se — não é um bug, é um término de namoro. Pare de fazer contas, deixe o Flutter cuidar disso e use o CompositedTransformTarget e o CompositedTransformFollower.

Se isso te ajudou, considere compartilhar com um colega desenvolvedor. Para mais descobertas como esta, clique em seguir — e até a próxima, happy Fluttering!

Please follow and like us:
error0
fb-share-icon
Tweet 20
fb-share-icon20