#flutter #debug #mobile #scrollview

Mayinab travava do nada ao navegar entre subtopics (7 tentativas até achar)

Miuna Hamasaki

esse foi o bug mais teimoso que ja enfrentei no Mayinab.

o sintoma: ao navegar rapido entre subjects e subtopics, o app congelava permanentemente. nao era lag. era freeze. tinha q matar o processo e reabrir. (ꐦ°᷄д°᷅)

e o bug era intermitente. funcionava de boa por um tempo, ai do nada travava. impossivel de reproduzir consistentemente, a nao ser com stress test de cliques rapidos.

As 6 Tentativas que Falharam

eu passei DIAS achando q era o GptMarkdown / flutter_math_fork (pq ja tinham me dado dor de cabeça antes com o bug do LaTeX). tentei de tudo:

# O que tentei Funcionou?
1 Substituir GptMarkdown por AppMarkdown nos flashcards
2 Remover IntrinsicWidth dos flashcards e checklists
3 Remover IntrinsicWidth de TODOS os tipos de nota
4 Debounce no callback do ChatScrollObserver
5 RepaintBoundary em cada nota e cada markdown
6 Remover toRebuildScrollViewCallback completamente

a tentativa 4 foi a mais frustrante. o debounce so atrasava o loop pra ~30Hz em vez de eliminar. cada 32ms um setState rodava, a lista reconstruia com tamanhos levemente diferentes, o observer flipava de novo... loop infinito mais lento, mas ainda infinito. ¯\_(ツ)_/¯

A Causa Raiz: ChatScrollObserver

depois de 6 fracassos eu finalmente parei de culpar o markdown e fui ler o codigo fonte do ChatScrollObserver do pacote scrollview_observer. e la tava o monstro.

tres mecanismos interligados causavam o freeze:

1. Cadeia perpetua de addPostFrameCallback

o construtor do ChatScrollObserver agenda observeSwitchShrinkWrap(). esse metodo, internamente, agenda outro addPostFrameCallback. o reattach() chama innerReattachCallBack que dispara _setupSliverController(), que agenda mais um callback. resultado: loop infinito de callbacks que nunca para. eh um trem desgovernado.

// o construtor ja comeca agendando o caos
ChatScrollObserver(this.observerController) {
  WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
    observeSwitchShrinkWrap(); // ← agenda OUTRO callback interno
  });
}

2. standby() força re-layout sem correcao de posicao

o standby() chama viewport.markNeedsLayout() pra forcar re-layout, mas sem ChatObserverClampingScrollPhysics (que eu nunca configurei no ListView), a correcao de posicao nunca eh executada. o markNeedsLayout() so causa trabalho desnecessario que alimenta o loop de observacao.

3. Mismatch de shrinkWrap

ChatScrollObserver comeca com innerIsShrinkWrap = true, mas meu ListView.builder tinha shrinkWrap: false hardcoded. o observeSwitchShrinkWrap detecta a diferenca, flipa pra false, chama reattach(), que agenda novos callbacks... e o ciclo se repete pra sempre. ヽ(°〇°)ノ

A Solucao (Tentativa 7)

removi o ChatScrollObserver inteiramente. sem meias medidas.

// ANTES (bugado):
_chatScrollObserver = ChatScrollObserver(_observerController)
  ..fixedPositionOffset = 5;

// DEPOIS (funcional):
// nada. removido. bye bye.

mantive o ListObserverController + ListViewObserver puros (pq esses sao usados pra jumpTo, animateTo e paginacao via onObserve). eles nao tem os side effects malucos do wrapper de chat.

Bonus: Bugs que achei no caminho

enquanto investigava o freeze, achei mais 3 bugs escondidos q contribuiam pro caos:

LEAK DE FOCUSNODE

era criado inline dentro do build() a cada rebuild, nunca era dispose()d. cada rebuild = novo FocusNode orfao. movido pra campo da classe com dispose() correto.

LEAK DE STREAM SUBSCRIPTION

o _watchChanges() no PaginatedNotesNotifier criava nova subscription sem cancelar a anterior quando os parametros mudavam. adicionei _changeSubscription?.cancel() antes de criar nova.

REBUILDS REDUNDANTES

o _refreshLoaded() substituia toda a state com nova lista mesmo quando o conteudo era identico (mesmos IDs, mesmos updatedAt). adicionei deep equality check pra pular quando nada mudou de verdade.

Resultado

Metrica Antes Depois
Freeze intermitente sim nao
dart analyze lib/ 0 erros 0 erros
flutter test 720 passed
Tentativas 6 falhas 1 sucesso

Licao aprendida

LEIA O CÓDIGO FONTE

leia o codigo fonte dos pacotes que vc usa. eu perdi dias achando q era o GptMarkdown, o IntrinsicWidth, o layout... quando o problema era um pacote de scroll que eu nunca tinha aberto o source. se eu tivesse lido o construtor do ChatScrollObserver no dia 1, teria economizado 6 tentativas.

e desconfie de qualquer coisa que agenda addPostFrameCallback dentro de outro addPostFrameCallback. isso eh receita pra loop infinito. (⌐■_■)