250-person R&D team
Tens of thousands of customers in the US
Processing billions in payments every year
July 2013: First live customer
April 2019: $250 million in Series E funding at $2.7 billion valuation
< 10% US POS market share so far
Lots more growth to come!
2018: Revenue up 148%
Send commands to the card reader
All void methods
Asynchronously returns all information and errors from controller commands
Implemented by us
interface Controller {
fun getDeviceInfo()
}
interface Listener {
fun onReturnDeviceInfo(deviceInfo: DeviceInfo)
fun onError(error: Exception)
}class MyListener : Listener {
var deviceInfo: DeviceInfo? = null
var error: Error? = null
override fun onReturnDeviceInfo(deviceInfo: DeviceInfo) {
this.deviceInfo = deviceInfo
}
override fun onError(error: Error) {
this.error = error
}
}fun getDeviceInfo(): DeviceInfo {
controller.getDeviceInfo() // returns nothing
while (listener.deviceInfo == null
&& listener.error == null) {
//could be an infinite loop
Thread.sleep(100) //blocks the thread
}
if (listener.error != null) {
val e = listener.error
listener.error = null //null the value for next time
throw e
}
val returnValue = listener.deviceInfo
listener.deviceInfo = null // null the value for next time
return returnValue
}interface Controller {
suspend fun getDeviceInfo(): DeviceInfo
}I wanted to get to a nice clean API where:
interface ControllerExtension {
suspend fun getDeviceInfo(): DeviceInfo
}interface ControllerExtension {
suspend fun getDeviceInfo(): DeviceInfo
}Requires asynchrony and inter-thread communication
class ListenerExtension : Listener {
val errorChannel = Channel<Error>(UNLIMITED)
val deviceInfoChannel = Channel<DeviceInfo>(UNLIMITED)
override fun onReturnDeviceInfo(deviceInfo: DeviceInfo) {
runBlocking { deviceInfoChannel.send(deviceInfo) }
}
override fun onError(error: Error) {
runBlocking { errorChannel.send(error) }
}
}class ControllerExtension(
private val controller: OriginalController,
private val listener: ListenerExtension
) {
override suspend fun getDeviceInfo(): DeviceInfo {
return triggerEventAndGetResult(
eventTrigger = { controller.getDeviceInfo() },
eventReceiver = controllerListener.deviceInfoChannel,
errorReceiver = controllerListener.errorChannel
)
}
}
suspend fun <T : Any> triggerEventAndGetResult(
eventTrigger: () -> Unit,
eventReceiver: ReceiveChannel<T>,
errorReceiver: ReceiveChannel<ControllerException>
): T = coroutineScope {
val deferredEvent = async<T> {
select {
errorReceiver.onReceive { error ->
throw error
}
eventReceiver.onReceive { event ->
event
}
}
}
eventTrigger()
return@coroutineScope deferredEvent.await()
}Makes it possible to listen to multiple suspend functions simultaneously and select the first result that becomes available.
Key Lesson 1
private val parentJob = Job()
private val coroutineScope = CoroutineScope(parentJob + Dispatchers.Default)
fun launchProcessing() {
try {
coroutineScope.launch {
throw IllegalStateException()
}
} catch (e: IllegalStateException) {
println("This will never be printed")
}
}private val parentJob = Job()
private val coroutineScope = CoroutineScope(parentJob + Dispatchers.Default)
suspend fun calculateNumber(): Int {
val deferredValue = coroutineScope.async<Int> {
throw IllegalStateException()
}
val value = try {
deferredValue.await()
} catch (e: IllegalStateException) {
println("Unable to get value, defaulting to 0")
0
}
val deferred1 = coroutineScope.async { 2 }
val deferred2 = coroutineScope.async { 2 }
return value + deferred1.await() + deferred2.await()
}private val parentJob = SupervisorJob()
private val coroutineScope = CoroutineScope(parentJob + Dispatchers.Default)
suspend fun calculateNumber(): Int {
val deferredValue = coroutineScope.async<Int> {
throw IllegalStateException()
}
val value = try {
deferredValue.await()
} catch (e: IllegalStateException) {
println("Unable to get value, defaulting to 0")
0
}
val deferred1 = coroutineScope.async { 2 }
val deferred2 = coroutineScope.async { 2 }
return value + deferred1.await() + deferred2.await()
}suspend fun calculateNumberInheritScope(): Int {
val value = try {
coroutineScope {
val deferredValue = async<Int> {
throw IllegalStateException()
}
deferredValue.await()
}
} catch (e: Exception) {
println("Unable to get value, defaulting to 0")
0
}
return coroutineScope {
val deferred1 = async { 2 }
val deferred2 = async { 2 }
value + deferred1.await() + deferred2.await()
}
}Use SupervisorJob instead of Job
Key Lesson 2
test {
systemProperty 'kotlinx.coroutines.debug', 'on'
//or
jvmArgs '-ea'
}System.setProperty("kotlinx.coroutines.debug",
BuildConfig.DEBUG ? "on" : "off");dependencies {
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.2.1'
}fun main() = runBlocking {
DebugProbes.install()
val deferred = async { computeValue() }
// Delay for some time
delay(1000)
// Dump running coroutines
DebugProbes.dumpCoroutines()
println("Printing single job")
DebugProbes.printJob(deferred)
}Coroutines dump 2018/11/12 21:44:02
Coroutine "coroutine#2":DeferredCoroutine{Active}@289d1c02, state: SUSPENDED
at kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:99)
at ExampleKt.combineResults(Example.kt:11)
at ExampleKt$computeValue$2.invokeSuspend(Example.kt:7)
at ExampleKt$main$1$deferred$1.invokeSuspend(Example.kt:25)
(Coroutine creation stacktrace)
at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:25)
at kotlinx.coroutines.BuildersKt.async$default(Unknown Source)
at ExampleKt$main$1.invokeSuspend(Example.kt:25)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at ExampleKt.main(Example.kt:23)
at ExampleKt.main(Example.kt)
... More coroutines here ...
"coroutine#2":DeferredCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:99)
"coroutine#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line ExampleKt.computeOne(Example.kt:14)
"coroutine#4":DeferredCoroutine{Active}, continuation is SUSPENDED at line ExampleKt.computeTwo(Example.kt:19) @Rule
@JvmField
public val timeout = CoroutinesTimeout.seconds(1)
-javaagent:kotlinx-coroutines-debug-1.2.1.jarKey Lesson 3
@Test
fun `getDeviceInfo returns the expected device info`() = runBlocking {
assertFalse(controller.getDeviceInfo()).isEqualTo(expectedDeviceInfo)
}
suspend fun getDeviceInfo(): DeviceInfo{
...
}fun updateData() {
val newData = getData()
postData(newData)
updateUI(newData)
}
suspend fun getData() {}
suspend fun postData(newData: Data) {}
suspend fun updateUI(newData: Data) {}fun updateData() {
coroutineScope.launch(Dispatchers.IO) {
val newData = getData()
postData(newData)
launch(Dispatchers.Main) { updateUI(newData) }
}
}
suspend fun getData() {}
suspend fun postData(newData: Data) {}
suspend fun updateUI(newData: Data) {}suspend fun updateData() {
val newData = getData()
postData(newData)
updateUI(newData)
}
suspend fun getData() {}
suspend fun postData(newData: Data) {}
suspend fun updateUI(newData: Data) {}Control the context & asynchronony
Know when its finished
suspend fun updateData() {
val newData = getData()
postData(newData)
updateUI(newData)
}
suspend fun getData() {}
suspend fun postData(newData: Data) {
withContext(Dispatchers.IO){
}
}
suspend fun updateUI(newData: Data) {
withContext(Dispatchers.Main){
}
}private val parentJob = Job()
private val coroutineScope = CoroutineScope(parentJob + Dispatchers.Default)
fun launchProcessing() {
coroutineScope.launch {
//some processing
}
coroutineScope.launch {
//some other processing
}
}fun CoroutineScope.launchProcessing() {
this.launch {
//some processing
}
this.launch {
//some other processing
}
}Make it a suspend function
suspend fun launchProcessing() {
coroutineScope {
launch {
//some processing
}
launch {
//some other processing
}
}
}Key Lesson 4
sealed class ControllerResult<R> {
data class Success<R>(val result: R) : ControllerResult<R>()
data class Failure<R>(val error: Error) : ControllerResult<R>()
}sealed class DeviceInfoResult {
data class Success(val deviceInfo: DeviceInfo) : DeviceInfoResult()
data class Failure(val error: Error) : DeviceInfoResult()
data class Timeout(val error: Error) : DeviceInfoResult()
}sealed class StartUsbResult {
object AlreadyConnected : StartUsbResult()
object Success : StartUsbResult()
data class Failure(val error: Error) : StartUsbResult()
data class Timeout(val error: Error) : StartUsbResult()
}KEEP-127 Encapsulate successful or failed function execution
Sealed Classes Instead of Exceptions in Kotlin - Philipp Hauer