Creating Custom Attachments on Android

In this tutorial, we’ll look at how to create and render custom attachments with the Stream Chat UI Components SDK for Android.

Messages in Stream Chat can contain a number of attachments. The UI Components library for Android renders these by default depending on their type. Images are rendered in a gallery layout, files are shown in a list, and links show rich previews of the content they're leading to.

You can render attachments in a custom way inside the same area within the message bubble. Here's the area filled with a custom attachment that's just a plain red box:

To determine when to do this, you can inspect the contents of each message item in the list, and decide whether you want to override the default attachments rendering for a given message.

Customizing attachments is the simpler of the two main customization options in the message list. The more complex approach is rendering an entire message by yourself - which is also possible with other APIs in the Stream Chat Android SDK. Those give you even more control, but you'll lose all of the default message UI (timestamps, avatars, read statuses, thread indicators, etc.) and have to build the entire message view from scratch.

Examples of three different message items in a list with various decoration

AttachmentViewFactory

Implementing custom attachments is done by extending the AttachmentViewFactory class from the SDK and overriding its createAttachmentView method. In general, you'll likely follow this kind of pattern for a custom attachment there:

1class CustomAttachmentViewFactory : AttachmentViewFactory() {
2    override fun createAttachmentView(
3        data: MessageListItem.MessageItem,
4        listeners: MessageListListenerContainer,
5        style: MessageListItemStyle,
6        parent: ViewGroup,
7    ): View {
8        return if (someCondition) {
9            createCustomView()
10        } else {
11            super.createAttachmentView(data, listeners, style, parent)
12        }
13    }
14}

You'll check some conditions based on the data received as a parameter, and use your own View creation logic when the condition is met. Otherwise, you can call the super method which will use the default rendering logic of the SDK.

You receive the entire Message object in data, so you could write code that handles multiple (or all) attachments on a given message. You could also add contents to the attachments area based on just the Message contents alone, possibly ignoring the attachments on it altogether.

When your factory is implemented, you can set it on MessageListView so that it will use your custom logic for rendering attachments, like so:

1messageListView.setAttachmentViewFactory(CustomAttachmentViewFactory())

Custom password attachment UI

For a real life use case, we'll create a special "password" attachment type which we can use for sending passwords in our messages. We won't deal with encryption this time around, but we'll make sure that the contents of the password attachment are hidden on the UI by default, to keep it safe from prying eyes.

First, let's create a layout file called attachment_password.xml:

1<?xml version="1.0" encoding="utf-8"?>
2<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
3    xmlns:app="http://schemas.android.com/apk/res-auto"
4    android:layout_width="match_parent"
5    android:layout_height="wrap_content"
6    android:layout_margin="8dp"
7    app:cardBackgroundColor="#FF4D56"
8    app:cardCornerRadius="12dp">
9
10    <TextView
11        android:id="@+id/passwordText"
12        android:layout_width="match_parent"
13        android:layout_height="wrap_content"
14        android:layout_gravity="center"
15        android:gravity="center"
16        android:inputType="textPassword"
17        android:textSize="30sp" />
18
19</com.google.android.material.card.MaterialCardView>

We'll use a MaterialCardView to create UI with rounded corners, matching the default styling of the Chat UX Kit. Inside that, we're adding a TextView to display the password, with the textPassword input type set by default, which hides the text content inside it.

Time to implement the factory to produce this layout when needed:

1class CustomAttachmentViewFactory : AttachmentViewFactory() {
2
3    override fun createAttachmentView(
4        data: MessageListItem.MessageItem,
5        listeners: MessageListListenerContainer,
6        style: MessageListItemStyle,
7        parent: ViewGroup,
8    ): View {
9        // 1
10        val passwordAttachment = data.message.attachments.find { it.type == "password" }
11        return if (passwordAttachment != null) {
12            // 2
13            createPasswordView(parent, passwordAttachment.extraData.get("password") as String)
14        } else {
15            // 3
16            super.createAttachmentView(data, listeners, style, parent)
17        }
18    }
19
20    private fun createPasswordView(parent: ViewGroup, password: String): View {
21        // 4
22        val binding = AttachmentPasswordBinding
23            .inflate(LayoutInflater.from(parent.context), parent, false)
24
25        // 5
26        binding.passwordText.text = password
27        binding.passwordText.setOnClickListener {
28            binding.passwordText.inputType =
29                binding.passwordText.inputType xor
30                    TYPE_TEXT_VARIATION_PASSWORD xor
31                        TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
32        }
33
34        return binding.root
35    }
36
37}

Let's review this code step-by-step:

  1. We're going through the attachments in the current message to determine whether it has an attachment with the "password" type. (Again, we could also handle all attachments on the Message if needed. We're making some assumptions about the structure of the message object here.)
  2. We're calling into our own method that will create the password UI.
  3. If we didn't find an attachment with our custom type, we fall back to the default implementation that will render link previews, media, and files.
  4. In our custom method, we inflate the XML layout using ViewBinding.
  5. We then populate this layout with data, and set up a listener so that clicking the password field will toggle between the regular and the visible password input types, hiding and showing the password.

Creating and sending custom attachments

All we have left to do is to send an attachment that contains a password to see our code in action. For this, we can use the following code to create an Attachment and add it to a Message. We're using the extraData map to add arbitrary info to the attachment, in this case, a password field that will contain the value we want to send.

1val attachment = Attachment(
2    type = "password",
3    extraData = mutableMapOf("password" to "12345"),
4)
5val message = Message(
6    cid = cid,
7    text = "This is the combination on my luggage",
8    attachments = mutableListOf(attachment),
9)
10ChatDomain.instance().useCases.sendMessage(message).enqueue(...)

The example above uses ChatDomain to send the constructed message, a class from our offline support library. Using this is the recommended way of sending messages if you're using offline support.

There are other ways of sending messages with the SDK, for example, by using ChatClient directly. Read more about Sending Messages in the documentation.

Custom attachments in action

Sending a custom attachment as described above will create a message like this on your UI now:

With the listener set up, you can tap on this View to toggle the visibility of the password:

Conclusion

That's all it takes to send and render custom attachments with Stream Chat's Android SDK! You can use this to easily add custom media, a map, product details, or whatever other content your app deals with to a message.

You can learn more about the Android SDK by checking out its GitHub repository (give it a ⭐️ while you're there), and by taking a look at the documentation. You can also go through the Android tutorial that shows you how to get started with the UI SDK.

Thanks for reading this article! Find us on Twitter @getstream_io or drop us feedback on GitHub if you have any questions.