Skip to main content

Custom message types

In addition to the built-in message types, you can create custom message types to fit your business needs.

Choose the base class based on your requirements:

  • MessageContent: For standard message content (e.g., text, location).
  • MediaMessageContent: For media content that requires file upload/download handling. Extends MessageContent with media file processing logic.

For more details on the message model, see Message overview.

tip

Custom message types must have a consistent structure across all platforms; otherwise, cross-platform messaging will fail.

Create a custom message

The SDK does not define or parse custom message content — you handle that yourself.

The @MessageTag annotation on a custom message type defines its unique identifier (objectname), storage behavior, display behavior, and unread count behavior.

Example: Standard custom message

The following is a complete example of a standard custom message:

kotlin
@MessageTag(value = "app:txtcontent", flag = MessageTag.ISCOUNTED)
class MyTextContent : MessageContent {
private val TAG = "appTextContent"
var content: String? = null

private constructor()

companion object {
@JvmStatic
fun obtain(content: String): MyTextContent {
val msg = MyTextContent()
msg.content = content
return msg
}

@JvmField
val CREATOR: Parcelable.Creator<MyTextContent> = object : Parcelable.Creator<MyTextContent> {
override fun createFromParcel(source: Parcel) = MyTextContent(source)
override fun newArray(size: Int) = arrayOfNulls<MyTextContent>(size)
}
}

override fun encode(): ByteArray? {
val jsonObj = super.getBaseJsonObject()
try {
jsonObj.put("content", this.content)
} catch (e: JSONException) {
Log.e(TAG, "JSONException ${e.message}")
}
return try {
jsonObj.toString().toByteArray(Charsets.UTF_8)
} catch (e: UnsupportedEncodingException) {
Log.e(TAG, "UnsupportedEncodingException", e)
null
}
}

constructor(data: ByteArray?) : super() {
if (data == null) return
val jsonStr = try {
String(data, Charsets.UTF_8)
} catch (e: UnsupportedEncodingException) {
Log.e(TAG, "UnsupportedEncodingException", e)
return
}
try {
val jsonObj = JSONObject(jsonStr)
super.parseBaseJsonObject(jsonObj)
if (jsonObj.has("content")) {
content = jsonObj.optString("content")
}
} catch (e: JSONException) {
Log.e(TAG, "JSONException ${e.message}")
}
}

override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToBaseInfoParcel(dest)
ParcelUtils.writeToParcel(dest, content)
}

constructor(parcel: Parcel) : super() {
super.readFromBaseInfoParcel(parcel)
content = ParcelUtils.readFromParcel(parcel)
}

override fun describeContents() = 0
}

Code walkthrough

Here is a step-by-step explanation of the custom message example:

  1. Add the @MessageTag annotation. This is required for all custom message types.

    ParameterTypeDescription
    valueStringRequired. The unique message type identifier (e.g., app:txtcontent). Keep it under 16 characters. Do not use the RC: prefix, which is reserved for built-in types.
    flagintRequired. Controls storage and counting behavior. See the flag values table below.
    messageHandlerClass<? extends MessageHandler>Optional. Use a custom handler if the encoded size exceeds 128 KB for media messages.

    Flag values:

    FlagUse case
    MessageTag.NONENot stored locally, not counted as unread. Supports missed messages. Use for command messages that don't need to be displayed.
    MessageTag.ISPERSISTEDStored locally but not counted as unread. Supports missed messages and remote history. Use for notification-style messages that need to be displayed but not counted.
    MessageTag.ISCOUNTEDStored locally and counted as unread. Supports missed messages and remote history. Use for text, image, and other user-facing messages.
    MessageTag.STATUSNot stored anywhere. Not counted as unread. Only delivered to online recipients; the server discards the message if the recipient is offline. Use for transient status like typing indicators.
  2. Define internal variables. Add the fields your message needs. In the example, content stores the message text.

  3. Implement a factory method. Provide a convenient way to create instances.

  4. Implement binary encode/decode methods. These convert between your message object and byte[].

    tip

    The parent class MessageContent provides encode/decode helpers for user (user info), mentionedInfo (mention data), and extra (extra info). Call the parent methods if your custom message needs these fields.

    • Override encode() to serialize the message object to byte[] (object → JSONObject → JSON string → byte[]).
    • Override the MessageContent(byte[] data) constructor to deserialize (byte[] → JSON string → JSONObject → object).
  5. Implement Parcel serialization. These methods support cross-process message transport. Use the SDK's ParcelUtils utility class.

    warning

    The read and write order in Parcel serialization must match exactly.

  6. Add getter and setter methods for your custom fields.

Example: Media custom message

tip

Unless you bypass the SDK's built-in upload logic, the message must include a localPath property (type: String?).

kotlin
@MessageTag(value = "app:mediacontent", flag = MessageTag.ISCOUNTED)
class MyMediaMessageContent : MediaMessageContent {

constructor(localUri: Uri) {
localPath = localUri.toString()
}

companion object {
@JvmStatic
fun obtain(localUri: Uri) = MyMediaMessageContent(localUri)

@JvmField
val CREATOR: Parcelable.Creator<MyMediaMessageContent> = object : Parcelable.Creator<MyMediaMessageContent> {
override fun createFromParcel(source: Parcel) = MyMediaMessageContent(source)
override fun newArray(size: Int) = arrayOfNulls<MyMediaMessageContent>(size)
}
}

override fun encode(): ByteArray {
val jsonObj = super.getBaseJsonObject()
try {
localPath?.let { jsonObj.put("localPath", it) }
} catch (e: JSONException) {
Log.e("JSONException", e.message)
}
return jsonObj.toString().toByteArray()
}

constructor(data: ByteArray) : super() {
val jsonStr = String(data)
try {
val jsonObj = JSONObject(jsonStr)
super.parseBaseJsonObject(jsonObj)
if (jsonObj.has("localPath")) {
localPath = jsonObj.optString("localPath")
}
} catch (e: JSONException) {
Log.e("JSONException", e.message)
}
}

override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToBaseInfoParcel(dest)
ParcelUtils.writeToParcel(dest, localPath)
}

constructor(parcel: Parcel) : super() {
super.readFromBaseInfoParcel(parcel)
localPath = ParcelUtils.readFromParcel(parcel)
}

override fun describeContents() = 0

fun getLocalUri(): Uri? = localPath?.let { Uri.parse(it) }
fun setLocalUri(localUri: Uri) { localPath = localUri.toString() }
}

Register custom messages

Register custom message types by calling registerCustomMessages before establishing the IM connection. Otherwise, unregistered types are handled as UnknownMessage.

tip

Register custom messages before connecting. Call this during your application lifecycle initialization.

kotlin
// In Application.onCreate(), after initialize but before connect
NCEngine.registerCustomMessages(listOf(
MyTextContent::class.java,
MyMessage::class.java
))

Parameters

ParameterTypeDescription
messageClassesList<Class<? extends MessageContent>>List of custom message classes. Each must extend MessageContent or MediaMessageContent.

Send custom messages

Send custom message types using the same methods as built-in types:

  • If the custom type extends MessageContent, use the standard message send method.
  • If the custom type extends MediaMessageContent, use the media message send method.

To enable push notifications for custom messages, provide a pushContent value when sending. Without it, offline recipients will not receive push notifications.

tip
  • Without pushContent, the server cannot trigger push notifications for custom messages.
  • Transient messages (MessageTag.STATUS) do not support push notifications.