Nowadays it's virtually impossible for an app to not interact with some form of REST API. Most developers will be familiar with libraries such as Retrofit which greatly reduce the code we need to write in order to perform said API requests. However, when it comes to real-time data, nothing beats WebSockets. Despite all the power they provide, it can be a hassle to consume. Wouldn't it be great if we had some Retrofit-like library for WebSockets? The folks at Tinder thought so.
Scarlet is a Retrofit inspired WebSockets client that manages the client-server communication for you. Scarlet uses a State Machine to handle the WebSocket connection nicely.
Getting started
To demonstrate how Scarlet works, we'll integrate with the Coinbase WebSockets API to get the real-time prices of a few cryptocurrencies. Since these prices have a very high update frequency, this is a good use case for WebSockets.
Scarlet has a plugin-based architecture, allowing you to decide what libraries to use for certain tasks. In this example we'll go with OkHttp for the underlying connection, Moshi for decoding messages into Kotlin objects and Coroutines to react to WebSocket events.
def scarlet_version = '0.1.12'
implementation "com.tinder.scarlet:scarlet:$scarlet_version"
// Optional dependencies, based on your setup.
implementation "com.tinder.scarlet:lifecycle-android:$scarlet_version"
implementation "com.tinder.scarlet:websocket-okhttp:$scarlet_version"
implementation "com.tinder.scarlet:message-adapter-moshi:$scarlet_version"
implementation "com.tinder.scarlet:stream-adapter-coroutines:$scarlet_version"
Declaring the API contract
Just like Retrofit, Scarlet lets you define an interface for the API methods you want to use:
interface CoinbaseService {
@Receive
fun observeWebSocket(): Flow<WebSocket.Event>
@Send
fun sendSubscribe(subscribe: Subscribe)
@Receive
fun observeTicker(): Flow<Ticker>
}
The code speaks for itself. Functions that send information to the API are annotated with @Send
while functions that receive data or listen to the state of the connection are marked with @Receive
.
The Subscribe
and Ticker
classes are our own data classes just like in Retrofit. In this case, we are using Moshi to encode & decode JSON objects. For a full reference, those classes are described below:
@JsonClass(generateAdapter = true)
data class Subscribe(
val type: String = "subscribe",
@Json(name = "product_ids") val productIds: List<String>,
val channels: List<String>,
)
@JsonClass(generateAdapter = true)
data class Ticker(
@Json(name = "product_id") val productId: String,
val price: String,
)
Controlling the state of your connection
Since the connection of a WebSocket is long-lived, we face certain challenges in order to keep it alive. Unstable network environments might be the most frequent issue to encounter, but we also need to account for the lifecycle of the application itself.
Scarlet gives us a few lifecycle handles out of the box:
AndroidLifecycle.ofApplicationForeground
connects and disconnects when the application is resumed and stopped, respectively.AndroidLifecycle.ofLifecycleOwnerForeground
depends on thelifecycleOwner
we set up, which could be, for instance, anActivity
.
Additionally, we can leverage the LifecycleRegistry
to manually control the connection state and, we can even combine multiple lifecycle objects.
For instance, we could have a requirement that states that the connection of the WebSocket will only be open in a given activity and when a user is logged in.
Note: You would have to create your own lifecycle object to check the login status, obviously.
When we are experiencing loss of connectivity, we might not want to continuously attempt to reconnect. Scarlet also allows us to configure a backoff strategy, and it comes with some common implementations:
LinearBackoffStrategy
ExponentialBackoffStrategy
ExponentialWithJitterBackoffStrategy
For more information on choosing which backoff strategy is best for your use-case, please refer to this AWS blog post which explains the differences in more detail.
Configuring Scarlet & creating a service instance
Finally, we just need to bring everything together when configuring Scarlet to create our service instance:
val client: OkHttpClient = /* ... */
val moshi: Moshi = /* ... */
val application: Application = /* ... */
val service = Scarlet.Builder()
.webSocketFactory(client.newWebSocketFactory("wss://ws-feed.pro.coinbase.com"))
.addMessageAdapterFactory(MoshiMessageAdapter.Factory(moshi))
.addStreamAdapterFactory(FlowStreamAdapter.Factory())
.lifecycle(AndroidLifecycle.ofApplicationForeground(application))
.build()
.create<CoinbaseService>()
We now have all the building blocks we need to create our monitoring app. If you would like to take a look at the full source code, you can find a sample app on GitHub.
As always, we hope you liked this article and if you have anything to add, we are available via our Support Channel.