Hey folks. I'm starting a new project with Kable....
# juul-libraries
f
Hey folks. I'm starting a new project with Kable. And I'm doing some tests to better use the library. I thought about making a bluetooth repository called BluetoothService and this repository is a singleton as it will be used by several screens of the app. This repository will have its own scope and this scope will be linked to the lifecycle of the main activity of the application. I'm at the beginning and would like your opinion. This BluetoothService is in common code and will be used by iOS and Android. Code on the thread.
Copy code
object BluetoothServiceImpl : BluetoothService {

    private val scanner = Scanner()
    private var peripheral: Peripheral? = null
    private var _scope: CoroutineScope? = null
    private val requireScope: CoroutineScope =
        if (_scope == null) CoroutineScope(context = Dispatchers.Main) else _scope!!

    private val _state = MutableStateFlow<State>(State.Disconnected())
    override val state: StateFlow<State> = _state.asStateFlow()

    override fun getAdversiments(): Flow<ScanDeviceItem> {
        return scanner.advertisements.map {
            ScanDeviceItem(name = it.name, macAddress = macAddressFrom(it))
        }
    }

    override fun connectWith(macAddress: String) {
        requireScope.launch {
            peripheral = requirePeripheral(requireScope, macAddress)
            peripheral?.state?.onEach { _state.emit(it) }?.launchIn(requireScope)
            peripheral?.connect()
        }
    }

    override fun disconnect() {
        requireScope.launch {
            withTimeoutOrNull(5000) {
                peripheral?.disconnect()
            }
        }
    }

    override fun dispose() {
        _scope?.cancel()
        _scope = null
    }

}
t
Based on what you posted, it seems you only need to connect to a single peripheral at a time, correct? If that is the case, then a singleton seems like a reasonable/simple way to address that. If you only need to scan for the peripheral on a single screen, it might make more sense to remove the scanner from your BLE service class, and simply have the scanner property on the view model where you actually need it — but it is fine how you have it as well. I do notice that you are creating a
CoroutineScope
w/ its context set to
Main
, but I don't see anywhere that you need that context. Kable doesn't care what context any of its functions are called from, so using
Default
might be a better choice (i.e. no need to specify the context). To remove some of the nullability/simplify things, I'd just create the scope as a
val
, for example:
Copy code
private val scope = CoroutineScope(Job())
I personally prefer fail-fast approach, so I'd change your peripheral property to something like:
Copy code
private var _peripheral: Peripheral? = null
private val peripheral: Peripheral
    get() = _peripheral ?: error("Peripheral not ready")
Then you set peripheral via
_peripheral = ...
and everywhere else use it via
peripheral.*
. It eliminates all the null
?.
operators you have to use but you'll crash when you've made a programming mistake (and used it before it was ready). Overall, your approach is a good start and would likely need to evolve if you scale the solution to (for example) support more than one peripheral at a time, etc.
f
Hello Travis, thank you so much for your feedback. Yes, at first I will only connect to one peripheral at a time. But I can evolve if I need to connect to more than one. In this case, how do you show the user which peripheral he is working on? I agree with you regarding the Coroutine context, Default is more appropriate in this case. I thought about leaving the scope nullable because I can cancel it when I exit the application, that way I can force the end of a connection that might have gotten stuck.
do you think it's a good approach to use
fun dispose()
in this case?
t
Yes, at first I will only connect to one peripheral at a time. But I can evolve if I need to connect to more than one. In this case, how do you show the user which peripheral he is working on?
We created a class that essentially wraps a collection of
Peripheral
objects. The class acts like a collection but with very limited functionality. To ensure that we never forget to close connections, etc. It has a signature similar to:
Copy code
class Peripherals {

    val peripherals: StateFlow<Map<String, Peripheral>>

    fun get(id: String): Peripheral

    // Throws if Peripheral already exists under the specified `id`.
    fun putOrThrow(id: String, peripheral: Peripheral)

    suspend fun removeAndDispose(id: String)
}
So, when we initially connect to the
Peripheral
we add it to this collection. Once added, that
Peripheral
is available anywhere in our app via the
id
. When we're done with it we simply call
removeAndDispose
and we're assured it was completely cleaned up. We also update the exposed
StateFlow
, so any screens that would show all peripherals would also be updated to reflect the removal. Because we have control over the wrapper (that acts like a collection) then via the exposed
Flow
we can subscribe to it on a screen that has to show all available peripherals, and on screens that only need a specific one, we use the
get(id)
method. There are certainly other approaches to address this, but this has worked well for us.
I thought about leaving the scope nullable because I can cancel it when I exit the application, that way I can force the end of a connection that might have gotten stuck.
Not sure I follow what you're trying to do. When the application exists, all BLE connections should be destroyed by the Android OS anyways.
do you think it's a good approach to use 
fun dispose()
 in this case?
Seems reasonable to me for the use case you described.
f
Thank you so much, your reply opened my mind.
😄 1
Travis, one more curiosity. Sorry if I'm bothering you.
We created a class that essentially wraps a collection of 
Peripheral
 objects
This class will wrap all connected peripherals, correct? If the peripherals will be used by multiple screens, which must have their own viewmodel, with what scope do you create the peripherals?
t
Not bothering me at all. We created a dedicated scope. Since it doesn't have a parent, it will have the lifecycle of the app, since we don't otherwise cancel it. If we wanted to dispose of all peripherals, then we could expose the cancellation of the dedicated scope.
f
hello travis, after a long time. I am here again. i have some questions about your approach, can you help me, please? The id of your collection is the macaddress of peripheral? (hypothetically) I have three screens A, B and C. The screen A just scans and connects to peripheral and the others two make read/write operations. The screen B get a peripheral via
fun get(id: String): Peripheral
you call peripheral.connect again or not? do the same for screen C??
t
We have a dedicated identifier (separate from MAC address). As MAC address is not available on JS or Apple platforms. Our dedicated identifier is in the advertisement data for our peripherals, making it easy to identify a unique peripheral even before connecting to it. If you don’t have that available, you could store a mapping of your own ID to the platform specific identifier (e.g. MAC address on Android). Your peripherals should be stored in a collection that outlives your screens. One possible approach is a singleton that holds a
Map<Identifier, Peripheral>
(where
Identifier
is a type of your choosing, could be
Uuid
). You could add to that collection on screen A, connect to it, then on screen B you retrieve the
Peripheral
via identifier. You shouldn’t need to call
connect
again because the peripheral should remain connected. Screen C, you just retrieve from the singleton again, like you did on screen B.
f
I like about dedicated identifier, that looks great. but for now I am going to use platform specific identifier. Yesterday I implemented the singleton approach, testing for a while. Tks a lot Travis