Cómo usar Azure SignalR desde una aplicación Android

En uno de los artículos anteriores te hablé sobre una prueba de concepto con SignalR, que trataba de un chat en tiempo real que moderaba los mensajes que los participantes enviaban a través de él. Hoy quiero contarte cómo usar la librería disponible para Java en una aplicación Android, tanto para enviar como recibir mensajes de este chat.

Crear un proyecto con Android Studio

Como vamos a empezar desde cero, lo primero que necesitamos es una aplicación, que crearemos a través de Android Studio.

Crear un proyecto con Android Studio

Para poder trabajar con SignalR la versión mínima soportada de la API de Android debe ser la 16.

Añadir librería de SignalR al proyecto

Una vez creado, lo siguiente que necesito es añadir la librería para Java de SignalR, en el archivo Gradle Scripts > build.gradle (Module app):

plugins {
    id 'com.android.application'
}
android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"
    defaultConfig {
        applicationId "com.example.androidclientsignalr"
        minSdkVersion 22
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
dependencies {
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    implementation 'com.microsoft.signalr:signalr:5.0.4'
    implementation 'org.slf4j:slf4j-jdk14:1.7.25'
}

En el momento de escribir este post la versión de SignalR para Maven es la 5.0.4. Revisa aquí si hay alguna nueva. También he añadido SLF4J para poder registrar las trazas que lanza la librería.

Permiso de acceso a Internet

Por otro lado, como necesitamos comunicarnos con Azure SignalR, es necesario añadir el permiso INTERNET en el archivo app/manifests/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.androidclientsignalr">
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidClientSignalR">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Layout para la prueba

Para poder comprobar que nuestro cliente funciona correctamente, añade los siguientes elementos en app/res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <ListView
        android:id="@+id/chatMessages"
        android:layout_width="336dp"
        android:layout_height="481dp"
        android:backgroundTint="@color/material_on_background_disabled"
        android:maxLines="20"
        android:ems="12"
        android:gravity="start|top"
        android:inputType="textLongMessage"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.186" />
    <EditText
        android:id="@+id/txtMessage"
        android:layout_width="333dp"
        android:layout_height="46dp"
        android:ems="10"
        android:inputType="textPersonName"
        android:text="Hello from Android!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/chatMessages"
        app:layout_constraintVertical_bias="0.107" />
    <Button
        android:id="@+id/btnSend"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="36dp"
        android:text="Send"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/txtMessage"
        app:layout_constraintVertical_bias="0.054" />
    <Button
        android:id="@+id/btnLogin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="Login"
        app:layout_constraintEnd_toStartOf="@+id/btnSend"
        app:layout_constraintHorizontal_bias="0.737"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/txtMessage" />
</androidx.constraintlayout.widget.ConstraintLayout>

Ahora lo siguiente que necesitamos es definir una lógica para esta interfaz, en app/java/com.example.androidclientsignalr/MainActivity.java:

package com.example.androidclientsignalr;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import com.microsoft.signalr.HubConnection;
import com.microsoft.signalr.HubConnectionBuilder;
import com.microsoft.signalr.HubConnectionState;
import java.time.Instant;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
    //UI Elements
    Button btnLogin;
    Button btnSend;
    EditText txtMessage;
    ListView chatMessages;
    ArrayList<String> messages;
    HubConnection hubConnection;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //Get UI elements
        btnLogin = (Button) findViewById(R.id.btnLogin);
        btnSend = (Button) findViewById(R.id.btnSend);
        txtMessage = (EditText) findViewById(R.id.txtMessage);
        chatMessages = (ListView) findViewById(R.id.chatMessages);
        messages = new ArrayList<>();
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, messages);
        chatMessages.setAdapter(adapter);

        //Create hub connection
        hubConnection = HubConnectionBuilder.create("https://WEBISTE_NAME.azurewebsites.net/chat").build();
        hubConnection.on("BroadcastMessage", (name, message, currentTime, ugly, terms) -> {
            System.out.println("[broadcastMessage] name: " + name);
            System.out.println("[broadcastMessage] message: " + message);
            System.out.println("[broadcastMessage] currentTime: " + currentTime);
            System.out.println("[broadcastMessage] ugly: " + ugly);
            System.out.println("[broadcastMessage] terms: " + terms);

            this.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    messages.add("[" + name + "] " + message);
                    adapter.notifyDataSetChanged();
                }
            });
        }, String.class, String.class, Float.class, Boolean.class, ArrayList.class);

        hubConnection.on("Echo", (name, message) -> {
            System.out.println("[echo] name: " + name);
            System.out.println("[echo] message: " + message);
        }, String.class, String.class);

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (btnLogin.getText().toString().toLowerCase().equals("login")) {
                    if (hubConnection.getConnectionState() == HubConnectionState.DISCONNECTED) {

                        try {
                            hubConnection.start().blockingAwait();
                        } catch (Exception ex) {
                            System.out.println("Error: " + ex.getMessage());
                        }
                        if (hubConnection.getConnectionState() == HubConnectionState.CONNECTED) {
                            btnLogin.setText("logged in");
                        }
                    }
                } else if (btnLogin.getText().toString().toLowerCase().equals("logged in")) {
                    if (hubConnection.getConnectionState() == HubConnectionState.CONNECTED) {
                        hubConnection.stop();
                        btnLogin.setText("login");
                    }
                }
            }
        });
        btnSend.setOnClickListener(new View.OnClickListener() {
            @RequiresApi(api = Build.VERSION_CODES.O)
            @Override
            public void onClick(View v) {
                if (hubConnection.getConnectionState() == HubConnectionState.CONNECTED) {
                    try {
                        String text = txtMessage.getText().toString();
                        //DateTime format: "2021-03-23T15:06:22.900Z"
                        hubConnection.send("broadcastMessage", new Object[]{"android-app", text, 1, Instant.now().toString()});
                        //hubConnection.send("echo", new Object[]{"android-app", text});

                    } catch (Exception ex) {
                        System.out.println("[ERROR] " + ex.getMessage());
                    }
                }
            }
        });
    }
}

Lo primero que hago es recuperar los elementos de la interfaz que he definido y acto seguido creo la conexión con Azure SignalR, utilizando HubConnectionBuilder.create y la URL de la web que está manejando la negociación con el servicio, para finalmente almacenar el resultado en un objeto del tipo HubConnection. Sin embargo, con esto solo no estoy conectada al servicio, sino que necesito llamar al metodo .start, para lo que he utilizado el evento click de un botón llamado btnLogin. Por otro lado, para estar a la escucha de posibles notificaciones, de otros clientes que envíen mensajes al chat e incluso de la propia aplicación Android, uso del método .on para poder suscribirnos a aquellos métodos que provienen de Azure SignalR (en este ejemplo broadcastMessage y echo). Por último, con .send podemos enviar información desde esta aplicación al servicio. Debes tener especial especial cuidado con los tipos, sobre todo con el formato de la fechas para que el parseo entre Java y C# se haga correctamente.

Si ahora ejecutas la aplicación, y accedes también al cliente web generado en el artículo anterior, podrás mandar mensajes desde diferentes tipos de cliente:

AndroidClientSignalR

El código lo tienes disponible como parte del repositorio de la PoC de SignalR en GitHub.

¡Saludos!