A Bluetooth terminal for Android and iOS (Kotlin Multiplatform + Compose Multiplatform): scan for devices, connect, and send and receive data. BLE first on both platforms; Bluetooth Classic (SPP) is planned for Android only, because iOS has no public API for it.
| Devices | Terminal | Settings |
|---|---|---|
![]() |
![]() |
![]() |
Android, connected to an ESP32 running the Nordic UART Service. MAC addresses and other nearby devices are blurred.
| Android | iOS | |
|---|---|---|
| Bluetooth availability, permission request, "turn on Bluetooth" | ✅ | placeholder |
| Scan for BLE devices, stop / scan again | ✅ | placeholder |
| Connect, connection status (connecting, failed, lost), disconnect | ✅ | placeholder |
| Terminal: send text, see sent and received messages | ✅ | placeholder |
| Serial profile detection (Nordic UART, HM-10, FFF0, Microchip) | ✅ | placeholder |
| Settings: services and characteristics, choose where to send and what to listen to, read a value | ✅ | placeholder |
iOS builds and shows "Bluetooth not supported" until the CoreBluetooth implementation of the library lands.
| Module | Role | Depends on |
|---|---|---|
androidApp / iosApp |
Platform shells: MainActivity + Application (starts Koin) / SwiftUI entry point |
:app |
:app |
Composition root: App() (theme, NavHost), Koin setup (initKoin, app scope, BluetoothSession, platform BluetoothClient), iOS framework |
everything |
:feature:terminal |
One feature for one user goal (connect and talk): Devices, Terminal and Settings screens | :bluetooth, :systemdesign |
:bluetooth |
Reusable KMP BLE library: no UI, no DI, no app types; only coroutines | — |
:systemdesign |
Shared theme (planned, empty for now) | — |
Modules are cut per feature, not per screen. Independent features (e.g. macros, history) will get their own modules.
One package per screen, each with its own Clean Architecture layers, plus a shared connection/
package. Screen packages never import each other.
| Package | presentation | domain | data |
|---|---|---|---|
connection/ (shared) |
— | ConnectionStatus, channel models, ConnectionRepository, ChannelRepository, ObserveConnection, Disconnect |
ConnectionRepositoryImpl, ChannelRepositoryImpl |
devices/ |
DevicesScreen, DevicesViewModel, BluetoothActions (expect/actual) |
Device, DeviceScan, DeviceRepository, ScanDevices, ConnectToDevice |
DeviceRepositoryImpl |
terminal/ |
TerminalScreen, TerminalViewModel |
TerminalMessage, ObserveMessages, SendMessage |
— (uses ChannelRepository) |
settings/ |
SettingsScreen, SettingsViewModel |
ServiceSettings, ObserveServiceSettings, SetSendTarget, SetListening, ReadCharacteristic |
— (uses ChannelRepository) |
- presentation depends on domain (use cases) only; domain is pure Kotlin with no
:bluetoothtypes; only data uses the library. - Each package has its own
internalKoin module (di/). The feature exposes two public entry points:terminalScreens()(navigation/) andterminalFeatureModule(di/), which includes the package modules.
| Source set | Contents |
|---|---|
commonMain |
BluetoothClient (adapter: state, scan, connect), BleConnection (one link: read, write, observe), BluetoothSession (holds the current link: Idle, Connecting, Connected, Failed, Lost), model/, serial/ (SerialProfiles, writeChunked), testing/ (fakes for other modules' tests) |
androidMain |
AndroidBleClient, AndroidBleConnection (BluetoothGatt), GattQueue (one GATT operation at a time) |
iosMain |
Placeholder client reporting Unsupported; CoreBluetooth implementation planned |
androidApp/ Android shell (MainActivity, Application)
iosApp/ iOS shell (Xcode project, SwiftUI)
app/ composition root (App.kt, di/)
feature/terminal/ connection/, devices/, terminal/, settings/, navigation/, di/
bluetooth/ BLE library (commonMain, androidMain, iosMain)
systemdesign/ theme (planned)
art/ README images and screenshots, app icon and store graphics
store/ Google Play listing texts and privacy policy
- Android:
./gradlew :androidApp:installDebug(a real phone: the emulator has no BLE devices nearby) - iOS: open /iosApp in Xcode and run it
Minimum Android version: 12 (API 31), so only BLUETOOTH_SCAN / BLUETOOTH_CONNECT are needed (no location).
./gradlew :bluetooth:testAndroidHostTest :feature:terminal:testAndroidHostTest :app:testAndroidHostTest
./gradlew :bluetooth:iosSimulatorArm64Test :feature:terminal:iosSimulatorArm64Test :app:iosSimulatorArm64TestTests use fakes, no hardware: FakeBluetoothClient / FakeBleConnection from :bluetooth, fake
repositories in the feature.
Learn more about Kotlin Multiplatform…
- Create an upload key once (keep the file and passwords safe, outside git; an absolute
storeFilepath lets the keystore live outside the repo):keytool -genkeypair -v -keystore keystore/upload-keystore.jks -alias upload -keyalg RSA -keysize 2048 -validity 10000 - Copy
keystore.properties.exampletokeystore.propertiesand fill in the passwords. On CI, setANDROID_KEYSTORE_FILE,ANDROID_KEYSTORE_PASSWORD,ANDROID_KEY_ALIASandANDROID_KEY_PASSWORDinstead. - Build the bundle for Google Play:
./gradlew :androidApp:bundleRelease(output:androidApp/build/outputs/bundle/release/androidApp-release.aab).


