UI theming for PubNub Chat Components for Android

PubNub Chat Components for Android rely on the Material Theme from Jetpack Compose that defines default values for your application color, typography, and shape attributes, allowing your app to have a consistent look and feel.

App theme

Compose libraries are used across our components, their default themes, and renderers that define how the components are drawn on the screen. Chat Provider applies the default themes to all components that make references to them. This happens at the start of every app built with PubNub Chat Components for Android.

Our Getting Started (GS) app applies the default Material Theme in the Theme.kt file:

MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
) {

ChatProvider(pubNub) {
content()
}
}

Light and dark themes

Light and dark themes, same as in Jetpack Compose, are defined in components by providing different pairs of colors (primary, secondary, etc.) that are used respectively in light and dark themes of your app (the so-called LightColorPalette and DarkColorPalette).

This is how the GS app uses both palettes to define the app theme:

@Composable
fun AppTheme(
pubNub: PubNub,
database: DefaultDatabase = Database.initialize(LocalContext.current),
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable() () -> Unit,
) {
val colors = if (darkTheme) DarkColorPalette
else LightColorPalette

Change light and dark palettes

To override these default color palettes in your app, simply provide different values for specific parameters in both light and dark color sets.

See overrides from the GS app in the Theme.kt file:

private val DarkColorPalette = darkColors(
primary = Light4,
primaryVariant = Amaranth,
secondary = Light4,
onPrimary = Amaranth,
)

private val LightColorPalette = lightColors(
primary = Light4,
primaryVariant = Amaranth,
onPrimary = Amaranth,
secondary = Light4,
onSecondary = Amaranth,
onSurface = DustyGray,
onBackground = MineShaft,
Variables

Specific values for variables like Light4 or Amaranth are defined in the Color.kt file in the GS app.

You can use the Material palette generator to help you choose the most appropriate light and dark sets of colors for your app.

Change shapes and typography

Similarly to the colors, you can override the default values for shapes and typography provided by the Material Theme. Our GS app differentiates the following types of shapes in the Shape.kt file:

val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

It also specifies the typography for long-form writing (body1) used in the GS app (Type.kt):

val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
)

Component themes

With Material Theme supporting the implementation of PubNub Chat Components for Android implementation, we defined how each of our components should look and behave. For this purpose, we created separate themes for each component and default renderers that specify how these components behave in a final app.

All these component themes are separate Android classes and have a unified naming convention of {ComponentName}Theme, like TypingIndicatorTheme.

Default themes and renderers

You can see all default component themes in specific component folders in the chat-components-android repository. Each component folder contains a theme file (like TypingIndicatorTheme.kt) and a renderer file under the renderer folder (like TypingIndicatorRenderer.kt with its default implementation specified in DefaultTypingIndicatorRenderer.kt).

Each component theme has a specified list of parameters it takes. These parameters define what the component looks like and only these parameters can be customized.

Just like Chat Provider handles the default app theme, component themes are applied by PubNub local theme providers, such as LocalTypingIndicatorTheme.

Let's analyze how these default component themes are composed based on the TypingIndicatorTheme class.

class TypingIndicatorTheme(
modifier: Modifier,
icon: IconTheme,
text: TextTheme,
)

As you can see, the TypingIndicatorTheme class takes the modifier, icon, and text parameters. Each of them has a hierarchical structure and references a variable that is specified in one common ThemeDefaults object in the ThemeDefaults.kt file in the repository.

See the TypingIndicatorTheme example and the composable function in which it is specified:

object ThemeDefaults {
...
@Composable
fun typingIndicator(
modifier: Modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
icon: IconTheme = IconThemeDefaults.icon(
tint = MaterialTheme.colors.primaryVariant.copy(
alpha = ContentAlpha.medium
)
),
text: TextTheme = TextThemeDefaults.text(
fontSize = 12.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.54f)
show all 18 lines

The typingIndicator function takes:

  • modifier which is a native Compose object that allows you to decorate your composable function and change its layout, behavior, and appearance by changing the given element's padding, width, or hight. Each theme for a given PubNub Chat Component for Android contains a modifier and allows you to customize the component to your needs. In this example, the typingIndicator function uses the Compose modifier to set the maximum width of the typing indicator and change the padding to 12 dp.

  • icon references IconTheme and overrides the IconThemeDefaults theme for an icon. Apart from default themes for components, we also provide the default themes for common objects used across multiple components, such as buttons, icons, shapes, or texts. See all of them defined in the common folder. In this example, the typingIndicator function uses the IconThemeDefaults theme for the icon but overrides the importance hierarchy of the Material Theme primary color for the tint to medium using the ContentAlpha object from Jetpack Compose.

  • text, similarly to icon, references and overrides our in-house default theme. In this example, it specifies that the typingIndicator must use TextThemeDefaults with fontSize of 12.sp and Material Theme onSurface color at 54% opacity.

PubNub themes

All in-house PubNub themes for components and common elements have a unified naming convention and end with the Theme suffix, like TextTheme.

Customize component theme

Now that you know how these component and element themes work in PubNub Chat Components for Android, see how you can override the default values they provide in your app.

Customization allows you to change such parameters as the position on the screen (through modifiers), colors, text sizes, and icons. To customize the look of a component, create a custom theme for the component in which you override one or a few default parameters specified for the component in the ThemeDefaults.kt file. You then need to pass the new custom theme to the {ComponentName}Theme function, for example ChannelListTheme, using the Android native helper called CompositionLocalProvider. To propagate the new style onto all child elements, call a specific PubNub local provider for the theme (in this example it would be LocalChannelListTheme).

You can do the same to override the default values for the default element themes and customize values for such elements as buttons, icons, or shapes. See the common folder for the list of all available themes.

Change message list width

The ThemeDefaults object specifies that the message list is drawn at full width through the modifier value of Modifier = Modifier.fillMaxWidth():

object ThemeDefaults {
...
@Composable
fun messageList(
modifier: Modifier = Modifier.fillMaxWidth(),
arrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp),
message: MessageTheme = message(),
messageOwn: MessageTheme = message(
title = TextThemeDefaults.messageTitle(MaterialTheme.colors.primary),
shape = ShapeThemeDefaults.messageBackground(
color = MaterialTheme.colors.primary.copy(
alpha = 0.4f
)
)
),
show all 18 lines

To override the default maximum width of the message list in your app, create a custom theme in which you change the modifier value to 200.dp:

val customTheme = ThemeDefaults.messageList(modifier = Modifier.width(200.dp))
MessageListTheme(customTheme) {
MessageList(messages)
}

Pass the new value to the MessageListTheme composable function using the CompositionLocalProvider helper and the LocalMessageListTheme theme provider:

@Composable
fun MessageListTheme(
theme: MessageListTheme,
content: @Composable() () -> Unit,
) {
CompositionLocalProvider(LocalMessageListTheme provides theme) {
content()
}
}

Change input shape

The InputThemeDefaults object specifies that the shape of the input box (shape) uses the default medium shape provided by the Material Theme:

object InputThemeDefaults {
@Composable
fun input(
shape: Shape = MaterialTheme.shapes.medium,
colors: TextFieldColors = TextFieldDefaults.textFieldColors(
textColor = MaterialTheme.colors.contentColorFor(MaterialTheme.colors.background),
),
) = InputTheme(shape, colors)
}

Override the shape of the input box to apply 50% to all four corners:

val customTheme = ThemeDefaults.input(shape = RoundedCornerShape(50))
MessageInputTheme(customTheme) {
MessageInput()
}

Pass the new value to the helper:

@Composable
fun MessageInputTheme(
theme: MessageInputTheme,
content: @Composable() () -> Unit,
) {
CompositionLocalProvider(LocalMessageInputTheme provides theme) {
content()
}
}

Change message title color

The ThemeDefaults object specifies that the title of your own message (messageOwn) sent in a channel should use the default color provided by the Material Theme:

object ThemeDefaults {
...
@Composable
fun messageList(
modifier: Modifier = Modifier.fillMaxWidth(),
arrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp),
message: MessageTheme = message(),
messageOwn: MessageTheme = message(
title = TextThemeDefaults.messageTitle(MaterialTheme.colors.primary),
shape = ShapeThemeDefaults.messageBackground(
color = MaterialTheme.colors.primary.copy(
alpha = 0.4f
)
)
),
show all 17 lines

Change the default title color to red:

val customTheme = ThemeDefaults.messageList(messageOwn = message(title = messageTitle(Color.Red))
)
MessageListTheme(customTheme) {
MessageList(messages)
}

Pass the new value to the helper:

@Composable
fun MessageListTheme(
theme: MessageListTheme,
content: @Composable() () -> Unit,
) {
CompositionLocalProvider(LocalMessageListTheme provides theme) {
content()
}
}

Custom renderers

Each component has a default renderer that specifies how the component should behave and be drawn in an app built with PubNub Chat Components for Android. All component renderers, together with their default implementations, are defined in a given component folder under the renderer subfolder.

  • ChannelList

    The default implementation of ChannelRenderer is available in the DefaultChannelRenderer.kt file. You can implement the following functions:

    FunctionDefault valueDescription
    Channel(name: String, description: String?, modifier: Modifier, profileUrl: String?, onClick: (() -> Unit)?, onLeave: (() -> Unit)?n/aA composable function to draw a channel item.
    Placeholder()n/aA composable placeholder used during data loading.
    Separator(title: String?, onClick: (() -> Unit)?)n/aA composable function to draw a separator for channel types.
  • MemberList

    The default implementation of MemberRenderer is available in the DefaultMemberRenderer.kt file. You can implement the following functions:

    FunctionDefault valueDescription
    Member(name: String, description: String?, profileUrl: String, online: Boolean?, onClick: () -> Unit, modifier: Modifier)n/aA composable function to draw a member item.
    Separator(title: String)n/aA composable function to draw a separator for members.
    Placeholder()n/aA composable placeholder used during data loading.
  • MessageList

    The default implementation of MessageRenderer is available in the GroupMessageRenderer.kt file. You can implement the following functions:

    FunctionDefault valueDescription
    Message(messageId: MessageId, currentUserId: UserId, userId: UserId, profileUrl: String, online: Boolean?, title: String, message: AnnotatedString?, timetoken: Timetoken, reactions: List<ReactionUi>, onMessageSelected: (() -> Unit)?, onReactionSelected: ((Reaction) -> Unit)?, reactionsPickerRenderer: ReactionsRenderern/aA composable function to draw a message item.
    Placeholder()n/aA composable placeholder used during data loading.
    Separator(text: String)n/aA composable function to draw a separator for members.
  • Message reactions (part of MessageList)

    The default implementation of ReactionsRenderer is available in the DefaultReactionsPickerRenderer.kt file. You can implement the following functions:

    FunctionDefault valueDescription
    fun Picker(onSelected: (Reaction) -> Unit)n/aComposable function to draw a reactions picker.
    fun PickedList(currentUserId: UserId, reactions: List<ReactionUi>, onSelected: (Reaction) -> Unit)n/aComposable function to draw all the reactions under the message.
  • MessageInput

    The default implementation of TypingIndicatorRenderer is available in the DefaultTypingIndicatorRenderer.kt file. You can implement the following functions:

    FunctionDefault valueDescription
    TypingIndicator(data: List<TypingUi>)n/aA composable function to draw the typing indicator.

    The TypingUi data contains information about the user, typing state, and the last signal timestamp.

    data class TypingUi(
    val user: MemberUi.Data,
    val isTyping: Boolean,
    val timestamp: Long = System.currentTimeMillis().timetoken,
    )

Create a custom renderer

If you need to change the structure of the view, you can create a custom renderer that extends the corresponding component's interface and pass it using the {componentName}Renderer parameter. Each component has different customization options. Check the renderer interfaces and default implementations to see the available data and functions you can customize to your app needs.

For example, if you want to create a custom renderer for TypingIndicator that is a part of the MessageInput component, create a separate file, like MyCustomTypingIndicatorRenderer.kt, with a new configuration for a custom MyCustomTypingIndicatorRenderer and pass it through the typingIndicatorContent parameter when invoking the MessageInput component in your chat app:

MessageInput(

typingIndicatorContent = { typing ->
MyCustomTypingIndicatorRenderer.TypingIndicator(typing)
},

)

Other customization options

If you use PubNub Chat Components to add chat functionality to your existing application, you would need to create the chat view in your app and customize its look to your needs. To add additional files, like images or icons, and static content to your Android project, import them in Android Studio to the respective subfolder of the res directory. This way all referenced resources will be kept externally and only referenced in your app code when needed.

Add chat string reference

For example, you can add a common Chat string value to the values/strings.xml file and reference it in your app when adding a new view for your chat app:

<resources>
<string name="chat">Chat</string>
</resources>

Add chat icon image

You can also add a chat icon to be displayed in the app's bottom navigation menu. To do that, select an SVG image and import it into your Android Studio project as a drawable resource. Android Studio will convert it into an XML file. Read the official Android documentation to learn how to do that.