Android quickstart
Install the pinned SDK version, obtain credentials from your backend, exchange online messages, and clean up.
This tutorial uses 1.0.5 to exchange online messages between Alice and Bob in separate clients. WebSocket JSON-RPC CONNECT authenticates the connection.
1. Prepare
Prepare the following:
- Android 5.0 (API 21) or later.
- An AndroidX application. For an exact reference, this tag itself uses Kotlin
1.9.0, Android Gradle Plugin8.1.4, Gradle8.4, and compileSdk34. - A WuKongIM single-node cluster or multi-node cluster whose
/readyzis healthy and whose WebSocket Gateway is reachable from the Android device. - A product backend that returns
uid, a short-livedtoken, andwebsocketUrlseparately for Alice and Bob.
Read Identity & Token first. Example variables come from the product backend; real tokens never belong in the APK, repository, Logcat, or crash reports.
To see messages first, follow the official example with two clients. The steps below integrate the SDK into your application.
The default device category is APP 0. Store the Token with the same device_flag on your backend: APP is 0, WEB is 1, and PC is 2.
2. Install the SDK
Add this to the application module's build.gradle.kts:
dependencies {
implementation("com.githubim:easysdk-android:1.0.5")
}The equivalent Groovy DSL is:
dependencies {
implementation 'com.githubim:easysdk-android:1.0.5'
}Confirm that gradle.properties enables AndroidX:
android.useAndroidX=trueNetwork permissions
Add these entries to AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />Production uses wss://. Android 9 and later restrict cleartext traffic by default; do not globally enable cleartext as a production TLS workaround.
3. Connect and listen
data class IMBootstrap(
val uid: String,
val token: String,
val websocketUrl: String,
)The product backend authenticates the current product user and selects a WebSocket address. The Android client receives only its own connection material and no Product HTTP management credential.
Android EasySDK is a process singleton, and v1.0.5 cannot be initialized a second time with another configuration. This example fits a minimal run with one UID per process. A production application that switches accounts must first design and test a complete logout strategy around this limitation.
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.githubim.easysdk.WuKongConfig
import com.githubim.easysdk.WuKongEasySDK
import com.githubim.easysdk.enums.WuKongChannelType
import com.githubim.easysdk.enums.WuKongEvent
import com.githubim.easysdk.listener.WuKongEventListener
import com.githubim.easysdk.model.ConnectResult
import com.githubim.easysdk.model.DisconnectInfo
import com.githubim.easysdk.model.Message
import com.githubim.easysdk.model.WuKongError
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
class ChatActivity : AppCompatActivity() {
private val easySDK = WuKongEasySDK.getInstance()
private var listenersRegistered = false
private var connected = false
private val connectListener = object : WuKongEventListener<ConnectResult> {
override fun onEvent(result: ConnectResult) = runOnUiThread {
connected = true
Log.i("EasySDK", "connected")
}
}
private val messageListener = object : WuKongEventListener<Message> {
override fun onEvent(message: Message) = runOnUiThread {
Log.i("EasySDK", "message received")
// Deduplicate by messageId before updating the UI.
}
}
private val disconnectListener = object : WuKongEventListener<DisconnectInfo> {
override fun onEvent(info: DisconnectInfo) = runOnUiThread {
connected = false
Log.i("EasySDK", "disconnected")
}
}
private val errorListener = object : WuKongEventListener<WuKongError> {
override fun onEvent(error: WuKongError) = runOnUiThread {
Log.e("EasySDK", "SDK operation failed")
}
}
fun connect(bootstrap: IMBootstrap) {
val config = WuKongConfig.Builder()
.serverUrl(bootstrap.websocketUrl)
.uid(bootstrap.uid)
.token(bootstrap.token)
.connectionTimeout(15_000)
.requestTimeout(15_000)
.maxReconnectAttempts(5)
.debugLogging(false)
.build()
val current = easySDK.getConfig()
if (current == null) {
easySDK.init(applicationContext, config)
} else {
check(
current.uid == bootstrap.uid &&
current.token == bootstrap.token &&
current.serverUrl == bootstrap.websocketUrl &&
current.connectionTimeoutMs == config.connectionTimeoutMs &&
current.requestTimeoutMs == config.requestTimeoutMs &&
current.maxReconnectAttempts == config.maxReconnectAttempts &&
current.deviceFlag == config.deviceFlag &&
current.debugLogging == config.debugLogging
) {
"WuKongEasySDK v1.0.5 cannot apply changed identity or configuration; " +
"stop and restart the process or upgrade the SDK"
}
}
registerListeners()
connected = easySDK.isConnected()
if (connected) return
lifecycleScope.launch {
runCatching { withTimeout(20_000) { easySDK.connect() } }
.onFailure {
connected = false
easySDK.disconnect()
Log.e("EasySDK", "connect failed")
}
}
}
private fun registerListeners() {
if (listenersRegistered) return
easySDK.addEventListener(WuKongEvent.CONNECT, connectListener)
easySDK.addEventListener(WuKongEvent.DISCONNECT, disconnectListener)
easySDK.addEventListener(WuKongEvent.MESSAGE, messageListener)
easySDK.addEventListener(WuKongEvent.ERROR, errorListener)
listenersRegistered = true
}
override fun onDestroy() {
if (listenersRegistered) {
easySDK.removeEventListener(WuKongEvent.CONNECT, connectListener)
easySDK.removeEventListener(WuKongEvent.DISCONNECT, disconnectListener)
easySDK.removeEventListener(WuKongEvent.MESSAGE, messageListener)
easySDK.removeEventListener(WuKongEvent.ERROR, errorListener)
listenersRegistered = false
}
if (isFinishing) easySDK.disconnect()
super.onDestroy()
}
}Listeners are registered only after init, and the same object references are retained for removeEventListener. The outer withTimeout bounds the complete wait at 20 seconds and disconnects after failure; SDK connection and request limits are 15 seconds, with at most 5 reconnect attempts. On Activity recreation, the page reads isConnected() instead of waiting for a CONNECT event that will not be replayed. A changed identity, token, URL, or connection setting stops explicitly rather than silently retaining a stale value. The example sets debugLogging(false) explicitly. Maven 1.0.5 applies that master gate to protocol-parsing and event-dispatch diagnostics and redacts public model strings. A production application normally lets an Application-scoped connection manager own the SDK while Activities or Fragments subscribe only to UI events. The screen-level owner above makes the minimal lifecycle visible.
4. Exchange the first message
Add this method inside the ChatActivity class above and call it from your send button after connection.
fun sendText(targetUid: String, text: String) {
check(connected) { "EasySDK is not connected" }
lifecycleScope.launch {
val payload: String = org.json.JSONObject()
.put("type", 1)
.put("version", 1)
.put("content", text)
.toString()
runCatching {
easySDK.send(
channelId = targetUid,
channelType = WuKongChannelType.PERSON,
payload = payload,
)
}.onSuccess {
Log.i("EasySDK", "SEND completed")
}.onFailure {
Log.e("EasySDK", "send failed")
}
}
}A returned send result means Alice obtained a result for the request. Bob must independently observe the message through his MESSAGE listener. Do not mark the peer as “received” inside Alice's send callback.
Because the SDK is a process singleton, use two devices, two emulators, or two independent application processes:
- Alice and Bob each obtain their own connection material from the product backend.
- Both sides observe
CONNECT; on failure, record only a stable error code and stage, never the original error text. - Alice calls
sendText("bob", ...)and records the send result. - Bob checks
fromUid,channelId,messageId, and the payload. - Bob sends back to Alice and proves the reverse direction.
- Recreate the Activity and confirm listeners are not duplicated; exit the application and confirm the connection closes.
5. Clean up
onDestroy() removes this page’s listeners and disconnects when the Activity finishes. An application connection belongs to the Application. Version 1.0.5 cannot reinitialize with another identity or configuration; never silently reuse an old Token.
6. Troubleshooting
SDK is not initialized: preserve theinit → addEventListener → connectorder.- The same UID gets a refreshed token or route and configuration changes:
v1.0.5has no public reset API. Stop explicitly and restart the process; do not continue with a stale token or URL. - The emulator connects but a physical device does not: confirm that the address is reachable outside the host or container and have the product backend return the device-reachable WSS ingress.
- CONNECT, SEND, or RECVACK parsing fails: confirm the server includes the EasySDK JSON-RPC compatibility implementation. The current server accepts snake_case fields and the README-aligned Android JSON text-string payload, as well as JSON objects and Base64 payloads, and returns snake_case results for Android. Use APP
0, WEB1, and PC2; do not carry forward older literals. - Raw JSON or
Paramsstill appears in Logcat withdebugLoggingdisabled: first confirm Gradle resolved1.0.5, clean the old build, then use a non-production canary to distinguish SDK output from application or third-party network logs. Stop the rollout and file a minimal reproduction if the Release artifact still exposes that canary. - Messages are duplicated: prevent repeated listener registration and deduplicate by
messageId.
Next
Continue with messaging and production checks. For offline recovery, conversations, unread counts, or push, see SDK selection. Versions and validation records retain the exact environments and scope of past runs.