Android WebSocket Clients for Amazon API Gateway
Tags: aws, websockets, android, kotlin
Published: 2021-01-08 09:25:05

---
title: Android WebSocket Clients for Amazon API Gateway
published: true
description: Interacting with an Amazon API Gateway WebSocket endpoint from an Android client, using Ktor and OkHttp in Kotlin
tags: aws, websockets, android, kotlin
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/644wmjpegzm09i2jycbv.png
---

WebSockets are a great way to achieve bi-directional communication between a mobile app and a backend. In December 2018, Amazon's API Gateway service [launched serverless support for WebSockets](https://aws.amazon.com/blogs/compute/announcing-websocket-apis-in-amazon-api-gateway/).

Setting it all up is somewhat involved. I recommend starting with the [Simple WebSockets Chat backend](https://github.com/aws-samples/simple-websockets-chat-app) they provide as a demo. We'll focus on how to talk to it from Android, using Square's familiar [OkHttp](https://square.github.io/okhttp/) or JetBrains' newer [Ktor](https://ktor.io/docs/clients-index.html).

## Pre-requisites

1. An [activate an AWS account](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/)
2. [The latest version of the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-mac.html#cliv2-mac-install-cmd)
3. [`wscat`](https://www.npmjs.com/package/wscat#installation) installed via `npm`.
4. [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install-mac.html#serverless-sam-cli-install-mac-sam-cli) installed via Homebrew (if on Mac.)
5. [Android Studio](https://developer.android.com/studio)

## Setting Up a Backend
Installing the serverless demo app is pretty straight-forward.

Checkout the Chat app:
```bash
git clone https://github.com/aws-samples/simple-websockets-chat-app.git
```

And deploy it to your account:
```bash
sam deploy --guided
```

After a while, that command will finish. You'll be able to see the provisioned resources by inspecting the outputs of the CloudFormation stack:
```bash
aws cloudformation describe-stacks \
    --stack-name simple-websocket-chat-app \
    --query 'Stacks[].Outputs'
```

It creates a DynamoDB table, three Lambda functions, an AWS IAM role, and an API Gateway with WebSockets support.

After you've glanced over the output to understand what all was created, let's zero in on the WebSocket endpoint itself. We'll need to find the URI that our client can use, to access it.

```bash
aws cloudformation describe-stacks \
    --stack-name simple-websocket-chat-app \
    --query 'Stacks[].Outputs[]' | \
jq -r '.[]|select(.OutputKey=="WebSocketURI").OutputValue'
```

This should output something like:
```console
wss://azgu36n0vf.execute-api.us-east-1.amazonaws.com/Prod
```

## Command Line Testing using `wscat`

Let's quickly test the backend, to make sure it works as we expect. We'll use the [`wscat` command-line utility](https://github.com/websockets/wscat#wscat). The command below will open a long-lived connection to the API Gateway, and we'll be able to send and receive messages in a subshell:

```console
$ wscat -c wss://azgu36n0vf.execute-api.us-east-1.amazonaws.com/Prod
Connected (press CTRL+C to quit)
> {"action": "sendmessage", "data": "foo"}
< foo
```

The JSON format above is required by the app we deployed. You can change the value of `"foo"`, but you can't change anything else.

If you try to pass something else, it doesn't work:
```console
> Hello, how are you?
< {"message": "Forbidden", "connectionId":"Y0bEuc0UIAMCIiA=", "requestId":"Y0bwuGjXIAMFmEg="}
```

But, if you open up multiple terminal windows, and connect them all to the endpoint, they'll all receive a valid message.

## Calling the API from Android via OkHttp

Now that we know the WebSocket API is working, let's start building an Android app to use as a client, instead.

WebSockets have been [supported in OkHttp since 3.5](https://developer.squareup.com/blog/web-sockets-now-shipping-in-okhttp-3-5/), which came out all the way back in 2016.

Initializing a WebSocket client is straight-forward:

```kotlin
val request = Request.Builder()
    .url("wss://azgu36n0vf.execute-api.us-east-1.amazonaws.com/Prod")
    .build()
val listener = object: WebSocketListener() {
    override fun onMessage(ws: WebSocket, mess: String) {
        // Called asynchronously when messages arrive
    }
}
val ws = OkHttpClient()
    .newWebSocket(request, listener)
```

To send a message to the API Gateway, all we have to do is:
```kotlin
ws.send(JSONObject()
    .put("action", "sendmessage")
    .put("data", "Hello from Android!")
    .toString())
```

We can add a button to our UI with [ViewBinding](https://developer.android.com/topic/libraries/view-binding), and fire the WebSocket message whenever we click it:
```kotlin
ui.button.setOnClickListener { 
    ws.send(JSONObject()
        .put("action", "Hello from Android!")
        .put("data", command)
        .toString())
}
```

Since all of the threading is handled inside OkHttp, there really isn't a lot more to it. Save a handle to your view binding and to to your WebSocket client when you create your `Activity`:
```kotlin
class MainActivity : AppCompatActivity() {
    private lateinit var ui: ActivityMainBinding
    private lateinit var ws: WebSocket

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ui = ActivityMainBinding.inflate(layoutInflater)
        setContentView(ui.root)
        connect() // As above
    }
```

## Calling the API from Android via Ktor

Ktor is built with the assumption that you're using Coroutines, and managing your own scope/context. This makes it a more flexible tool, but adds some additional complexity.

The basic setup for the tool is going to look like this:
```kotlin
private suspend fun connect(ktor: HttpClient, u: Url) {
    ktor.wss(Get, u.host, u.port, u.encodedPath) {
        // Access to a WebSocket session
    }
}

private /* not suspend */ fun connect() {
    val url = Url("wss://azgu36n0vf.execute-api.us-east-1.amazonaws.com/Prod")
    val ktor = HttpClient(OkHttp) {
        install(WebSockets)
    }
    lifecycleScope.launch(Dispatchers.IO) {
        connect(ktor, url)
    }
}
```

Inside the trailing closure, you have access to an instance of [`DefaultClientWebSocketSession`](https://api.ktor.io/1.3.2/io.ktor.client.features.websocket/-default-client-web-socket-session/index.html). It has two important members:
 1. A [`ReceiveChannel`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/index.html) named `ingoing`, and
 2. A [`SendChannel`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/) named `outgoing`.

`SendChannel` and `ReceiveChannel` are the inlet and outlet to a Kotlin [`Channel`](https://kotlinlang.org/docs/reference/coroutines/channels.html#channels), which is basically like a suspendible queue.

It's pretty trivial to send and receive some simple messages inside the WebSocket session closure:
```kotlin
ktor.wss(Get, u.host, u.port, u.encodedPath) {
    // Send a message outbound
    val json = JSONObject()
        .put("action", "sendmessage")
        .put("data", "Hello from Android!")
        .toString()
    outgoing.send(Frame.Text(json))

    // Receive an inbound message
    val frame = incoming.receive()
    if (frame is Frame.Text) {
        ui.status.append(frame.readText())
    }
}
```

However, we're missing a bunch of functionality that we had in our OkHttp solution. Namely:

1. We want to send and receive data at the same time, in a loop, and
2. We want to get notifications when the connection opens and closes.

## Parallel send and receive in Ktor

Our goal here is ultimately to dispatch a stream of messages to the `outgoing` channel, and to consume a stream of messages from the `ingoing` channel, _at the same time_. The basic approach we'll use is to launch two bits of work asynchronously, and then wait for them to finalize:

```kotlin
ktor.wss(Get, u.host, u.port, u.encodedPath) {
    awaitAll(async {
        // Code that will send messages
    }, async {
        // Code that will receive messages
    })
}
```

We need some stream of events to trigger the send messages. Adding a button element to our UI makes sense. But, we'd like to model the button clicks as a stream of commands, by the time we dispatch events over the WebSocket.

Let's first create an extension function to map button clicks into a `Flow<Unit>` (credit to [StackOverflow](https://stackoverflow.com/a/57914394/695787)):
```kotlin
private fun View.clicks(): Flow<Unit> = callbackFlow {
    setOnClickListener { offer(Unit) }
    awaitClose { setOnClickListener(null) }
}
```

Now, we can listen to a flow of events from a button, map them into the format we need, and send them:
```kotlin
ui.button.clicks()
    .map { click -> "Hello from Android!" }
    .map { message -> JSONObject()
        .put("action", "sendmessage")
        .put("data", message)
        .toString()
    }
    .map { json -> Frame.Text(json) }
    .collect { outgoing.send(it) }
```

That will work well for the outgoing events. Now, we just need to respond to _inbound_ events, in the second `async` block.

In this case, there isn't a lot of value to mapping the result. The value we receive over the socket is the contents associated with the `"data"` key in the messages. So for example, we might get `"Hello from Android!"`: 

```kotlin
incoming.consumeEach { frame ->
    if (frame is Frame.Text) {
        ui.status.append(frame.readText())
    }
}
```

When it's all said and done, we end up with something like this:
```kotlin
ktor.wss(Get, u.host, u.port, u.encodedPath) {
    awaitAll(async {
        ui.button.clicks()
            .map { click -> JSONObject()
                .put("action", "sendmessage")
                .put("data", "Hello from Android!")
                .toString()
            }
            .map { json -> Frame.Text(json) }
            .collect { outgoing.send(it) }
    }, async {
        incoming.consumeEach { frame ->
            if (frame is Frame.Text) {
                ui.status.append(frame.readText())
            }
        }
    }
})
```

## Ktor's lifecycle events
OkHttp allowed us to override callbacks on the `WebSocketListener`, to get notified of various lifecycle events:
```kotlin
val listener = WebSocketListener() {
    override fun onOpen(ws: WebSocket, res: Response) {
        // when WebSocket is first opened
    }
    override fun onClosed(ws: WebSocket, code: Int, reason: String) {
        // when WebSocket is closed
    }
}
```

Ktor doesn't work like that. They suggest some [different approaches to recover those events](https://ktor.io/docs/websocket.html#standard-events).

The easiest one to recover is the event when the socket opens. It's just the first thing that happens inside of the `wss` session closure:

```kotlin
ktor.wss(Get, u.host, u.port, u.encodedPath) {
    ui.status.append("Connected to $u!")
}
```

To get more insight into the WebSocket termination, we can expand our processing in our receive block:
```kotlin
incoming.consumeEach { frame ->
    when (frame) {
        is Frame.Text -> { /* as above */ }
        is Frame.Close -> {
            val reason = closeReason.await()!!.message
            ui.status.append("Closing: $reason")
        }
    }
}
```

Catching failures is also fairly easy:

```kotlin
private fun connect() {
    val url = Url("wss://azgu36n0vf.execute-api.us-east-1.amazonaws.com/Prod")
    val client = HttpClient(OkHttp) {
        install(WebSockets)
    }
    lifecycleScope.launch(Dispatchers.IO) {
        try {
            connect(client, url)
        } catch (e: Throwable) {
            val message = "WebSocket failed: ${e.message}"
            ui.status.append(message)
        }
    }
}
```

## Wrapping Up

Well, there you have it: a rough and dirty explanation of how to use OkHttp and Ktor to consume an Amazon API Gateway WebSockets API. 🥳.

[The code for this project is available on my GitHub page](https://github.com/jamesonwilliams/android-websocket-client#simple-websocket-app).