Projectoverzicht
Arcana is een AI-gestuurde tarotmetgezel die gebruikers door kaartleggingen leidt via een chatinterface. De AI-backend — verantwoordelijk voor de tarotkennis en de duidingslogica — is door het team van de klant ontwikkeld. Onze verantwoordelijkheid was de mobiele Flutter-client: een snelle, verzorgde chatervaring op zowel iOS als Android.
De aard van het product leverde meteen een technische uitdaging op. Chatsessies worden lang. Gebruikers komen dagelijks terug, verzamelen leggingen, en de berichtgeschiedenis kan duizenden regels bereiken. Tegelijk streamt de AI zijn antwoorden in realtime — opgemaakte tekst met vetgedrukte duidingen, cursieve kaartnamen, koppen en opsommingen. Een chatscherm bouwen dat allebei elegant afhandelt, op 60 fps op goedkope hardware, was de kern van ons werk.

Dagelijkse kaart trekken

Streamend chatantwoord

Kaartlegging
Realtime streamen via SSE
De AI-antwoorden komen binnen als een stroom tokens, niet als één lading. We bouwden een Server-Sent Events-client (SSE) in Flutter die een blijvende HTTP-verbinding met de backend opent en de tokenstroom verwerkt terwijl die binnenkomt. Elk binnenkomend brokje wordt aan het actieve bericht in de chat toegevoegd, wat gebruikers het vertrouwde "typ"-effect geeft — de duiding die woord voor woord verschijnt.
SSE in Flutter afhandelen vroeg om zorgvuldig beheer van de verbindingslevenscyclus: opnieuw verbinden bij netwerkonderbrekingen zonder gedeeltelijke inhoud te verliezen, onvolledige UTF-8-reeksen op brokgrenzen bufferen, en zorgen dat de UI bij elk binnenkomend token alleen de betreffende berichtwidget opnieuw opbouwt en niet de hele lijst.
Markdown renderen terwijl het binnenkomt
Omdat de AI zijn antwoorden in Markdown opmaakt — vet voor kaartnamen, cursief voor trefwoorden, koppen voor onderdelen van een legging, lijsten voor kernpunten — moest de chatrenderer Markdown stapsgewijs ontleden en tonen, en in realtime bijwerken naarmate nieuwe tokens binnenkomen.
We bouwden een eigen incrementele Markdown-renderer die de groeiende tekenreeks bij elke update verwerkt zonder het hele bericht opnieuw te ontleden. Onvolledige tokens aan het einde van de huidige buffer (een niet-gesloten ** of een half geschreven kop) worden in de wacht gehouden en als platte tekst getoond tot het scheidingsteken is opgelost. Dat voorkomt geflikker en haperende opmaak terwijl het antwoord afloopt.
Prestaties meten op eenvoudige toestellen
Lange chatgeschiedenissen zijn een gegeven bij een app voor dagelijks gebruik. We ontwierpen het systeem om 5.000+ berichten aan te kunnen zonder achteruitgang, en valideerden dat met gestructureerde benchmarks op Android-toestellen uit het laagste segment — telefoons die representatief zijn voor de budgetklasse, waar de druk op Flutter-prestaties het hoogst is.
Onze benchmarksuite scrolde door chatlijsten van uiteenlopende omvang (500, 2.000 en 5.000 berichten) terwijl er tegelijk een SSE-antwoord binnenstroomde, en mat gedurende de hele rit de frametijden. De eerste builds lieten haperingen zien bij snel scrollen vanaf zo'n 2.000 berichten, doordat de lay-outberekeningen voor de Markdown-bellen duur werden.
We pakten dat met een aantal gerichte optimalisaties aan:
Luie lay-outcache: gerenderde Markdown-lay-outs worden per bericht-ID gecachet. Zodra een berichtbel is uitgelijnd, worden de afmetingen en de widgetboom bij volgende scrollrondes hergebruikt in plaats van opnieuw berekend.
Virtualisatie op basis van slivers: de chatlijst gebruikt een SliverList met een eigen delegate die geen widgets buiten beeld opbouwt. Alleen berichten binnen of vlak bij het zichtbare venster worden aangemaakt, waardoor de widgetboom ondiep blijft ongeacht het totale aantal berichten.
Geïsoleerde herbouw van de stroom: het streamende bericht onderaan de lijst wordt los van de historische lijst beheerd. SSE-tokenupdates leiden tot een gerichte herbouw van alleen de actieve bel, en laten de gevirtualiseerde lijst volledig ongemoeid.
Na deze optimalisaties bleef de frametijd in alle benchmarkscenario's consequent onder de 16 ms — ook bij geschiedenissen van 5.000 berichten met een actieve stroom — op de beoogde eenvoudige toestellen.
Resultaat
De uiteindelijke client levert een chatervaring die bij elke diepte van de geschiedenis en op elk toestel in het beoogde bereik vloeiend en responsief aanvoelt. Streamende antwoorden verschijnen soepel in realtime met de juiste Markdown-opmaak, en langdurige gebruikers die honderden sessies verzamelen lopen nooit tegen traagheid aan. De prestatiebasis die we legden geeft het product ruimte om te groeien zonder de renderarchitectuur opnieuw te bekijken.
De client van Arcana is gebouwd als onderdeel van ons werk aan Flutter-appontwikkeling, waar renderprestaties als deze een kernpunt zijn.
