Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Expressive (a)synchronous code using coroutines

Expressive (a)synchronous code using coroutines

As everyone knows, long-running operations in Android apps should never be executed on the main thread. With the goal of protecting developers from making mistakes, the Android ecosystem relies heavily on asynchronous APIs that can be found all over the framework and also in many third-party libraries. Although this might do the trick for simple apps and workflows, this becomes a problem as complexity grows in the codebase. In this talk, we'll explore how to leverage coroutines to convert asynchronous APIs into synchronous ones for the sake of expressiveness, readability and developer sanity.

Avatar for Flávio Faria

Flávio Faria

February 27, 2020
Tweet

Other Decks in Programming

Transcript

  1. fun makeAsyncCall() { val asyncApi = AsyncApi() asyncApi.doSomethingAsync { result

    -> runOnUiThread { // do something useful with result } } }
  2. • Getting the user location • Playing media • Managing

    Bluetooth devices • Third-party service libraries
  3. GATT Server (device) Service 1 Characteristic 1 Characteristic 2 Service

    2 Characteristic 1 Characteristic 2 Characteristic 3
  4. 1. Scan for BLE devices 2. Connect to a device

    3. Discover services 4. Retrieve characteristic value
  5. private val adapter = BluetoothAdapter.getDefaultAdapter() fun discoverDevice() { adapter.bluetoothLeScanner.startScan(scanCallback) }

    private val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { } }
  6. private val adapter = BluetoothAdapter.getDefaultAdapter() fun discoverDevice() { adapter.bluetoothLeScanner.startScan(scanCallback) }

    private val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { if (result.device.name == "My Battery-Powered BLE device") { adapter.bluetoothLeScanner.stopScan(this) val gatt = result.device.connectGatt(context, false, gattCallback) } } }
  7. private val adapter = BluetoothAdapter.getDefaultAdapter() fun discoverDevice() { adapter.bluetoothLeScanner.startScan(scanCallback) }

    private val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { if (result.device.name == "My Battery-Powered BLE device") { adapter.bluetoothLeScanner.stopScan(this) val gatt = result.device.connectGatt(context, false, gattCallback) } } }
  8. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { } }
  9. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) { gatt.discoverServices() } } }
  10. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) { gatt.discoverServices() } } }
  11. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) { gatt.discoverServices() } } }
  12. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {…} }
  13. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { } }
  14. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { gatt.services .find { it.uuid == BATTERY_SERVICE_UUID } ?.characteristics ?.find { it.uuid == BATTERY_CHAR_UUID } ?.let { characteristic -> gatt.readCharacteristic(characteristic) } } } }
  15. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { gatt.services .find { it.uuid == BATTERY_SERVICE_UUID } ?.characteristics ?.find { it.uuid == BATTERY_CHAR_UUID } ?.let { characteristic -> gatt.readCharacteristic(characteristic) } } } }
  16. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { gatt.services .find { it.uuid == BATTERY_SERVICE_UUID } ?.characteristics ?.find { it.uuid == BATTERY_CHAR_UUID } ?.let { characteristic -> gatt.readCharacteristic(characteristic) } } } }
  17. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {…} }
  18. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {…} override fun onCharacteristicRead( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int ) { } }
  19. private val gattCallback = object : BluetoothGattCallback() { override fun

    onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {…} override fun onCharacteristicRead( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int ) { if (status == BluetoothGatt.GATT_SUCCESS) { val batteryLevel = BigInteger(characteristic.value) Log.d("BLE", "Battery level: $batteryLevel”) } } }
  20. • What thread are these requests running on? • What

    thread are these callbacks running on?
  21. • What thread are these requests running on? • What

    thread are these callbacks running on? • Is all this thread switching really needed?
  22. • What thread are these requests running on? • What

    thread are these callbacks running on? • Is all this thread switching really needed? • What if we're already on a background thread?
  23. • Make assumptions about the client's execution requirements • Less

    expressive / complicated statement flow (goto)
  24. • Make assumptions about the client's execution requirements • Less

    expressive / complicated statement flow (goto) • Couple business logic with execution logic
  25. • Make assumptions about the client's execution requirements • Less

    expressive / complicated statement flow (goto) • Couple business logic with execution logic • Harder to mock
  26. • Make assumptions about the client's execution requirements • Less

    expressive / complicated statement flow (goto) • Couple business logic with execution logic • Harder to mock • Harder to debug
  27. • Make assumptions about the client's execution requirements • Less

    expressive / complicated statement flow (goto) • Couple business logic with execution logic • Harder to mock • Harder to debug • Harder to handle errors
  28. • Make assumptions about the client's execution requirements • Less

    expressive / complicated statement flow (goto) • Couple business logic with execution logic • Harder to mock • Harder to debug • Harder to handle errors • Asynchronism is overly fine-grained
  29. class SuspendableBluetoothLeScanner(private val context: Context) { suspend fun scan() =

    callbackFlow { val scanCallback = object : ScanCallback() { } } }
  30. class SuspendableBluetoothLeScanner(private val context: Context) { suspend fun scan() =

    callbackFlow { val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { } } } }
  31. class SuspendableBluetoothLeScanner(private val context: Context) { suspend fun scan() =

    callbackFlow { val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { if (!isClosedForSend) { offer(SuspendableBluetoothDevice(context, result.device)) } } } } }
  32. class SuspendableBluetoothLeScanner(private val context: Context) { suspend fun scan() =

    callbackFlow { val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { if (!isClosedForSend) { offer(SuspendableBluetoothDevice(context, result.device)) } } } with(BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner) { startScan(scanCallback) } } }
  33. class SuspendableBluetoothLeScanner(private val context: Context) { suspend fun scan() =

    callbackFlow { val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { if (!isClosedForSend) { offer(SuspendableBluetoothDevice(context, result.device)) } } } with(BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner) { startScan(scanCallback) awaitClose { stopScan(scanCallback) } } } }
  34. sealed class GattEvent { class OnConnectionStateChange(val newState: Int) : GattEvent()

    class OnServicesDiscovered(val services: List<BluetoothGattService>) : GattEvent() class OnCharacteristicRead(val characteristic: BluetoothGattCharacteristic) : GattEvent() object Error : GattEvent() }
  35. class CoroutineGattCallback : BluetoothGattCallback() { private val _eventsChannel = BroadcastChannel<GattEvent>(Channel.BUFFERED)

    val eventsChannel: ReceiveChannel<GattEvent> get() = _eventsChannel.openSubscription() }
  36. class CoroutineGattCallback : BluetoothGattCallback() { private val _eventsChannel = BroadcastChannel<GattEvent>(Channel.BUFFERED)

    val eventsChannel: ReceiveChannel<GattEvent> get() = _eventsChannel.openSubscription() override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { } }
  37. class CoroutineGattCallback : BluetoothGattCallback() { private val _eventsChannel = BroadcastChannel<GattEvent>(Channel.BUFFERED)

    val eventsChannel: ReceiveChannel<GattEvent> get() = _eventsChannel.openSubscription() override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { _eventsChannel.offer(GattEvent.OnConnectionStateChange(newState)) } else { _eventsChannel.offer(GattEvent.Error) } } }
  38. class CoroutineGattCallback : BluetoothGattCallback() { private val _eventsChannel = BroadcastChannel<GattEvent>(Channel.BUFFERED)

    val eventsChannel: ReceiveChannel<GattEvent> get() = _eventsChannel.openSubscription() override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { _eventsChannel.offer(GattEvent.OnConnectionStateChange(newState)) } else { _eventsChannel.offer(GattEvent.Error) } } }
  39. class CoroutineGattCallback : BluetoothGattCallback() { private val _eventsChannel = BroadcastChannel<GattEvent>(Channel.BUFFERED)

    val eventsChannel: ReceiveChannel<GattEvent> get() = _eventsChannel.openSubscription() override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {…} }
  40. class CoroutineGattCallback : BluetoothGattCallback() { private val _eventsChannel = BroadcastChannel<GattEvent>(Channel.BUFFERED)

    val eventsChannel: ReceiveChannel<GattEvent> get() = _eventsChannel.openSubscription() override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {…} }
  41. class CoroutineGattCallback : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status:

    Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { } }
  42. class CoroutineGattCallback : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status:

    Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS){ _eventsChannel.offer(GattEvent.OnServicesDiscovered(gatt.services)) } else { _eventsChannel.offer(GattEvent.Error) } } }
  43. class CoroutineGattCallback : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status:

    Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS){ _eventsChannel.offer(GattEvent.OnServicesDiscovered(gatt.services)) } else { _eventsChannel.offer(GattEvent.Error) } } }
  44. class CoroutineGattCallback : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status:

    Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {…} }
  45. class CoroutineGattCallback : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status:

    Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {…} override fun onCharacteristicRead( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int ) { } }
  46. class CoroutineGattCallback : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status:

    Int, newState: Int) {…} override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {…} override fun onCharacteristicRead( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int ) { if (status == BluetoothGatt.GATT_SUCCESS) { _eventsChannel.offer(GattEvent.OnCharacteristicRead(characteristic)) } else { _eventsChannel.offer(GattEvent.Error) } } }
  47. class SuspendableBluetoothDevice( private val context: Context, private val bluetoothDevice: BluetoothDevice

    ) { private var gatt: BluetoothGatt? = null private val callback = CoroutineGattCallback() }
  48. class SuspendableBluetoothDevice( private val context: Context, private val bluetoothDevice: BluetoothDevice

    ) { private var gatt: BluetoothGatt? = null private val callback = CoroutineGattCallback() suspend fun connect(): Boolean { } }
  49. suspend fun connect(): Boolean { with(callback.eventsChannel.consumeAsFlow()) { gatt = bluetoothDevice.connectGatt(context,

    false, callback) return map { when (it) { is GattEvent.OnConnectionStateChange -> it.newState == BluetoothGatt.STATE_CONNECTED is GattEvent.Error -> false else -> null } } } }
  50. suspend fun connect(): Boolean { with(callback.eventsChannel.consumeAsFlow()) { gatt = bluetoothDevice.connectGatt(context,

    false, callback) return map { when (it) { is GattEvent.OnConnectionStateChange -> it.newState == BluetoothGatt.STATE_CONNECTED is GattEvent.Error -> false else -> null } }.filterNotNull() } }
  51. suspend fun connect(): Boolean { with(callback.eventsChannel.consumeAsFlow()) { gatt = bluetoothDevice.connectGatt(context,

    false, callback) return map { when (it) { is GattEvent.OnConnectionStateChange -> it.newState == BluetoothGatt.STATE_CONNECTED is GattEvent.Error -> false else -> null } }.filterNotNull().first() } }
  52. suspend fun connect(): Boolean { with(callback.eventsChannel.consumeAsFlow()) { gatt = bluetoothDevice.connectGatt(context,

    false, callback) return map { when (it) { is GattEvent.OnConnectionStateChange -> it.newState == BluetoothGatt.STATE_CONNECTED is GattEvent.Error -> false else -> null } }.filterNotNull().first() } }
  53. class SuspendableBluetoothDevice( private val context: Context, private val bluetoothDevice: BluetoothDevice

    ) { private var gatt: BluetoothGatt? = null private val callback = CoroutineGattCallback() suspend fun connect(): Boolean {…} }
  54. class SuspendableBluetoothDevice( private val context: Context, private val bluetoothDevice: BluetoothDevice

    ) { private var gatt: BluetoothGatt? = null private val callback = CoroutineGattCallback() suspend fun connect(): Boolean {…} suspend fun discoverServices() = gatt?.let { } ?: emptyList() }
  55. class SuspendableBluetoothDevice( private val context: Context, private val bluetoothDevice: BluetoothDevice

    ) { private var gatt: BluetoothGatt? = null private val callback = CoroutineGattCallback() suspend fun connect(): Boolean {…} suspend fun discoverServices() = gatt?.let { with(callback.eventsChannel.consumeAsFlow()) { it.discoverServices() filterIsInstance<GattEvent.OnServicesDiscovered>().first().services } } ?: emptyList() }
  56. class SuspendableBluetoothDevice( private val context: Context, private val bluetoothDevice: BluetoothDevice

    ) { private var gatt: BluetoothGatt? = null private val callback = CoroutineGattCallback() suspend fun connect(): Boolean {…} suspend fun discoverServices() = gatt?.let { with(callback.eventsChannel.consumeAsFlow()) { it.discoverServices() filterIsInstance<GattEvent.OnServicesDiscovered>().first().services } } ?: emptyList() }
  57. class SuspendableBluetoothDevice( private val context: Context, private val bluetoothDevice: BluetoothDevice

    ) { private var gatt: BluetoothGatt? = null private val callback = CoroutineGattCallback() suspend fun connect(): Boolean {…} suspend fun discoverServices() = gatt?.let {…} ?: emptyList() }
  58. class SuspendableBluetoothDevice( private val context: Context, private val bluetoothDevice: BluetoothDevice

    ) { private var gatt: BluetoothGatt? = null private val callback = CoroutineGattCallback() suspend fun connect(): Boolean {…} suspend fun discoverServices() = gatt?.let {…} ?: emptyList() suspend fun readCharacteristic(characteristic: BluetoothGattCharacteristic) = gatt?.let { } }
  59. class SuspendableBluetoothDevice( private val context: Context, private val bluetoothDevice: BluetoothDevice

    ) { private var gatt: BluetoothGatt? = null private val callback = CoroutineGattCallback() suspend fun connect(): Boolean {…} suspend fun discoverServices() = gatt?.let {…} ?: emptyList() suspend fun readCharacteristic(characteristic: BluetoothGattCharacteristic) = gatt?.let {…} }
  60. val bluetoothLeScanner = SuspendableBluetoothLeScanner(context) val bluetoothDevice = bluetoothLeScanner.scan().first() with(bluetoothDevice) {

    if (connect()) { val services = discoverServices() val batteryService = services.firstOrNull { it.uuid == BATTERY_SERVICE_UUID } } }
  61. val bluetoothLeScanner = SuspendableBluetoothLeScanner(context) val bluetoothDevice = bluetoothLeScanner.scan().first() with(bluetoothDevice) {

    if (connect()) { val services = discoverServices() val batteryService = services.firstOrNull { it.uuid == BATTERY_SERVICE_UUID } batteryService?.characteristics?.firstOrNull { it.uuid == BATTERY_CHAR_UUID } } } }
  62. val bluetoothLeScanner = SuspendableBluetoothLeScanner(context) val bluetoothDevice = bluetoothLeScanner.scan().first() with(bluetoothDevice) {

    if (connect()) { val services = discoverServices() val batteryService = services.firstOrNull { it.uuid == BATTERY_SERVICE_UUID } batteryService?.characteristics?.firstOrNull { it.uuid == BATTERY_CHAR_UUID }?.let { val batteryLevel = readCharacteristic(it)?.value Log.d("BLE", "Battery level: ${BigInteger(batteryLevel)}”) } } }
  63. val bluetoothLeScanner = SuspendableBluetoothLeScanner(context) val bluetoothDevice = bluetoothLeScanner.scan().first() with(bluetoothDevice) {

    if (connect()) { val services = discoverServices() val batteryService = services.firstOrNull { it.uuid == BATTERY_SERVICE_UUID } batteryService?.characteristics?.firstOrNull { it.uuid == BATTERY_CHAR_UUID }?.let { val batteryLevel = readCharacteristic(it)?.value Log.d("BLE", "Battery level: ${BigInteger(batteryLevel)}”) } } else { Log.d("BLE", "Error connecting to device") } }
  64. val bluetoothLeScanner = SuspendableBluetoothLeScanner(context) val bluetoothDevice = bluetoothLeScanner.scan().first() with(bluetoothDevice) {

    if (connect()) { val services = discoverServices() val batteryService = services.firstOrNull { it.uuid == BATTERY_SERVICE_UUID } batteryService?.characteristics?.firstOrNull { it.uuid == BATTERY_CHAR_UUID }?.let { val batteryLevel = readCharacteristic(it)?.value Log.d("BLE", "Battery level: ${BigInteger(batteryLevel)}”) } } else { Log.d("BLE", "Error connecting to device") } }
  65. • Async APIs sometimes can be confusing and hard to

    follow • Avoid making assumptions about execution requirements
  66. • Async APIs sometimes can be confusing and hard to

    follow • Avoid making assumptions about execution requirements • Code should be linear like a recipe for better expressiveness
  67. • Async APIs sometimes can be confusing and hard to

    follow • Avoid making assumptions about execution requirements • Code should be linear like a recipe for better expressiveness • Coroutines can help clean up callback-heavy APIs