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.