Creating a Live Stream Screen Using Jetpack Compose
Creating a live-streaming app
TL;DR
Just a demo application showing how to build different components of a Live Stream Screen using Jetpack Compose. I have used a demo screen for now and discussed how we can make a live-streaming application.

At Eloelo, we don’t use Jetpack Compose as we migrated from React Native to Native Android with Kotlin around a year ago, but being a person not bound by any programming language, I just love to explore. Jetpack Compose came in very handy as I’m familiar with writing Dart in the Flutter SDK; it has the same kind of widget structures as the composables here.
Below is a wireframe of the live-streaming screen I designed. The live streaming screen we are creating is based on this design, and all the components are listed step by step with code, along with some resources.

Let’s Get Started
We are not going into ‘Start with Jetpack Compose’ stuff. There are a lot of videos and blogs about that. We will look into the components for the live stream, and at last, enjoy the surprise.
1. Live header
Starting with the live header, the code snippet is below, but we should understand how things are working.
We need a Row
to arrange the profile avatar, name, following details, and a close live button. The verticalAlignment
needs to be vertically centered and a row will have the avatar, name, and following details again. Other things like padding, width, and sizes are clear.
Note: We could integrate this with a live endpoint, something like /get/liveDetails
, and have things render from the server side. I will talk about those things in future articles.
Here’s our first code snippet:
@Composable
fun LiveHeader() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.live_profile),
contentDescription = "live profile",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.border(2.dp, Color.White, CircleShape)
)
Column(
modifier = Modifier.padding(start = 8.dp)
) {
Text(text = "Nishchal Raj", fontSize = 14.sp, color = Color.White)
Text(text = "Following", fontSize = 12.sp, color = Color.White)
}
}
Image(
Icons.Filled.Close,
contentDescription = "",
colorFilter = ColorFilter.tint(Color.White)
)
}
}
Below is an example of how the follow button can work. We can have a local variable, isFollowClicked
, that will handle the follow UI modifications, and when clicked, call an endpoint that can also be inserted in comments or become anything else.

Text(
text = if (isFollowClicked) {"Following"} else { "+ Follow" }, fontSize = 12.sp, color = Color.White,
modifier = if (isFollowClicked) Modifier else {
Modifier
.padding(vertical = 2.dp)
.border(
width = 1.dp,
color = Color.White,
shape = RoundedCornerShape(24.dp)
)
.padding(horizontal = 4.dp)
.clickable {
isFollowClicked = true
}
},
)
2. Live indicator
The thing is, Jetpack Compose is self-explanatory, so the words written in the code snippet can be easily understood just by looking at them.
In this part, we created a Column
that will have items arranged at the end. The indicators for the live can be different composables that will make it clean, and like the above data, they can be fetched from a socket connection because this is the real-time data of the ongoing live stream.
@Composable
fun LiveIndicator() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.End
) {
ViewersIndicator()
GiftsIndicator()
CoinsIndicator()
FollowersIndicator()
}
}
@Composable
private fun ViewersIndicator() {
Row(
modifier = Modifier
.padding(vertical = 2.dp)
.background(
Color.LightGray, shape = RoundedCornerShape(
topStart = 24.dp, bottomStart = 24.dp
)
),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier
.size(24.dp)
.padding(vertical = 4.dp),
painter = painterResource(id = R.drawable.ic_baseline_remove_red_eye_24),
contentDescription = "viewers watching"
)
Text(
modifier = Modifier
.padding(top = 2.dp, bottom = 2.dp, start = 8.dp, end = 10.dp),
text = "2M",
fontSize = 12.sp,
color = Color.Black,
)
} // viewers watching
}
@Composable
private fun CoinsIndicator() {
Row(
modifier = Modifier
.padding(vertical = 2.dp)
.background(
Color.LightGray, shape = RoundedCornerShape(
topStart = 24.dp, bottomStart = 24.dp
)
),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(24.dp).padding(vertical = 4.dp),
painter = painterResource(id = R.drawable.baseline_monetization_on_24),
contentDescription = "coins earned",
)
Text(
modifier = Modifier
.padding(top = 2.dp, bottom = 2.dp, start = 8.dp, end = 10.dp),
text = "13M",
fontSize = 12.sp,
color = Color.Black
)
} // coins earned
}
@Composable
private fun GiftsIndicator() {
Row(
modifier = Modifier
.padding(vertical = 2.dp)
.background(
Color.LightGray, shape = RoundedCornerShape(
topStart = 24.dp, bottomStart = 24.dp
)
),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier
.size(24.dp)
.padding(vertical = 4.dp),
painter = painterResource(id = R.drawable.baseline_card_giftcard_24),
contentDescription = "gifts received"
)
Text(
modifier = Modifier
.padding(top = 2.dp, bottom = 2.dp, start = 8.dp, end = 10.dp),
text = "1M",
fontSize = 12.sp,
color = Color.Black
)
} // gifts got
}
@Composable
private fun FollowersIndicator() {
Row(
modifier = Modifier
.padding(vertical = 2.dp)
.background(
Color.LightGray, shape = RoundedCornerShape(
topStart = 24.dp, bottomStart = 24.dp
)
),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(24.dp).padding(vertical = 4.dp),
painter = painterResource(id = R.drawable.baseline_groups_2_24),
contentDescription = "followers gained"
)
Text(
modifier = Modifier
.padding(top = 2.dp, bottom = 2.dp, start = 8.dp, end = 10.dp),
text = "200K",
fontSize = 12.sp,
color = Color.Black
)
} // followers gained
}
3. Live comment recycler
In Jetpack Compose, we use LazyColumn
and LazyRow
to create recycler views. Because we have a vertical view of the comments, we will use LazyColumn
to modify the height as needed (here, we’ll divide the height by 5). Item
s will have the data coming from the socket and render them into a composable with their respective padding and background.
A thing to note here is there are two types of padding: the top one is for the view, and the latter is for the internal view. The item
contains the commenter, name, and message avatars.
@Composable
fun LiveComments() {
LazyColumn(
modifier = Modifier.height(
(LocalConfiguration.current.screenHeightDp/5).dp
)
) {
items(SampleData.commentsData) { comment ->
CommentsItem(comment)
}
}
}
@Composable
private fun CommentsItem(comment: CommentsData) {
Row(
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 2.dp)
.background(Color.LightGray, shape = RoundedCornerShape(16.dp))
.padding(start = 4.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
verticalAlignment = Alignment.Top
) {
Image(
painter = painterResource(id = comment.avatar),
contentDescription = "avatar",
modifier = Modifier
.size(16.dp)
.border(color = Color.White, width = 1.dp, shape = CircleShape)
.padding(2.dp)
)
Text(
text = "${comment.name}:",
color = Color.White,
modifier = Modifier.padding(horizontal = 4.dp),
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold
)
Text(text = comment.comment, color = Color.White, fontSize = 12.sp,)
}
}
Each item
can contain numerous properties as per the use case, such as a single-click to open the commenter's profile, a long-click to react to an emoji on a comment, or a swipe left or right to reply to the comment. We will talk about these features later in this article.
4. Live gifting recycler
Unlike comment recycler, we have LazyRow
for the gifts. item
holds gift
’s image and the coin needed for the gift
.
@Composable
fun LiveGifts() {
LazyRow {
items(SampleData.giftsData) { gift ->
GiftItem(gift)
}
}
}
@Composable
private fun GiftItem(gift: GiftsData) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(vertical = 8.dp, horizontal = 6.dp)
) {
Image(
painter = painterResource(id = gift.giftImage),
contentDescription = gift.giftName,
modifier = Modifier.size(32.dp),
colorFilter = ColorFilter.tint(Color.White)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = gift.giftValue,
fontSize = 12.sp,
color = Color.White
)
}
}
By adding clicks to these items, we can send them to the host and present them as an animation. From the server side, we can change the user, host coins, or gems.
5. Live comment box
Now, we have an interesting composable. We have a TextField
that takes a value when the value is changed. We modified it to have rounded corners and a trailing send icon with placeholder text.
This is straightforward.
@Composable
fun LiveCommentBox() {
var text by remember { mutableStateOf(TextFieldValue("")) }
TextField(
value = text,
onValueChange = { newText ->
text = newText
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
shape = RoundedCornerShape(32.dp),
trailingIcon = { Icon(Icons.Filled.Send, "", tint = Color.Black) },
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.LightGray,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
placeholder = {
Text(text = "Type a comment")
},
)
}
To send a comment into the socket after the user clicks on the send icon, we need to add a modifier for the clickable and add a function that will send data into the socket. The observer for the comments will get the comment on other users’ side, and locally, it can be directly added to the comment list.
modifier = Modifier.clickable {
sendCommentToSocket(text.text)
}
After the send icon is clicked, the text can be cleaned too.
6. Live screen running
After summing up everything in the onCreate
of our activity, we will have the scaffold inside our theme.
setContent {
JetpackComposeTutorialTheme {
Scaffold {
setUpLiveLayout()
}
}
}
Inside the theme, we have a composable
function that will create a Box
which has the live stream running and all other elements of the live screen.
@Composable
private fun setUpLiveLayout() {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
) {
Image(
painter = painterResource(id = R.drawable.live_studio),
contentDescription = "",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
Column(
modifier = Modifier.fillMaxHeight(),
Arrangement.SpaceBetween
) {
Column {
LiveHeader()
LiveIndicator()
}
Column {
LiveComments()
LiveGifts()
LiveCommentBox()
}
}
}
}
Now, to run the live stream in an application, we have many options, such as the following:
As I’ve been in the live streaming field for the last two and a half years, I have explored WebRTC, Jitsi, AntMedia, Amazon IVS, Google Live Stream API, and mostly Agora. At Eloelo, we use the Agora SDK for our use case because we have customized numerous things in it, and it fits our needs better than others. When I first started in Agora, I created something like Instagram Live for a client in Germany.
This article explains the process and can be useful if you are not using Jetpack Compose.
Here is a blog by Meherdeep Thakur (an engineer at Agora) explaining how you can use the Agora SDK with Jetpack Compose and bring back your live stream. In the code snippets above, we have not discussed how the live stream will run, but here’s a clarification on how we can integrate live stream inside an app with Jetpack Compose. The same blog can be found in Agora’s blog section, so it can be read too.
Now, here’s the surprise! We can complete it together and make it a full-fledged live-streaming app. The following GitHub repository has all the code discussed in this article. All contributions are welcome, and more can be found in the CONTRIBUTING.md
file.
To start, you can make the six components discussed fully functional. I really appreciate you’ve read this long article. Thanks for coming so far.
Happy composing and streaming!