๐ Build a Modern Cricket Score App
✨ Introduction
If you love both cricket and Android development, this tutorial is the perfect match! In this post, we’ll build a modern live cricket score app using Jetpack Compose and CricAPI — a free and easy-to-use API that provides real-time cricket data. The best part? We’ll do it without OkHttp, making the setup beginner-friendly and efficient.
๐งฐ What You’ll Learn
- Connect Jetpack Compose app with a real cricket API
- Fetch live data such as teams, scores, and venues
- Build beautiful UI components with Compose
- Implement navigation and a details screen
⚙️ Step 1: Project Setup
Create a new Jetpack Compose project in Android Studio (Arctic Fox or newer).
Add dependencies in build.gradle
implementation("androidx.navigation:navigation-compose:2.8.0")
implementation("io.coil-kt:coil-compose:2.4.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
CodingBihar
— Explore Expert-Written Tutorials, Projects, and Tools to Master App Development —
๐ Step 2: Fetch Data from CricAPI
Use this endpoint to get live match data:
https://api.cricapi.com/v1/currentMatches?apikey=YOUR_API_KEY&offset=0
Data classes for API response
data class MatchResponse(val data: List<Match>)
data class Match(
val id: String,
val name: String,
val status: String,
val venue: String,
val teamInfo: List<TeamInfo>,
val score: List<Score>?
)
data class TeamInfo(val name: String, val shortname: String, val img: String)
data class Score(val r: Int, val w: Int, val o: Float, val inning: String)
๐ Step 3: Create a Retrofit API Service
interface CricketApiService {
@GET("v1/currentMatches")
suspend fun getMatches(
@Query("apikey") apiKey: String,
@Query("offset") offset: Int = 0
): MatchResponse
}
๐จ Step 4: Build the UI with Jetpack Compose
@Composable
fun MatchCard(match: Match, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { onClick() },
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(Modifier.padding(12.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
match.teamInfo.forEach { team ->
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Image(
painter = rememberAsyncImagePainter(team.img),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
Text(team.shortname, fontWeight = FontWeight.Bold)
}
}
}
Spacer(modifier = Modifier.height(6.dp))
Text(text = match.name, fontWeight = FontWeight.SemiBold)
Text(text = match.status, style = MaterialTheme.typography.bodySmall)
match.score?.forEach {
Text("${it.inning}: ${it.r}/${it.w} (${it.o} overs)")
}
}
}
}
๐ฑ Step 5: Add a Details Screen
@Composable
fun MatchDetailsScreen(match: Match) {
Column(modifier = Modifier.padding(16.dp)) {
Text(match.name, fontWeight = FontWeight.Bold, fontSize = 22.sp)
Spacer(modifier = Modifier.height(8.dp))
Text("Venue: ${match.venue}")
Text("Status: ${match.status}")
Spacer(modifier = Modifier.height(12.dp))
match.score?.forEach {
Text("${it.inning} — ${it.r}/${it.w} in ${it.o} overs")
}
}
}
๐ Step 6: Optional — Show Data in Hindi
CricAPI data is in English, but you can translate your app’s UI labels using
stringResource() and Hindi strings.xml.
<string name="venue">เคฎैเคฆाเคจ</string>
<string name="status">เคธ्เคฅिเคคि</string>
Then use it in Compose:
Text(text = stringResource(R.string.venue) + ": " + match.venue)
How I built My Final Code is Below
MainActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
EQPlayerTheme {
val context = LocalContext.current
/*Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/
CricketAppNav()
}
}
}
}
@Composable
fun CricketAppNav(viewModel: MatchViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "match_list") {
// ๐ List Screen
composable("match_list") {
MatchListScreen(
viewModel = viewModel,
onMatchClick = { match ->
val matchJson = java.net.URLEncoder.encode(Gson().toJson(match), "UTF-8")
navController.navigate("match_detail/$matchJson")
}
)
}
// ๐ Details Screen
composable(
"match_detail/{match}",
arguments = listOf(navArgument("match") { type = NavType.StringType })
) { backStackEntry ->
val matchJson = backStackEntry.arguments?.getString("match") ?: ""
val match = Gson().fromJson(
java.net.URLDecoder.decode(matchJson, "UTF-8"),
Match::class.java
)
MatchDetailsScreen(match = match, onBack = { navController.popBackStack() })
}
}
}
RetrofitApi
package com.codingbihar.cricketbuzz
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
data class MatchResponse(
val data: List?,
val status: String?
)
data class Match(
val id: String,
val name: String,
val matchType: String,
val status: String,
val venue: String,
val date: String,
val teams: List,
val teamInfo: List,
val score: List?
)
data class TeamInfo(
val name: String,
val shortname: String,
val img: String
)
data class Score(
val r: Int,
val w: Int,
val o: Float,
val inning: String
)
interface CricApiService {
@GET("v1/currentMatches")
suspend fun getCurrentMatches(
@Query("apikey") apiKey: String,
@Query("offset") offset: Int = 0
): MatchResponse
companion object {
private const val BASE_URL = "https://api.cricapi.com/"
fun create(): CricApiService {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CricApiService::class.java)
}
}
}
MatchViewModel
package com.codingbihar.cricketbuzz
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class MatchViewModel : ViewModel() {
private val _matches = MutableStateFlow>(emptyList())
val matches: StateFlow> = _matches
private val api = CricApiService.create()
fun fetchMatches() {
viewModelScope.launch {
try {
val response = api.getCurrentMatches("01d061b5-3563-42f5-b42f-0e3ghg143814")
_matches.value = response.data ?: emptyList()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
CricketScreen
package com.codingbihar.cricketbuzz
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
@Composable
fun MatchListScreen(
viewModel: MatchViewModel,
onMatchClick: (Match) -> Unit
) {
Column(Modifier.fillMaxSize().systemBarsPadding()) {
val matches by viewModel.matches.collectAsState()
LaunchedEffect(Unit) {
viewModel.fetchMatches()
}
Row (Modifier.fillMaxWidth()){
Text("\uD83C\uDFCF", style = MaterialTheme.typography.displayMedium)
Text(text = stringResource(R.string.app_name), style = MaterialTheme.typography.displayMedium)
}
LazyColumn(
modifier = Modifier.padding(8.dp)
) {
items(matches) { match ->
MatchCard(match = match, onClick = { onMatchClick(match) })
}
}
}
}
@Composable
fun MatchCard(
match: Match,
onClick: (Match) -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
.clickable { onClick(match) },
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(12.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
match.teamInfo.forEach { team ->
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Image(
painter = rememberAsyncImagePainter(team.img),
contentDescription = team.name,
modifier = Modifier.size(48.dp),
contentScale = ContentScale.Crop
)
Text(team.shortname, fontWeight = FontWeight.Bold)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(text = match.name, fontWeight = FontWeight.SemiBold)
Text(text = match.status, style = MaterialTheme.typography.bodySmall)
Text(text = "Venue: ${match.venue}", style = MaterialTheme.typography.bodySmall)
match.score?.forEach {
Text("${it.inning}: ${it.r}/${it.w} (${it.o} overs)")
}
}
}
}
@Composable
fun MatchDetailsScreen(
match: Match,
onBack: () -> Unit
) {
Column(
modifier = Modifier.systemBarsPadding()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
match.teamInfo.forEach { team ->
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Image(
painter = rememberAsyncImagePainter(team.img),
contentDescription = team.name,
modifier = Modifier.size(72.dp)
)
Text(team.name, fontWeight = FontWeight.Bold)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Text("๐ Venue: ${match.venue}", style = MaterialTheme.typography.bodyMedium)
Text("๐
Date: ${match.date}", style = MaterialTheme.typography.bodyMedium)
Text("⚡ Status: ${match.status}", style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider()
Text(
"๐ Scorecard",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(vertical = 8.dp)
)
match.score?.forEach { score ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
shape = RoundedCornerShape(10.dp)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(text = score.inning, fontWeight = FontWeight.SemiBold)
Text(text = "Runs: ${score.r}, Wickets: ${score.w}, Overs: ${score.o}")
}
}
}
}
}
Output:

๐ Conclusion
Congratulations ๐! You’ve just built a live cricket score app with Jetpack Compose and CricAPI — clean, fast, and modern. Try adding player details, Hindi toggle, or live refresh to enhance your project further.
๐ฌ FAQs
Q1. Is CricAPI free?
Yes, it offers a free plan with limited daily API calls.
Q2. Can I use Hindi or other languages?
You can translate UI text; the API data remains in English.
Q3. How can I auto-refresh the scores?
Use LaunchedEffect with delay() in Compose to refetch every 30 seconds.
