In the first part, we started discovering how powerful Kotlin Multiplatform is applied into the mobile app world. We explored the idea behind it, we discovered the concepts of expected
and actual
together with some problems that may occur, and we took as an example a basic Bluetooth Low Energy (BLE) devices discovery library.
But we want more.
After the implementation of the common discovery module and the two basic applications (Android, iOS), I decided to improve the Kotlin module, allowing the platform-specific implementations to perform simple operations according to the BLE protocol. I wanted the BluetoothAdapter
class to be able to connect to a BLE device, discover its Services, and for each service its Characteristics. The goal was to implement an integration with the Xiaomi Mi Band 2, a well-known smart bracelet and fitness tracker, which has been largely hacked by some passionate users that developed third-party apps on it (you can find some interesting projects on Github). The Band has a single button on its hardware, which allows to display one by one the fitness data it acquired. My intention was just to listen to the button touch events, and deliver a message to the user in the app every time the band’s button gets tapped.
Pretty simple idea, but still challenging.
A bit of context about the BLE protocol
Before starting, you need to know a bit about the BLE protocol that we’re going to need for this Kotlin project. In a nutshell, most of the devices (commonly called peripherals) conform to the Generic Attribute profile (GATT), which is a generic specification for sending data between devices using Bluetooth. A peripheral can be a Client that sends GATT commands (i.e. smartphone), a Server which receives such commands and returns responses, or both, in particular cases. The communication between the two entities is made by operating on the server’s characteristics. On each of them, many of operations can be performed, like read, write and notify. In particular, the notify operation allows the client to receive server-initiated updates, ideally like push notifications. That was the case of my button press events.
Each peripheral exposes various services, which are scoped groups of characteristics. UUIDs are used for uniquely identifying the mentioned entities.
A cool reference can be found here, if you want to know more.
Let’s start with this Kotlin BLE project!
For communicating with a peripheral, at first you need to know what kind of services and characteristics it exposes. I used a well-known application called LightBlue for this. It’s pretty simple to use and you can examine all the peripheral info. By looking also on the Github projects I mentioned before (in particular here), I discovered the Mi Band’s service and characteristic which provides notifications when the button gets tapped.
// BLE devices share a common UUID for identifying services and characteristics. Only 4 digits in the first UUID segment are different. private const val SERVICE_BUTTON_PRESSED = "FEE0" private const val CHAR_BUTTON_PRESSED = "0010"
I started by defining a common interface for connecting to a peripheral and querying its services and characteristics. Using the expect
/actual
paradigm, the final definition looked like this:
expect class BluetoothAdapter { var listener: BluetoothAdapterListener? fun discoverDevices(callback: (BluetoothDevice) -> Unit) fun stopScan() fun findBondedDevices(callback: (List<BluetoothDevice>) -> Unit) fun connect(device: BluetoothDevice) fun disconnect() fun discoverServices() fun discoverCharacteristics(service: BleService) fun setNotificationEnabled(char: BleCharacteristic) fun setNotificationDisabled(char: BleCharacteristic) }
As you can see in the methods signatures, I made some minimal abstractions on the platform-specific implementations of the peripherals, services and characteristics, to let the common codebase work independently on them.
expect class BluetoothDevice { val id: String val name: String } data class BleService( val id: String, val device: BluetoothDevice ) data class Ble Characteristic( val id: String, val value: ByteArray?, val service: BleService )
Notice that I marked BluetoothDevice
as an expectation. This is due to the fact that I needed to keep together the Android BluetoothDevice
and the iOS CBPeripheral
with the actualization of the BluetoothDevice
.
Kotlin BLE – Android module
actual data class BluetoothDevice( actual val id: String, actual val name: String, internal val androidDevice: BluetoothDevice )
Kotlin BLE – iOS module
actual data class BluetoothDevice( actual val id: String, actual val name: String, internal val peripheral: CBPeripheral )
For gaining access to services and characteristics, you need usually to follow some steps, which may vary between Android and iOS. First, you need to connect to the device (good point, man!). Once connected, start the services discovery and then discover the characteristics you need for each service.
For easily describing in which state the process was, I defined a sealed class
in the shared module, which at the end looked like the below:
sealed class BleState { data class Connected(val device: BluetoothDevice): BleState() data class Disconnected(val device: BluetoothDevice): BleState() data class ServicesDiscovered(val device: BluetoothDevice, val services: List<BleService>): BleState() data class CharacteristicsDiscovered(val device: BluetoothDevice, val chars: List<BleCharacteristic>): BleState() data class CharacteristicChanged(val device: BluetoothDevice, val characteristic: BleCharacteristic): BleState() }
The state instance is emitted by the BluetoothAdapter
calling a BluetoothAdapterListener
interface method. I choose this paradigm because all the steps were asynchronous (as you can imagine).
interface BluetoothAdapterListener { fun onStateChange(state: BleState) }
Then, I implemented my BluetoothAdapter
for the two platforms. The actual
implementations were pretty straight-forward. Also, I added some shared UI logic applying MVP, but I’ll cover this topic in the next post, I promise you.
As I mentioned before, the steps of discovering services and characteristics are different between the two platforms. On iOS you need to manually start the discovery of characteristics for the service you are interested to, and for accomplishing this need, I exposed a method in my BluetoothAdapter
class. On the contrary, on the Android platform, when you discover a service you can access to all his characteristics for free.
But why?
Probably, the Android engineers thought that once discovered a service, a user wants to play at once with its characteristics. And probably, the Apple engineers restricted the immediate access for security reasons. Perhaps for the same reasons why on Android you get the peripheral MAC address for free, and on iOS you need to explicitly request it instead. But this is another story.
Also, be aware of threading. On the Android platform, each callback method of the BluetoothGattCallback
(called when a service is discovered, when a device is connected etc.) is called from a background thread. This can cause issues if you execute some UI related code in your listener implementations.
Well done! Your Kotlin BLE app is on the way
Cool, I implemented using Kotlin multiplatform class which allows BLE communication!
When I first run it on Android I was excited…
But!
But my enthusiasm was brought down early.
The characteristic discovery worked great, and so did the notification enabling. But guys, I was not able to receive the button taps notifications.
It was so ironic and so weird that I fully implemented a multiplatform application, and the only failure point was the platform related code! Luckily, was not my fault. At least, not at all.
For connecting to the Mi Band 2, your device needs to the be authorized by the Mi Fit application, and you need to perform the GATT connection by choosing the Band from the list of the already bonded peripherals. Probably, when connected to it, its app writes to a characteristic some info that let the Band to keep alive the connection. A fact I noticed is that after short time the Mi Band dropped the connection to a device without the Mi Fit app installed and configured.
Once I installed my multiplatform app on my personal phone where I have my Band’s data, all worked great.
After compiling against iOS, I found another problem with the iOS framework that was generated and embedded into Xcode. At first, the BleState.CharacteristicChanged
class of my Kotlin codebase had the property characteristic
named char
, for simplicity.
I think you can figure out the issue ?
Solution of problem
When compiling for the iOS target, all your code is visible from your Swift project by using Objective-C header files. And we all know that char
is the name of a type in Obj-C. The issue was that the generated header file contained a syntax error in the class declaration, as you can see in the below screenshot.
The syntax error into the compiled Obj-C header.
Once I refactored it, the error obviously disappeared.
So, the moral of the story is: always use meaningful names.
Ok sorry not really the case.
And so the moral of the story is: always use names which will not clash with reserved keywords of your target platform.
More multiplatform-ish.
End of the Klotin BLE project
I would like to take advantage of the problems I faced for underlining that Kotlin MPP did not give me any strange problem or unexpected error by itself, since all the multiplatform stuff worked as expected. The problems I found were due to a third-party limitation (in the case of the Mi Band) and to a misuse of the API (the iOS syntax error).
I’m very fascinated by this powerful technology, and I think I’ll start advocating for it’s adoption in some incoming projects, where it will be opportune.
Conclusion
But the trip didn’t reach the end! We explored a MPP using the Bluetooth, which is not a common use case for our beloved mobile applications. In the next part of this series, we will discover how to fetch some data from the network and deliver them to the user adopting a MVP pattern… all flavored with some hot Kotlin Coroutines spices.
Stay tuned, thanks for reading!
You can find all the code behind this project in this Github repository: https://github.com/MOLO17/kotlin-mpp-poc