Back to blog feed

Tutorials

BLE in Flutter: Pairing, Reconnects, and Data Reliability

Field-tested BLE patterns for pairing, reconnects, and packet reliability across Android and iOS under unstable radio conditions.

BLE in Flutter: Pairing, Reconnects, and Data Reliability

BLE looks simple in demo mode and brutal in production mode. Real users walk between rooms, background the app, connect to old firmware, and reconnect through noisy radio conditions. Reliability comes from protocol discipline, not from a single retry loop.

Common production failure boundaries

Teams usually hit the same failure pattern:

  • Connection established but services never discovered after app resume.
  • Device bonded, but key exchange invalid after OS update.
  • Notification stream active, but packets arrive out of order.
  • Reconnect loop drains battery and still fails under weak signal.

You cannot solve these with "try again" logic alone. You need explicit connection state control and a packet contract.

A connection state machine that survives real usage

We implemented a strict state machine:

idle -> scanning -> connecting -> discovering -> ready -> degraded -> reconnecting -> ready

Key rules:

  • Any operation outside ready is rejected with a typed error.
  • Service discovery has a timeout and a single retry before full reconnect.
  • Background resume always re-validates characteristics before writes.
  • Reconnect attempts use jittered backoff with a maximum retry budget per session.

This removed hidden transitions and made logs debuggable.

Pairing and bonding strategy

For devices that require secure writes, we separated "pairing success" from "session readiness". A paired device still had to pass a capability handshake (firmware version, required characteristics, MTU support) before we considered the session healthy.

When handshake failed, we surfaced a clear state to users instead of silently looping. That single UX change reduced support tickets because users could recover by re-pairing once instead of force-closing repeatedly.

Data reliability contract

Every BLE packet carried:

  • Sequence number
  • Message type
  • CRC or checksum
  • Ack requirement flag

Write path used an application-level ack for critical commands. If ack was missing, the command was retried idempotently with the same sequence number. Duplicate command handling lived on device side and in app command queue.

This kept control messages consistent even when radio quality dipped.

Reconnect control without battery abuse

Reconnect policy was split by trigger:

  • Passive drop while screen on: quick retries with short jitter.
  • Drop during background: defer until foreground unless operation is critical.
  • Device out of range: mark unreachable and stop active scanning after budget exhausted.

We also exported reconnect metrics by device model and OS version. That exposed model-specific instability we would have blamed on app code otherwise.

Test matrix before release

Before every release, we ran:

  • Foreground and background reconnect on Android + iOS.
  • Weak-signal packet loss simulation.
  • Firmware mismatch and compatibility downgrade path.
  • Long-session soak test for notification stream integrity.

Without this matrix, BLE regressions appear only after launch.

Practical takeaway

Reliable BLE in Flutter is a protocol + lifecycle problem. Treat connection states as explicit contracts, enforce packet integrity rules, and cap reconnect behavior with measurable budgets.

Related reading:

Comments

Threaded replies are enabled. Upvote useful ideas.