Skip to content

Make Advertisement serializable#1215

Closed
twyatt wants to merge 1 commit into
mainfrom
cursor/serializable-advertisement-d99b
Closed

Make Advertisement serializable#1215
twyatt wants to merge 1 commit into
mainfrom
cursor/serializable-advertisement-d99b

Conversation

@twyatt

@twyatt twyatt commented Jul 15, 2026

Copy link
Copy Markdown
Member

Closes #861

Adds kotlinx.serialization support to Advertisement and PlatformAdvertisement, so they can be used (for example) as navigation arguments with Compose Navigation (per the request in #861 / discussion #788):

@Serializable
data class PeripheralDetails(val advertisement: Advertisement)

Design

The platform advertisement implementations wrap live platform objects (ScanResult, CBPeripheral + advertisement dictionary, BluetoothAdvertisingEvent), which cannot themselves be serialized. Instead:

  • Serialization captures a snapshot of the advertisement data into an internal AdvertisementCapture (the wire representation): name, peripheralName, identifier (as String), isConnectable, rssi, txPower, uuids, full service data and manufacturer data maps, and (Android only) the raw scan record bytes.
  • Deserialization restores a PlatformAdvertisement holding the captured data. Restored advertisements are not "live" — properties reflect the advertisement at the time it was serialized.
  • Advertisement and PlatformAdvertisement are annotated with @Serializable(with = ...) (AdvertisementSerializer / PlatformAdvertisementSerializer), so no additional configuration (contextual serializers, etc.) is needed at usage sites.

Serialized advertisements are only guaranteed to deserialize on the same platform they were serialized on (identifier is a platform specific value that is not portable across platforms).

Platform notes

Platform Restored implementation Notes
Android CapturedAndroidAdvertisement Also Parcelable (via custom Parceler). bondState/bluetoothDevice are retrieved on-demand from the local adapter, so Peripheral(advertisement) works with restored advertisements.
Apple CapturedCoreBluetoothAdvertisement cbPeripheral is retrieved on-demand via retrievePeripheralsWithIdentifiers (throws NoSuchElementException if unknown to the system, mirroring Peripheral(Identifier)).
JS / WasmJS CapturedWebBluetoothAdvertisement Web Bluetooth provides no synchronous way to recover a BluetoothDevice, so Peripheral(advertisement) now fails with a descriptive IllegalArgumentException for restored advertisements (directing users to Peripheral(Identifier)).
JVM (btleplug) BtleplugAdvertisement Already a plain data holder; restored directly (identifier reconstructed via String.toIdentifier()).

Other changes:

  • kable-core now applies the org.jetbrains.kotlin.plugin.serialization plugin and exposes kotlinx-serialization-core as an api dependency.
  • Extracted BluetoothDevice.toBondState() (previously inlined in ScanResultAndroidAdvertisement) for reuse by the restored implementation.
  • Peripheral(Advertisement) on Apple now accepts any PlatformAdvertisement (previously hard-cast to CBPeripheralCoreBluetoothAdvertisement).
  • README: new "Serialization" section under Scanning.
  • API dumps updated (Companion.serializer() on the interfaces, plus the two public serializer objects).

Testing

  • commonTest: JSON round-trip of the capture (runs on JVM, Android host, JS, and WasmJS).
  • androidHostTest (Robolectric): JSON round-trip of a ScanResult-backed advertisement via PlatformAdvertisementSerializer (verifies all data retained), stability of re-serialization, AdvertisementSerializer/PlatformAdvertisementSerializer equivalence, and Parcel round-trip of a restored advertisement.
  • Verified locally (Linux): compileKotlinJvm, compileKotlinJs, compileKotlinWasmJs, assembleAndroidMain, jvmTest, testAndroidHostTest, jsTest/wasmJsTest (only pre-existing environment-dependent Web Bluetooth availability tests fail in headless CI-less Chrome), lintKotlin, apiCheck, and apiDump. Apple targets could not be compiled locally (macOS-only) and rely on CI.
Open in Web Open in Cursor 

Adds kotlinx.serialization support to Advertisement and
PlatformAdvertisement (e.g. for use as Compose Navigation arguments, see
issue #861).

Serialization captures a snapshot of the advertisement data (via an
internal AdvertisementCapture wire representation). Deserialization
restores a PlatformAdvertisement holding the captured data (restored
advertisements are not "live").

- Advertisement and PlatformAdvertisement are annotated with
  @serializable (with AdvertisementSerializer and
  PlatformAdvertisementSerializer, respectively), so they can be used in
  serializable classes without additional configuration.
- Each platform provides capture/restore implementations:
  - Android: CapturedAndroidAdvertisement (also Parcelable); bondState
    and bluetoothDevice are retrieved on-demand from the local adapter.
  - Apple: CapturedCoreBluetoothAdvertisement; cbPeripheral is retrieved
    on-demand from the system.
  - JS/WasmJS: CapturedWebBluetoothAdvertisement; Peripheral cannot be
    created from a restored advertisement (Web Bluetooth requires a
    BluetoothDevice), so Peripheral(Advertisement) now fails with a
    descriptive error for restored advertisements.
  - JVM: restores as BtleplugAdvertisement (which is already a data
    holder).

Closes #861

Co-authored-by: Travis Wyatt <travis.i.wyatt@gmail.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0fbc014. Configure here.

}

internal actual fun AdvertisementCapture.restore(): PlatformAdvertisement =
CapturedAndroidAdvertisement(this)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android restored ad Peripheral crash

High Severity

Deserializing an Advertisement on Android yields a CapturedAndroidAdvertisement, but Peripheral(advertisement) still requires a ScanResultAndroidAdvertisement. Navigation or other flows that decode a serialized advertisement and call that builder will fail with a ClassCastException instead of using the restored bluetoothDevice path described for this platform.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0fbc014. Configure here.

@twyatt twyatt added the minor Changes that should bump the MINOR version number label Jul 15, 2026 — with Cursor
@twyatt

twyatt commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

This PR exposed some complexities that I don't feel comfortable introducing into Kable. See #861 for more details.

@twyatt twyatt closed this Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Changes that should bump the MINOR version number

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make Advertisement serializable

2 participants