Skip to content
Hardware and mobile apps September 24, 2026

Bluetooth is never finished: what we learned connecting hardware to mobile apps

Pairing a device and reading data from it is roughly a fifth of the work. The rest is differences between Bluetooth versions, phones and operating systems, permissions, the connection lifecycle, the protocol on top of it, and diagnostics in the field. We draw on a real project in which we have been working with a Bluetooth reader since 2023 – along with the mistakes we made along the way.

Summary

  • Pairing and reading data is only a small part of the work – most of it is platforms, versions, permissions, the connection lifecycle and the protocol on top of it.
  • “Bluetooth” is not one technology: classic Bluetooth and BLE have different events and different callbacks, and in practice you need to handle both.
  • Over the past few years Android has changed its permission rules repeatedly, and iOS is a different world altogether – a single Dart interface sits on top of two completely different implementations.
  • For devices with a secured protocol you can't do without the manufacturer's SDK – and you are tied to its release cycle. It once cost us a rewrite of the native layer.
  • Bugs that can't be reproduced during development can only be solved by collecting context from the field.
  • A Bluetooth integration keeps costing money after launch – budget for it as an ongoing item, not a one-off development effort.

The project we draw on

It is a mobile app for healthcare staff working in the field. The app reads data from a chip card through an external reader and sends it to a server. We won't go into the contents of the cards – what matters here is the path the data takes from the reader to the app.

The first version of the app was built in March 2022 in about ten days. The reader connected by cable over USB, on both Android and iOS. Bluetooth arrived in May 2023. Since then the app has been changing every year – with a new operating system, a new version of the manufacturer's SDK, a new phone in the field. USB now serves only as a fallback on older Android devices.

The app is written in Flutter. But it is not Dart (Flutter's language) that talks to the reader – it is native code for each platform. Dart calls it through what is known as a MethodChannel – a channel between Flutter and code written directly for Android or iOS. On Android this was originally Java and C++, on iOS the manufacturer's library.

We didn't know most of what we write about below from the start. We learned it from bugs that came in from the field.

Pairing shows up on the desk. Everything else shows up only in production, on phones you have never seen.

“Bluetooth” is not one thing

When a customer says “we want a Bluetooth reader”, they may mean two different technologies. Classic Bluetooth (we call it BT3 in the project) and Bluetooth Low Energy (BLE, also BT4) share a name, but in code they behave as two separate things. Each has its own set of events and its own callbacks – functions the system calls when something happens.

That's why our app on Android has to support both at once – it holds three reader instances (for classic Bluetooth, for BLE and for USB) and searches for them in parallel. Once it finds something, it must decide which kind of reader it is and assign it to the right instance. iOS works differently – we get to that below.

Classic Bluetooth (BT3)

  • Events – its own set
  • Callbacks – its own set
  • In the app – a separate reader instance

Bluetooth Low Energy (BLE)

  • Events – a different set than BT3
  • Callbacks – a different set than BT3
  • In the app – a separate reader instance

USB

  • Use – fallback on older Android devices
  • In the app – a separate reader instance

Permissions on Android: the rules change over time

Bluetooth rules in Android have been changing gradually and the app had to keep up:

  1. Up to Android 11

    The app had to ask for location permission too, even though it doesn't use location at all – otherwise it won't find Bluetooth devices.

  2. From Android 12

    Separate permissions for scanning and connecting to devices were added; location no longer has to be required.

  3. From Android 14

    Another technical requirement was added for how the app listens to system notifications about Bluetooth state.

Every permission also means a system dialog. The dialogs appear to the user one after another, and for each one we have to handle what happens after it is denied. On top of that there is one more state: Bluetooth is turned off, or the user turns it off while the app is running.

Important: The user won't say “it doesn't work”. They will say “the app does nothing”. The cause is often a single dismissed dialog – so the app must be able to say clearly what is missing.

iOS is a different world

iOS follows different rules – and it's not just different permissions. Without special certification from the manufacturer (Apple's MFi program), an app can hardly reach classic Bluetooth on iOS, which is why we rely mainly on BLE there. The system also shows its own dialog with a text we define ourselves, and we don't get to the connection details – iOS doesn't expose them.

The result: in Dart the interface looks the same for both platforms, but underneath run two completely different implementations. Each has its own bugs and its own behaviour.

Your own GATT, or the manufacturer's SDK?

GATT is how a BLE device exposes its data: it publishes it as services and characteristics (values) that can be read or written. If the device documents its GATT profile, you can talk to it directly, without the manufacturer's library.

With our reader that wasn't an option. Readers that work with sensitive data on a card usually don't communicate over an open, documented GATT profile – the manufacturer secures the transmission with its own closed protocol, and you can't crack it without their SDK. So in Flutter we use the flutter_blue_plus package only to detect the state of the Bluetooth adapter (on, off). The connection to the reader and the reading of data are handled by the manufacturer's SDK.

Manufacturer's SDK

  • Plus – faster development, a ready-made interface to the reader
  • Minus – dependence on a library we don't control

Own GATT

  • Plus – full control over every step of the connection
  • Minus – more work and our own responsibility for behaviour on every phone

This dependency has a price. In October 2025 our app paid it in full – we don't control the library we depend on, and when the manufacturer released a new version, we had to rewrite the native layer (see below).

Your own GATT therefore makes sense only for devices that publish their profile – typically where data security isn't critical. The manufacturer's SDK, on the other hand, is often the only way with devices that secure data with their own protocol, like our reader. The question “do we need their SDK, or will a generic Bluetooth library do?” is therefore worth asking right at the start, before the budget estimate.

Connection lifecycle

Connecting to the reader is not a one-off action. It is a loop that runs the whole time the user is on the screen. In our project it looks like this:

  • we search for the reader periodically, roughly every 2.5 s
  • we read data roughly every 2 s
  • we don't flood the USB reader with queries – the loop doesn't ask it on every pass, only on every n-th one
  • when leaving the screen everything stops, and on disconnect the state is cleaned up
  • a card can be inserted, removed or unpowered, and each state is handled differently
  • the user can manually try to reconnect (3 attempts)

The numbers belong to our reader and will differ for another device – but the principle (regular searching, repeated reading, cleanup after disconnect) is common for hardware readers.

Even seemingly trivial things can go wrong. In April 2025 we fixed a bug where the reader didn't stop when the user left the screen. The result was overlapping connections and unnecessary battery drain.

The protocol on top of Bluetooth

A working connection doesn't mean working reading. Another protocol runs on top of Bluetooth – the one we use to talk to the card itself. It is based on APDU commands, short binary messages that tell a chip card what to do. This layer belongs to reading chip cards, not to Bluetooth itself – with another type of device (a sensor, a printer) the protocol on top of the connection will be completely different, but there will be one all the same. Several things surprised us here:

  • We send commands one at a time, with a short pause between them – the card can't handle one query right after another.
  • A response sometimes doesn't arrive in one piece – we have to ask separately for the rest of the data.
  • The data is compressed and in a text encoding that isn't common today – without unpacking it correctly it is unreadable.

This particular combination is not a property of Bluetooth or of readers in general – it depends precisely on how the card and the data format on it are built. A different card, a different project, different quirks; the only general thing is that there will be some.

Important: Even when the connection works, data errors look the same as connection errors. The user only sees that the card wasn't read. They can't tell whether Bluetooth dropped or we unpacked the data wrongly.

Diagnostics in the field

In January 2023 errors started coming in from readers that we couldn't reproduce during testing. So we changed our approach: instead of hunting for the cause at a desk, we started collecting context directly from the field. To every error we attach the phone model, the operating system version and card attributes (the ATR – a short identifier the card returns when powered on – and status codes). We send only new errors, to avoid a flood of reports.

The main source of errors is device variability: different phone manufacturers and a range of Android versions. Such variety is hard to cover with your own testing.

UX for people who aren't technicians

The app's users are not technicians and shouldn't have to be. They want to insert a card and see the result. Everything we described above has to stay hidden behind a simple interface:

  • Automatic connection – the app finds and connects to the reader on its own.
  • Reading progress indicator – the user can see that something is happening.
  • Separate error messages for iOS and Android – the causes and the fixes differ between platforms.
  • Locking the app in the background – when it moves to the background, the app locks to protect personal data.

Vendor lock-in and updates

The manufacturer's SDK comes as binary libraries, separately for Android and iOS and per processor architecture. We have to keep them at the right version and update them with every new operating system version.

In April 2025 that meant new versions of the reader SDK and raising Android's target SDK to 34. In October 2025 the manufacturer released a new SDK version. We dropped our original bridge between Java and C++ (JNI), built around the older SDK, and moved to the manufacturer's new Java API with native libraries for each processor architecture.

It took a week: on October 16 we built the app for the first time, still without Bluetooth, and by October 23 Bluetooth was already working on Android. We rewrote roughly 500 lines of native code. The rewrite wasn't big, yet it was a week of work we couldn't have planned in advance.

Can pairing be simplified?

Yes, and it's worth keeping an eye on. Android and iOS are gradually introducing newer ways of pairing that solve part of the problems described above at the system level – the system finds and pairs the device on its own, without unnecessary dialogs. Whether that suits your device depends on what its hardware and the manufacturer's SDK allow – it is worth finding that out already when estimating the project.

Technical detail: on Android this is CompanionDeviceManager (since Android 8), on iOS AccessorySetupKit (since iOS 18). Newer versions of both platforms also report a lost pairing better, and Android 17 adds automatic re-pairing.

What this means for budget and planning

Our experience points to a few recommendations for anyone planning an app with hardware:

  • Plan a regular maintenance budget, not just the first development. A new operating system or a new manufacturer SDK can mean more work.
  • Before estimating, find out what kind of Bluetooth device it uses (classic, BLE, both) and what SDK the manufacturer offers for it – for which platforms and processor architectures.
  • Budget Android and iOS as two separate implementations, even if Flutter unifies them on top.
  • Include diagnostics in the very first version – we added ours only after the first errors from the field. Bugs that can't be reproduced can't be solved without collecting context.
  • Ask the manufacturer how often it releases new SDK versions and how long it supports older ones. The answer determines how often you will be modifying the native layer.

Planning an app connected to hardware?

We connect mobile apps with readers, sensors, printers, measuring instruments and medical devices. Before we start, we're happy to go through what device you have, what SDK its manufacturer offers and where we see the biggest risks.

Get in touch – a no-obligation consultation will help you estimate risks and budget before the decision is made.

Discuss your project
Back to blog