Je hebt een hardwareproduct en je hebt er een app bij nodig. Aan de oppervlakte simpel genoeg. Maar zodra Bluetooth Low Energy in beeld komt, groeit de scope op manieren die pas zichtbaar worden als je al drie maanden bezig bent.
Wij leerden dat bij het bouwen van de companion app voor Classified Cycling, een Belgisch bedrijf dat draadloze aandrijftechnologie maakt voor fietsers. Hun hardware verbindt over BLE, heeft over-the-air firmware-updates nodig en draait buiten, waar Bluetooth zich onvoorspelbaar gedraagt. De app beheert intussen verbindingen met vier verschillende hardwaretypes, integreert met twee externe partnerecosystemen en verzorgt de firmwaredistributie voor allemaal. Wat begon als “een pairing-scherm en wat instellingen” werd een volledig uitgebouwd connectiviteitsplatform.
Deze post behandelt wat BLE companion apps echt vragen, waarom Flutter er goed mee overweg kan, waar de echte complexiteit zit en welke vragen de moeite waard zijn voor je begint.
Wat BLE companion apps echt doen
Een companion app is de mobiele interface naar een hardwaretoestel. Voor de meeste producten betekent dat vier dingen:
Pairing en device discovery. De app scant naar BLE-randapparatuur in de buurt, herkent het juiste toestel (meestal aan fabrikantspecifieke advertising data), authenticeert ermee en bewaart de bond. Op iOS beheert het systeem de bonding-status. Op Android doe je dat zelf. Die twee gedragen zich verschillend genoeg om in de praktijk twee aparte engineeringproblemen te zijn.
Configuratie. Eenmaal gekoppeld leest en schrijft de app configuratiewaarden via BLE-characteristics. Voor Classified Cycling gaat het om de toewijzing van shifterknoppen, ritmodi en schakeldrempels. Elke instelbare parameter is een GATT-characteristic met een UUID, een datatype en lees- en schrijfrechten.
Live telemetrie. Sommige toestellen zenden continu data uit. De hub van Classified stuurt batterijniveau, verbindingsstatus en schakelmomenten door als BLE-notificaties. De app abonneert zich op die characteristics en werkt de UI in realtime bij. De uitdaging is dat betrouwbaar doen terwijl de telefoon in een truizak zit, de hub op een fietsframe hangt en er een peloton koolstofvezel en metaal tussen zit.
OTA firmware-updates. Dit is het lastigste stuk. Firmware over BLE pushen betekent enkele honderden kilobytes overzetten via een traag, verlieslatend kanaal, op een manier die het toestel niet kan bricken als het onderbroken wordt. De meeste hardware gebruikt het Device Firmware Update-protocol van Nordic Semiconductor (Nordic DFU) of iets vergelijkbaars. De app orchestreert de overdracht, handelt retries af en moet netjes herstellen als de verbinding halverwege wegvalt.
Waarom Flutter werkt voor BLE
Het BLE-ecosysteem van Flutter is samengekomen rond twee goed onderhouden packages: flutter_reactive_ble en flutter_blue_plus. Beide dekken de volledige levenscyclus van een verbinding. Beide ondersteunen iOS en Android vanuit één codebase.
Dat punt van die ene codebase weegt zwaarder dan het klinkt. BLE loopt op iOS en Android al op OS-niveau uiteen. Bouw je aparte native apps, dan schrijf en onderhoud je twee aparte BLE-stacks. Met Flutter schrijf je de BLE-laag één keer en vang je de platformverschillen op in een afgebakende laag in plaats van verspreid over twee codebases.
Het plugin-ecosysteem is ook volwassen voor specifieke use cases. Nordic DFU heeft een goed ondersteund Flutter-package. iBeacon-detectie, GATT-profielen voor gangbare hardwarecategorieën, beheer van achtergrondscans: dat zijn opgeloste problemen met onderhouden packages, geen dingen die je vanaf nul bouwt.
Voor wie de beslissing neemt: Flutter voor BLE betekent één team, één codebase en een ecosysteem met genoeg diepgang om de meeste eisen aan een companion app meteen af te dekken. Het realistische alternatief, aparte native iOS- en Android-teams, kost meer en duurt langer voor functioneel hetzelfde resultaat.
Waar de echte complexiteit zit
Het pairing-scherm is het makkelijke deel. Dit is wat echt tijd kost.
iOS en Android bonden anders. iOS beheert BLE-bonding op OS-niveau. Android legt het bloot op app-niveau. Herinstalleert een gebruiker op iOS de app, dan onthoudt het OS de bond nog wel, maar is het opgeslagen device-ID van de app weg, waardoor de app zijn eigen gekoppelde toestellen niet terugvindt zonder opnieuw te scannen. Op Android beheer je bonding zelf. Die randgevallen vragen expliciete afhandeling.
Bluetooth-omstandigheden buiten. De Classified-app draait op fietsen. De telefoon zit in een truizak, de hub hangt onder het zadel en de renner beweegt aan 40 km/u door een veld van koolstof en metaal. Een BLE-signaal plant zich in die omstandigheden niet betrouwbaar voort. De app heeft automatische herverbindingslogica nodig, monitoring van de verbindingskwaliteit en een state machine die het verschil kent tussen “tijdelijk losgekoppeld” en “gebruiker is weggewandeld”.
De levenscyclus van een BLE-verbinding. Een BLE-verbinding is geen socket. Ze heeft toestanden: scannen, verbinden, services ontdekken, authenticeren, characteristics lezen, notificaties ontvangen en loskoppelen. Elke toestand kan falen, en het herstel verschilt per fout. Dit correct beheren vraagt een expliciete state machine, geen reeks callbacks. Zo ziet de levenscyclus eruit voor Classified:
Verwerking in de achtergrond. iOS en Android beperken allebei wat apps in de achtergrond mogen doen. Moet de app een BLE-verbinding aanhouden terwijl het scherm uit staat, gebruikelijk bij fitness- en fietsapps, dan moet je de achtergrondmodi expliciet afhandelen. De Core Bluetooth-achtergrondmodi van iOS vragen om declaraties van capabilities en leggen beperkingen op aan wat je mag lezen. De Android-beperkingen op achtergrondscans werden strenger in Android 8 en opnieuw in Android 12.
Dart-isolates voor scanloops. Continu scannen naar BLE vanuit de hoofdisolate van Dart gaat ten koste van de UI-performantie. Het juiste patroon is de scanloop in een aparte Dart-isolate draaien en de scanresultaten via message passing terugsturen naar de UI-isolate. Dat is hetzelfde patroon dat we gebruiken voor blokkerende native operaties in ons Flutter FFI-werk.
Het venster van de firmware-update. Een OTA-update duurt 2 tot 5 minuten over een gangbare BLE-link. In die tijd mag de app de verbinding niet verliezen, mag de batterij van de telefoon niet leeglopen en mag het OS de update niet onderbreken. Dat is geen UI-probleem, dat is reliability engineering.
Geen van deze dingen is onoverkomelijk. Maar elk ervan vraagt bewuste engineering, en de tijd stapelt op. Heeft je team nog nooit een BLE companion app uitgeleverd, dan ligt de schatting voor “de BLE-laag” waarschijnlijk 40 tot 60% lager dan ze zou moeten zijn.
BLE-app tegenover een webinterface
Niet elk verbonden toestel heeft een native companion app nodig. Web Bluetooth bestaat en werkt in browsers op Chromium. Een webdashboard kan voor heel wat use cases configuratie en statusweergave dekken. Zo kies je:
| Vereiste | Native app | Web Bluetooth / webdashboard |
|---|---|---|
| OTA firmware-updates | Ja | Beperkt (enkel bestandsoverdracht) |
| BLE-verbinding in de achtergrond | Ja | Nee |
| iOS-ondersteuning | Ja | Nee (Apple blokkeert Web BLE op iOS) |
| Offline gebruik | Ja | Deels |
| Distributie via de app store | Verplicht | Niet nodig |
| Pushnotificaties | Ja | Beperkt |
| Complexe GATT-profielen | Ja | Ja |
| Snel een eerste versie | Trager | Sneller |
Web Bluetooth draait alleen in Chrome en Edge. Op iOS werkt het helemaal niet. Zitten je gebruikers op iPhones, dan is een webinterface geen volwaardige vervanging voor een app.
Voor use cases die alleen over configuratie gaan, waar Android je enige platform is en gebruikers technisch genoeg zijn om een browser te gebruiken, is Web Bluetooth redelijk. Zodra iOS, verbinding in de achtergrond, OTA-updates of distributie naar consumenten meespelen, heb je een native app nodig.
Wat je vraagt voor je begint
Dit zijn de vragen die we in elk discovery-gesprek over een BLE companion app stellen. Geef je zo’n project uit handen, dan bespaart heldere antwoorden hierop vooraf tijd en voorkomt het verrassingen in de scope.
Hoeveel toesteltypes? Elk toesteltype is een aparte BLE-implementatie. Een hub, een sensor en een afstandsbediening die allemaal vanuit één app beheerd moeten worden, zijn drie aparte verbindingsstacks, geen enkele.
Welke BLE-profielen gebruik je? Standaard GATT-profielen (Heart Rate, Battery, Device Information) hebben bestaande Flutter-packages. Custom profielen vragen werk aan de protocolimplementatie. Voor het D-Fly-protocol van Shimano moesten we bijvoorbeeld hun eigen pairing-sequentie vanaf nul implementeren. Dat beschreven we in detail in de post over de Shimano DI2-integratie.
Zijn OTA firmware-updates vereist? Dat verdubbelt de complexiteit van de BLE-laag. Niet figuurlijk. De retrylogica, de orchestratie van de overdracht en de herstelafhandeling voor OTA vragen ruwweg evenveel engineering als al de rest samen.
Moet de app een BLE-verbinding aanhouden in de achtergrond? Zo ja, reken dan de afhandeling van achtergrondmodi op beide platformen mee, plus testwerk op de volle waaier aan Android-fabrikanten (Samsung, Xiaomi en Huawei doen batterijoptimalisatie elk anders).
Hoe ziet gebruik buiten eruit? Scenario’s met consumentenelektronica (installatie thuis, af en toe iets instellen) stellen heel andere eisen aan betrouwbaarheid dan sport- en fitnessscenario’s (constante beweging, wisselend signaal, soms veiligheidskritisch).
Zijn er regelgevende eisen? Medische toestellen, toestellen die inhaken op veiligheidskritische systemen en toestellen in gereguleerde categorieën (automotive, luchtvaart, medisch) hebben certificeringseisen die de BLE-implementatie beïnvloeden.
Hoe ligt de verdeling over platformen? Zit 80% van je gebruikers op iOS, dan verandert dat de prioritering. Heb je ondersteuning voor Android 8 nodig, dan bepaalt dat welke API’s beschikbaar zijn. Moet je binnen drie maanden op beide platformen live, dan moeten teamgrootte en scope dat weerspiegelen.
Belangrijkste inzichten
De Classified-app begon als een pairing-scherm. Vier jaar en meerdere hardwaregeneraties later beheert ze verbindingen met vier toesteltypes, integreert ze met twee partnerecosystemen en draait ze in omstandigheden waar de meeste connectiviteitscode nooit voor bedoeld was. Die evolutie is typisch.
BLE companion apps zijn geen apps die toevallig Bluetooth gebruiken. De connectiviteitslaag is het product. Ontwerp en bouw ze vanaf het begin zo.
Het voordeel van één Flutter-codebase is reëel bij BLE-projecten. De consolidatie van het library-ecosysteem en het kunnen delen van verbindingslogica tussen iOS en Android drukken de doorlopende onderhoudskost merkbaar.
OTA firmware-updates zijn geen feature voor later. Zijn ze nodig, scope ze dan vanaf het begin. De architectuurbeslissingen die OTA veilig maken, laten zich moeilijk achteraf inbouwen.
De vragen in de vorige sectie zijn niet uitputtend, maar elk team dat ze vooraf niet helder kan beantwoorden, ontdekt zijn scope halverwege het project. Discovery-gesprekken bestaan niet voor niets.
Bouw je een team dat van React Native is gemigreerd, dan is het BLE-verhaal in Flutter een van de duidelijkste winsten. We gaan dieper in op die migratiebeslissing in onze post over migratie van React Native naar Flutter. Is BLE je belangrijkste drijfveer, dan is Flutter de juiste keuze en is de migratie de investering waard.
Het ecosysteem van Flutter reikt verder dan BLE. Evalueer je Flutter ook voor e-commerce, zoals scan-to-order flows met Bluetooth-barcodescanners, dan legt onze post over Flutter-Magento-integratie uit hoe die twee patronen elkaar raken.
Hulp nodig bij de ontwikkeling van een BLE-app?
