| Android | iOS |
|---|---|
![]() |
![]() |
HRAM is a Kotlin Multiplatform app for heart rate & activity tracking with BLE heart rate monitors.
It uses Compose Multiplatform for shared UI, Kotlin Multiplatform for shared logic, Koin for DI, and an SQL
database for storing heart rate activities.
Tested with a Decathlon HRM Belt as an example device.
Status: Work in progress / Prototype
No implied warranty or guarantee of functionality. Use at your own risk.
This project is for educational purposes in software development only.
It is not a medical application and must not be used for medical assessment, diagnosis, monitoring, or treatment.
- macOS
- JDK 17+
- Android Studio Otter 2 Feature Drop | 2025.2.2 RC 2 or newer
- Xcode 26+
git
The project is currently under active development, and only a debug version of the application is available.
Open HRAM in Android Studio. Select the Android configuration for androidApp. Choose a device/emulator. Run. Useful tasks:
./gradlew :composeApp:assembleDebug./gradlew :composeApp:installDebug
Open HRAM in Android Studio. Select the iOS configuration for iosApp. Choose a device/emulator. Run.
/scripts - This directory contains shell scripts for building the iOS application.
Builds the Compose Multiplatform framework for iOS.
Usage:
./scripts/build-framework.sh [SDK] [CONFIGURATION]Parameters:
SDK(default:iphonesimulator): Target SDK -iphonesimulatororiphoneosCONFIGURATION(default:Debug): Build configuration
Examples:
./scripts/build-framework.sh iphonesimulator Debug
./scripts/build-framework.sh iphoneos
./scripts/build-framework.sh # Uses defaultsOutput:
- Framework location:
composeApp/build/xcode-frameworks/{CONFIGURATION}/{SDK}/ComposeApp.framework
Builds the iOS Xcode project.
Usage:
./scripts/build-xcode.sh [SDK] [CONFIGURATION] [SIMULATOR_NAME]Parameters:
SDK(default:iphonesimulator): Target SDK -iphonesimulatororiphoneosCONFIGURATION(default:Debug): Build configurationSIMULATOR_NAME(default:iPhone 16e): Simulator device name (only used for iphonesimulator)
Examples:
./scripts/build-xcode.sh iphonesimulator Debug "iPhone 16e"
./scripts/build-xcode.sh iphoneos Debug
./scripts/build-xcode.sh # Uses defaultsOutput:
- App location:
./build/ios-{SDK}/Build/Products/{CONFIGURATION}-{SDK}/
To build the complete iOS application, run both scripts in sequence:
# Build the framework
./scripts/build-framework.sh iphonesimulator Debug
# Build the Xcode project
./scripts/build-xcode.sh iphonesimulator Debug "iPhone 16e"Unit tests for the shared logic are located in composeApp/src/commonTest. The project utilizes the following testing libraries:
kotlin.test: For standard assertions.kotlinx-coroutines-test: For testing coroutine-based asynchronous code.Mokkery: For creating mocks and stubs of dependencies.
To execute all tests in the composeApp module, run:
./gradlew testDebugUnitTest
To run a specific test class:
./gradlew :composeApp:testDebugUnitTest --tests "com.achub.hram.ble.core.connection.HramConnectionTrackerTest"
Test coverage is generated using Kover To generate an HTML coverage report, run the following command:
./gradlew koverHtmlReport
HRAM focuses on:
- Discovering, connecting, and reading data from BLE heart rate devices.
- Tracking heart rate sessions basic info.
- Visualizing heart rate data using charts and indication views.
- Storing activity data in a local database.
- Sharing core logic (tracking, BLE, database, view models) and UI between Android and iOS.
For compatibility, devices must implement the standard Heart Rate Service (UUID: 0x180D).
The project is organized into packages, with the core logic residing in composeApp/src/commonMain.
composeApp/src/commonMain/kotlin/com/achub/hram/ble/- BLE scanning, connection, and data handling.data/- Database entities, DAOs, and activity repository.di/- Koin dependency injection modules.screen/- screens for each feature (Main, Activities, Record).tracking/- Business logic for managing activity tracking sessions.view/- Reusable UI components like charts and dialogs.
- BLE Layer is implemented in
hram/ble:- The app communicates with BLE devices that implement the standard Heart Rate Service.
BleDevicemodel describing discovered devices;identifierfield used for mac address (Android) or UUID (iOS).HrNotificationmodel for heart rate data (from the Heart Rate Measurement characteristic).BleNotificationencapsulatesHrNotification, battery level, and BLE connection status.- Core components:
BleConnectionManager: Manages Bluetooth state, device scanning, and the connection lifecycle ( connect/disconnect/reconnect).BleDataRepo: Provides streams for BLE characteristic data (heart rate measurement, battery level).HrDeviceRepo: A high-level repo that coordinates theBleConnectionManagerandBleDataRepoto provide a unified interface for interacting with HR devices.
What implemented:
- Scanning for and managing BLE heart rate devices.
- Connecting to devices and receiving heart rate notifications.
- Reconnecting on disconnection with retry logic.
- Parsing low-level BLE data.
BLE Connection and Data flow:
flowchart LR
A["SCANNING <br> ads"] -->|filter<br>HR Service| B[DEVICE<br>SELECTED]
B -->|identifier| C[CONNECT<br>Peripheral]
C --> D[SUBSCRIBE:<br>HRM 0x2A37,<br>Battery 0x2A19]
D --> E[PARSE<br>NOTIFICATIONS]
E --> F[UI<br>or<br>DATABASE]
BLE reconnection flow:
flowchart TD
A[Observe device connection state] --> |new state| B{State == Connected or Connecting?}
B -->|YES| A
B -->|No| D[Peripheral disconnect]
D --> E[Peripheral connect]
E --> F{Connected?}
F --> |YES| A
F --> |NO, attempts <= 3| E
F --> |NO, attempts >3| G[CONNECTION FAILED]
Tracking is implemented in hram/tracking:
ActivityTrackingManager- interface for activity tracking.- Start, pause, resume, stop tracking.
- Integration with BLE data streams and activity repository.
- Core stopwatch logic abstracted in
StopWatch.
What works:
- Heart rate session lifecycle (start/pause/stop etc.).
- Time tracking for each session.
- Combined use of BLE data and stopwatch inside tracking manager.
Implementation Details:
Background execution is nuanced per platform. TrackingController is the central entry point found in hram/tracking/TrackingController.kt. It provides a unified interface but has platform-specific implementations that leverage the shared ActivityTrackingManager and reactive state repositories:
-
Android:
- Uses a Foreground Service (
BleTrackingService) to keep the app alive. TrackingControllersends Intents to the service.- The Service delegates work to
ActivityTrackingManagerand observes shared state repositories to update Notifications (remote views).
- Uses a Foreground Service (
-
iOS:
- Relies on iOS background modes (CoreBluetooth).
TrackingControllerdelegates toActivityTrackingManager.TrackingControllerobserves shared state repositories and callLiveActivityManagerto push updates to Live Activities.
flowchart TB
%% Shared UI
subgraph Shared CMP ["Shared UI (CMP)"]
direction TB
UI[User Command]
subgraph VM[ViewModel]
TC_Shared[TrackingController]
end
VM --> TC_Shared
UI --> VM
end
%% Background Mode
subgraph BG[Background Mode Implementation]
direction LR
%% Android Platform
subgraph Android ["Android Platform"]
TC_And[TrackingController <br> Android]
Service[BleTrackingService]
Notif[Notification <br> Manager]
Service -- "Updates RemoteViews" --> Notif
end
%% Shared KMP
subgraph Shared ["Shared Logic (KMP)"]
VM[ViewModel]
TC_Shared[TrackingController]
ATM[ActivityTrackingManager]
Repos[(State Repo)]
ATM -- "Update State" --> Repos
end
%% iOS Platform
subgraph iOS ["iOS Platform"]
LAM[LiveActivityManager]
LA[Native Live Activities <br> Swift Code]
TC_iOS[TrackingController <br> iOS]
TC_iOS -- "Controls" --> LAM
LAM -- "Interop call" --> LA
end
end
%% Cross-layer connections
TC_Shared -- "Implementation" --> TC_And
TC_Shared -- "Implementation" --> TC_iOS
Service -- "Calls" --> ATM
TC_iOS -- "Calls" --> ATM
Service -. "Listen" .-> Repos
TC_iOS -. "Listen" .-> Repos
TC_And -. "Intents (Start/Stop)" .-> Service
Located under hram/data:
- Activity repositories:
HrActivityRepostores & retrieves HR activities.
- Database:
HramDatabaseinhram/data/db.- Heart rate and activity entities.
- Queries to read & write activity data.
- Optimized heart rate aggregation per activity: splits sessions into time buckets and calculates average heart rate directly in the database for fast, efficient queries.
What works:
- Persisting activity data (heart rate sessions) locally.
- Querying history via the repository layer.
The app persists transient state (like current BLE connection status or active tracking session info) to survive process death or navigation. Ble-Notifications state will be extracted to in-memory singleton repo, in the near future.
Packages:
com.achub.hram.data.repo.state: Repository interfaces and implementations for reactive state exposure.com.achub.hram.data.store: logic for serialization and storage (using DataStore).
Key Components:
- BleStateRepo: reflects the current state of BLE connection state(Disconnected, Connecting, Connected, etc.).
- TrackingStateRepo: Manages the state of the activity (Idle, Active, Paused).
- DataStore: Uses
BleStateSerializerandTrackingStateStageSerializerto save state to disk asynchronously.
Common UI code lives under hram/screen and hram/view:
- Screens:
screen/main:- Entry Compose screen(s) for main app navigation.
screen/activities:- Screens for listing activities and viewing details (history).
hram/view/chart- chart components for visualizing HR/metrics.
screen/record:- Screens for recording an activity (live HR, timer, etc.).
What works:
- Compose-based UI shared across platforms.
- Custom charts for heart rate data.
- Custom AGSL shaders for visual effects (Liquid Ripple for Bottom Bar, Liquid Wave for Heart animation).
- Custom components using new Material 3 Expressive
- Localization support for English and Ukrainian.
Activities Screen:
| iOS | Android |
|---|---|
![]() |
Record Screen:
| iOS | Android |
|---|---|
![]() |
To keep the user informed during a workout (even when the device is locked), the app uses platform-specific ongoing notifications:
-
Android: Custom Notifications with
RemoteViews.- Displays real-time heart rate.
- Includes a "breathing" animation synced to the heart rate.
-
iOS: Live Activities & Dynamic Island.
- Implemented using SwiftUI and WidgetKit.
- Shows heart rate, session duration, and battery level on the Lock Screen and Dynamic Island.
- Updates are pushed from the shared Kotlin code via
LiveActivityManager.
| iOS | Android |
|---|---|
![]() |
![]() |
| Dynamic Island on iOS |
|---|
![]() |
![]() |
![]() |
Dependency injection is implemented using Koin under hram/di:
Koin.kt- starting point for DI initialization.AppModule.kt- app-level bindings.ViewModelModule.kt- registrations for view models.TrackingModule.kt- bindings for tracking managerBleModule.kt,BleDataModule.ktin - BLE-specific bindings.DatabaseModule.kt,DataModule.ktin - database and repository bindings.UtilsModule.kt- utility bindings.DatabaseModule.ktandBleModule.ktprovide platform-specific implementations where needed.
| Category | Technology |
|---|---|
| Language | Kotlin (Multiplatform), Swift (iOS Shell) |
| UI | Compose Multiplatform |
| Architecture | MVVM, Repository Pattern |
| Dependency Injection | Koin Annotations |
| Permissions | moko-permissions |
| Persistence | Room (KMP) |
| BLE | Kable |
| Logging | Napier |
| Testing | kotlin.test, kotlinx-coroutines-test, Mokkery |
| Code Coverage | Kover |
- No external cloud sync/export.
- Limited error handling and UX for BLE edge cases.








