Building an Inbox with Jetpack Compose

Helder Pinhal
Jun 26 2020
Posted in Engineering & Technology

Taking Jetpack Compose for a test-drive

In 2019’s AndroidDevSummit, Google announced Jetpack Compose as Developer Preview. Much like Apple’s SwiftUI, Compose is a declarative UI framework.

With Kotlin taking over the Android world, the Android team has been (re)building plenty of libraries to help developers write less, cleaner, more performant code. One of them being Compose, its goal is to allow for faster, more frequent releases of the UI foundations opposed to the current situation — with every new Android release.

At the time of writing, Compose has just released its 0.1.0-dev14 version. Because it is still under developer preview, the API surface is prone to breaking changes and various bugs. Our recommendation is not to use Compose in a production app but once it comes to the stable channel it will definitely take over on how we build user interfaces for Android.

For the time being, let’s play with it and build an inbox similar to the one we built with SwiftUI.

Getting started

You can download the boilerplate project from the repo. Alternatively, the final result is available on the master branch.

This already includes a basic integration of the Notificare SDK. You will need a Notificare application, update notificareconfig.properties, a Google Maps API key placed under local.properties and a FCM's google-services.json.

For detailed instructions you can follow our documentation.

Composing the Inbox

Below is a quick preview of the view we intend to create for an inbox item. It’s composed of three sections — an attachment image with a colour indicator, the actual content (title & message), the date when the notification has been received plus an indicator for its read/unread state.

To create the attachment section, we’ll leverage the boilerplate utility loadImageUrl to asynchronously load the attachment image.

@Composable
private fun AttachmentIndicator(item: NotificareInboxItem) {
    when (val state = item.attachment?.uri?.let { loadImageUrl(url = it) }) {
        null -> {
            Box(
                modifier = Modifier
                    .size(width = 4.dp, height = 48.dp)
                    .gravity(Alignment.CenterVertically),
                backgroundColor = item.color,
                shape = RoundedCornerShape(4.dp)
            )
        }
        is UiState.Loading -> {
            Box(
                modifier = Modifier.size(width = 80.dp, height = 60.dp),
                shape = RoundedCornerShape(4.dp),
                border = Border(2.dp, item.color)
            )
        }
        is UiState.Success -> {
            Box(
                modifier = Modifier.size(width = 80.dp, height = 60.dp),
                shape = RoundedCornerShape(4.dp),
                border = Border(2.dp, item.color)
            ) {
                Image(
                    asset = state.data.asImageAsset(),
                    modifier = Modifier.fillMaxSize(),
                    contentScale = ContentScale.Crop
                )
            }
        }
        is UiState.Error -> {
            Box(
                modifier = Modifier.size(width = 80.dp, height = 60.dp),
                shape = RoundedCornerShape(4.dp),
                border = Border(2.dp, item.color),
                gravity = Alignment.Center
            ) {
                Image(
                    asset = vectorResource(R.drawable.ic_baseline_error_24),
                    colorFilter = ColorFilter.tint(MaterialTheme.colors.error)
                )
            }
        }
    }
}

For the actual inbox item view, there’s not much going on. It’s a very straightforward composable function.

@Composable
private fun InboxItem(
    item: NotificareInboxItem
) {
    val context = ContextAmbient.current

    Row(
        modifier = Modifier
            .clickable(onClick = {
                Notificare.shared().openInboxItem(context as Activity, item)
            })
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        AttachmentSection(item)

        Column(
            modifier = Modifier
                .weight(1f)
                .padding(horizontal = 16.dp)
        ) {
            Text(
                text = item.title ?: "---",
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                style = MaterialTheme.typography.body2.copy(
                    fontWeight = FontWeight.SemiBold
                )
            )

            ProvideEmphasis(emphasis = EmphasisAmbient.current.medium) {
                Text(
                    text = item.message,
                    maxLines = 2,
                    overflow = TextOverflow.Ellipsis,
                    style = MaterialTheme.typography.body2
                )
            }
        }

        Column(
            horizontalGravity = Alignment.End
        ) {
            ProvideEmphasis(emphasis = EmphasisAmbient.current.medium) {
                Text(
                    text = item.timestamp.asTimeAgo(),
                    style = MaterialTheme.typography.caption,
                    maxLines = 1,
                    modifier = Modifier.padding(bottom = 8.dp)
                )
            }

            if (!item.status) {
                Box(
                    modifier = Modifier.size(8.dp),
                    shape = CircleShape,
                    backgroundColor = Color(0, 122, 255)
                )
            }
        }
    }
}

Lastly, the inbox view itself is just a list of inbox items. If there are no items, we present an empty message otherwise we present the items themselves. In a typical Android app we would be using a RecyclerView to achieve this. With Compose we have LazyColumnItems.

Since Compose is all about composable, reactive UI, we can leverage LiveData and its extension observeAsState to seamlessly create a reactive inbox view.

@Composable
fun Inbox(liveData: LiveData<SortedSet<NotificareInboxItem>>) {
    val state = liveData.observeAsState()
    val inboxItems = state.value

    when {
        inboxItems?.isEmpty() == true -> {
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalGravity = Alignment.CenterHorizontally
            ) {
                Text(
                    text = "No messages found",
                    style = MaterialTheme.typography.caption
                )
            }
        }
        inboxItems?.isNotEmpty() == true -> {
            LazyColumnItems(items = inboxItems.toList()) {
                InboxItem(item = it)
                Divider()
            }
        }
    }
}

And to tie things together, let’s update our MainActivity to render the inbox. Replace the existing setContent block with the following.

setContent {
    InboxTheme {
        Inbox(Notificare.shared().inboxManager.observableItems)
    }
}

Conclusion

Jetpack Compose really accelerates UI development on Android. Despite still being in its infancy, we can already see how it will impact development in the upcoming years. We'll keep updating the repo with our latest experiments. Stay tuned!

As always, we are available via our Support Channel if you have any questions.

Keep up-to-date with the latest news