Sitemap

WebSockets, ViewModel e Jetpack Compose

8 min readJun 29, 2023

Fui pesquisar como utilizar WebSockets com Android e fiquei muito contente em descobrir como é fácil. 😌 Então resolvi compartilhar aqui por meio de um exemplo bem simples.

Servidor WebSocket

Para testar esse exemplo, não é preciso criar um servidor WebSocket do zero. Nesse post será utilizado o site PieSocket, onde é possível criar uma conta gratuita e "levantar" um servidor WebSocket.

Após criar a conta e fazer o login, na opção "Dashboard" (do lado esquerdo) é preciso criar um "Channel Cluster" clicando no botão "Create Channels Cluster".

Press enter or click to view image in full size
Criando um Channel Cluster no site PieSocket

No campo "Cluster Name" é possível definir um nome para o cluster, mas o importante aqui é escolher o tipo do plano. O plano gratuito permite 200 conexões simultâneas e 200 mil mensagens por dia. Isso é mais do que o suficiente para o teste que será feito aqui.

Mais abaixo, ainda é possível escolher a região onde esse cluster será criado. No momento da escrita desse artigo, o cluster mais próximo do Brasil é em Nova Iorque. Selecione essa opção e clique em "Create Cluster".

Criando um Channel Cluster no site PieSocket

Após criar o cluster, selecione-o na listagem e será exibida diversas informações sobre ele.

Press enter or click to view image in full size

Na implementação do aplicativo que será apresentada a seguir, será preciso o “Cluster ID” e da “API Key”, pois a URL para estabelecer a conexão com o servidor segue o seguinte padrão:
wss://<cluster_id>.piesocket.com/v3/1?api_key=<API_Key>

Perceba que existe um botão "Test online" que será utilizado ao final deste artigo para testar o envio de mensagens do servidor para o cliente.

Criando a aplicação Android

Dependências

Crie um novo projeto Compose no Android Studio (neste post estou usando a versão Flamingo | 2022.2.1 Patch 2).

Essas são as dependências necessárias para o projeto.

dependencies {
// Automaticamente adicionadas ao projeto
implementation 'androidx.core:core-ktx:1.10.1'
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation platform('androidx.compose:compose-bom:2022.10.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
// Dependências que precisaremos neste exemplo
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'

Além das dependências adicionadas automaticamente, as duas últimas foram incluídas:

  • OkHttp para realizar a conexão com o WebSocket;
  • View Model Compose, para poder instanciar o view model mais facilmente (usando a função viewModel()). Caso esteja usando Hilt ou Koin, essa última dependência não é necessária.

Message Service

A classe MessageService listada a seguir contém os métodos para conectar, desconectar, enviar e receber mensagens do WebSocket.

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener

class MessageService {
private val _isConnected = MutableStateFlow(false)
val isConnected = _isConnected.asStateFlow()

private val _messages = MutableStateFlow(emptyList<Pair<Boolean, String>>())
val messages = _messages.asStateFlow()

private val okHttpClient = OkHttpClient()
private var webSocket: WebSocket? = null

private val webSocketListener = object : WebSocketListener() {

override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
_isConnected.value = true
webSocket.send("Android Client Connected")
}

override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
_messages.update {
val list = it.toMutableList()
list.add(false to text)
list
}
}

override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
}

override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason)
_isConnected.value = false
}

override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
super.onFailure(webSocket, t, response)
}
}

fun connect() {
val cluseterId = "SEU_CLUSTER_ID" // e.g: s9999.nyc1
val apiKey = "SUA_API_KEY"
val webSocketUrl =
"wss://$cluseterId.piesocket.com/v3/1?api_key=$apiKey"

val request = Request.Builder()
.url(webSocketUrl)
.build()

webSocket = okHttpClient.newWebSocket(request, webSocketListener)
}

fun disconnect() {
webSocket?.close(1000, "Disconnected by client")
}

fun shutdown() {
okHttpClient.dispatcher.executorService.shutdown()
}

fun sendMessage(text: String) {
if (_isConnected.value) {
webSocket?.send(text)
_messages.update {
val list = it.toMutableList()
list.add(Pair(true, text))
list
}
}
}
}

O atributo isConnected é um StateFlow para informar se o socket está conectado ou não. Em messages (que também é um StateFlow) ficará armazenada a lista de mensagens recebidas pelo servidor. Perceba que é uma lista de Pair<Boolean,String> onde: o Boolean indica se a mensagem foi enviada pelo próprio device, ou veio do servidor; e a String é a mensagem em si. Fiz dessa forma para simplificar o exemplo, mas na prática, teríamos uma estrutura como um JSON, onde faríamos o decode, converteríamos para um objeto e teríamos algo mais elaborado.

A parte mais importante desse código é o objeto webSocketListener que implementa a interface WebSocketListener. Os métodos dessa interface são quase auto-explicativos:

  • onOpen é chamado quando a conexão com o web socket é estabelecida. Neste momento, o valor da propriedade isConnected é atualizado e a mensagem "Android Client Connected" é enviada para o servidor.
  • onMessage é chamado quando uma mensagem do servidor é recebida. Essa mensagem é do tipo String (e não um array de bytes) o que deixa a manipulação muito mais simples. Nesse momento, a lista de mensagens recebidas é atualizada, como está vindo do servidor, o valor false é atribuído seguido da mensagem.
  • onClosing será invocado quando o servidor ou o cliente iniciar o processo de desconexão indicando que nenhuma outra mensagem será transmitida.
  • onClose é invocado quando cliente e servidor indicaram que nenhuma outra mensagem será transmitida e a conexão foi liberada com sucesso. Nenhuma outra chamada para este listener será feita. Aqui é onde o atributo isConnected deve ser atualizado.
  • onFailure é chamado quando o WebSocket foi fechado devido a um erro de leitura ou gravação na rede. As mensagens enviadas e recebidas podem ter sido perdidas. Nenhuma outra chamada para este listener será feita.

No método connect, é onde a conexão é estabelecida. O objeto Request é criado passando a URL do servidor como parâmetro. Perceba que na URL é preciso o ID do cluster e a API Key. O ID do cluster possui também a região onde encontra-se o servidor (por exemplo, s9999.nyc1 — New York City 1).

Caso queira que o servidor notifique automaticamente o cliente, enviando de volta uma cópia da mensagem recebida, basta adicionar &notifySelf=1 ao final da URL de conexão.

Para criar a instância do webSocket é preciso do okHttpClient. A conexão é estabelecida ao invocar o método newWebSocket passando o objeto Request e o WebSocketListener.

O método disconnect encerra a conexão. O primeiro parâmetro é o código de status de fechamento da conexão conforme definido na RFC-6455. O valor 1000 indica um encerramento normal. O segundo parâmetro é a razão da desconexão (até 123 bytes). Aqui foi usada uma mensagem simples.

O shutdown serve para encerrar o Executor atrelado ao objeto OkHttpClient.

Finalmente, em sendMessage, caso o socket esteja conectado, a mensagem é enviada pelo WebSocket e a lista de mensagens é atualizada.

View Model

O trabalho do MainViewModel listado a seguir é simplesmente intermediar a comunicação entre a Composable function (que será mostrada na próxima seção) e o MessageService que foi criada na seção anterior.

import androidx.lifecycle.ViewModel

class MainViewModel : ViewModel() {

private val messageService = MessageService()

val socketStatus = messageService.isConnected
val messages = messageService.messages

override fun onCleared() {
super.onCleared()
messageService.shutdown()
}

fun send(text: String) {
messageService.sendMessage(text)
}

fun connect() {
messageService.connect()
}

fun disconnect() {
messageService.disconnect()
}
}

Nada de especial aqui. Estamos apenas delegando as chamadas para o MessageService. Caso você esteja usando uma biblioteca de injeção de dependência (como Hilt ou Koin), você deve injetar esse serviço no construtor.

Composable UI

A implementação da UI será bem simples. Na AppBar será exibido o status da conexão (Conectado ou Desconectado) e um botão para Conectar e Desconectar do servidor. Na parte inferior teremos um painel para o usuário digitar e enviar a mensagem. E ocupando o restante da tela teremos a listagem de mensagens.

Começaremos pela função WebSocketChatScreen.

@Composable
fun WebSocketChatScreen(
viewModel: MainViewModel = viewModel()
) {
val status by viewModel.socketStatus.collectAsState(false)
val messages by viewModel.messages.collectAsState(emptyList())

Scaffold(
topBar = {
TopAppBar(
isConnected = status,
onConnect = viewModel::connect,
onDisconnect = viewModel::disconnect
)
}
) {
Column(Modifier.padding(it).fillMaxSize()) {
LazyColumn(Modifier.weight(1f).fillMaxWidth()) {
items(messages) { item ->
MessageItem(item = item)
}
}
BottomPanel(onSend = viewModel::send)
}
}
}

Essa função instancia o MainViewModel e observa seus dois StateFlow (status e messages). No Scaffold é definida a topAppBar que está utilizando a função TopAppBar declarada a seguir:

@Composable
private fun TopAppBar(
isConnected: Boolean,
onConnect: () -> Unit,
onDisconnect: () -> Unit,
) {
val statusText = if (isConnected) "Connected" else "Disconnected"
val contentDesc = if (!isConnected) "Connect" else "Disconnect"
val buttonIcon = if (isConnected) Icons.Outlined.Close else Icons.Outlined.Check
TopAppBar(
title = { Text(statusText) },
actions = {
IconButton(onClick = {
if (!isConnected) {
onConnect()
} else {
onDisconnect()
}
}) {
Icon(imageVector = buttonIcon, contentDescription = contentDesc)
}
},
)
}

Na parte superior é exibido se o socket está conectado ou não. Além disso, o botão de conectar/desconectar fica nessa barra.

Voltando ao Scaffold , temos uma LazyColumn que exibe a listagem das mensagens. Cada mensagem é exibida pela função MessageItem. Essa função é bem simples: caso a mensagem tenha sido enviada pelo usuário, aparecerá "You: <mensagem>" , caso a mensagem tenha vindo do servidor, será exibido "Other: <mensagem>".

@Composable
private fun MessageItem(item: Pair<Boolean, String>) {
val (iAmTheSender, message) = item
Text(
text = "${if (iAmTheSender) "You: " else "Other: "} $message",
modifier = Modifier.padding(8.dp)
)
}

Por fim, o BottomPanel é a parte da interface onde o usuário digitará a mensagem a ser enviada.

@Composable
private fun BottomPanel(
onSend: (String) -> Unit
) {
var text by remember {
mutableStateOf("")
}
Row(Modifier.padding(8.dp)) {
OutlinedTextField(
value = text,
onValueChange = { s -> text = s },
modifier = Modifier.weight(1f),
)
TextButton(
onClick = {
onSend(text)
text = ""
},
enabled = text.isNotBlank(),
) {
Text("Send")
}
}
}

Agora é preciso fazer dois ajustes no AndroidManifest.xml. Primeiro, adicionar a permissão de internet.

<uses-permission android:name="android.permission.INTERNET" />

Em seguida, redimensionar a activity quando o teclado for exibido.

<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize" <-- adicionar

Pronto! Basta executar a aplicação, conectar-se ao servidor e enviar mensagens. Para enviar mensagens a partir do servidor, basta clicar no botão "Test online". A imagem a seguir mostra a aplicação sendo executada no emulador ao lado da página de teste do servidor web socket da PieSocket.

Press enter or click to view image in full size
Página de teste do Web Socket da PieSocket e o aplicativo Android desenvolvido nesse artigo

Conclusão

Realmente é muito simples utilizar Web Sockets no Android. Obviamente uma aplicação profissional requer uma complexidade muito maior, com um tratamento mais elaborado dos dados e um melhor gerenciamento de conexão. Mas o processo de conexão, envio e recebimento das mensagens é realmente muito simples.

Esse artigo foi totalmente baseado no post "WebSockets in Android with OkHttp and ViewModel" escrito por

, a quem dou os devidos créditos.

4br4ç05,
nglauber

--

--

Nelson Glauber
Nelson Glauber

Written by Nelson Glauber

Android Developer. Google Developer Expert for Android/Kotlin. Author of “Dominando o Android”.

No responses yet