
Build Stunning Grids in Minutes with LazyVerticalGrid | ๐ ๐ ๐ ๐ |
Build Stunning Grids in Minutes with LazyVerticalGrid | ๐ ๐ ๐ ๐ | ๊ด๋ จ


LazyVerticalGrid
in action in the NHL Hockey app on Google Play.Want to create stunning grid layouts in your Jetpack Compose app? Look no further thanLazyVerticalGrid
. This powerful toolsimplifiesthe process of designing and implementing efficient grid-based interfaces. In this comprehensive tutorial, Iโll share my insights and experience usingLazyVerticalGrid
in a real-worldproductionapp on Google Play. Iโll explore its key features, best practices, and practical tips to help you create stunning grids that captivate your users. ๐ค
To populate the grid with player data, I make a network call to retrieve information for the selected season. Hereโs how I have implemented that:
// Wrapper for state management
sealed class PlayersUiState {
data object Loading : PlayersUiState()
data class Success(val players: List<Player>) : PlayersUiState()
data class Error(val throwable: Throwable) : PlayersUiState()
}
private val _uiState = MutableStateFlow<PlayersUiState>(PlayersUiState.Loading)
val uiState: StateFlow<PlayersUiState> = _uiState.asStateFlow()
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
viewModelScope.launch {
_uiState.emit(PlayersUiState.Error(throwable = throwable))
}
}
suspend fun getSkatersAndGoalies(season: String) {
viewModelScope.launch(context = ioDispatcher + coroutineExceptionHandler) {
repository.getAllNhlPlayers(season)
.catch { e ->
_uiState.emit(PlayersUiState.Error(Throwable(e.message ?: "Unknown error")))
}
.collectLatest { players ->
val sortedPlayers = (players.forwards + players.defensemen + players.goalies).sortedBy { it.lastName.default }
_uiState.emit(PlayersUiState.Success(sortedPlayers))
}
}
}
- **Fetch player data:**Use
repository.getAllNhlPlayers(season)
to retrieve player data for the specified season. - **Handle errors:**Catch any exceptions that might occur during the network call and emit an error state to the UI.
- **Sort players:**Combine the forwards, defensemen, and goalies, then sort them by last name.
- **Emit success:**Emit a success state to the UI, including the sorted players and the transformed season string.
UI Composable
**Now I bring the state to life by connecting it to the UI components.**Hereโs how Iโve implemented it:
@Composable
fun ShowLazyVerticalGridPlayers(uiState: PlayersUiState.Success, navController: NavController) {
val players = uiState.players
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
val isCollapsed by remember { derivedStateOf { scrollBehavior.state.collapsedFraction == 1f } }
val title = if (!isCollapsed) "ALL NHL\\nPLAYERS" else "PLAYERS"
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
ParallaxToolBarV2(
scrollBehavior = scrollBehavior,
navController = navController,
title = title,
color = DefaultBlack,
actions = {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(DefaultNhlTeam.teamLogo)
.decoderFactory(SvgDecoder.Factory())
.crossfade(true)
.diskCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = null,
modifier = Modifier.padding(horizontal = 8.dp).size(60.dp)
)
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.margin_medium_large)))
}
)
},
bottomBar = { BottomAppBar(Modifier.fillMaxWidth()) { SetAdmobAdaptiveBanner() } },
) { padding ->
LazyVerticalGrid(
modifier = Modifier.padding(padding),
columns = GridCells.Fixed(3),
contentPadding = PaddingValues(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp),
content = {
items(players.size) { index ->
PlayerCell(players\[index\], navController)
}
}
)
}
}
TheLazyVerticalGrid
component creates a grid layout with 3 columns. It applies padding around the grid and its content, and populates the grid withPlayerCell
components based on theplayers
list.
Compose Fun Fact
You should hoist UI state to the lowest common ancestor between all the composables that read and write it.
Note
You shouldnโt pass ViewModel instances down to other composables. (You canโt build**@Preview**) โ๐
โ Instead โ
Use:Property drilling
โProperty drillingโ refers to passing data through several nested children components to the location where theyโre read.
The Cell
**The PlayerCell
composable displays each playerโs information in a simple card format.**It includes the playerโs headshot, name, and a โPROFILEโ button to navigate to their details. Hereโs how itโs structured:
@Composable
fun PlayerCell(player: Player, navController: NavController) {
val scope = rememberCoroutineScope()
DisposableEffect(scope) { onDispose { scope.cancel() } }
Card(modifier = Modifier.padding(4.dp).fillMaxWidth(),
border = BorderStroke(1.dp, colorResource(R.color.whiteSmokeColor)),
colors = CardDefaults.cardColors(containerColor = colorResource(R.color.whiteColor))) {
Column(modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Spacer(Modifier.height(12.dp))
Box(Modifier.clip(CircleShape).size(74.dp).background(colorResource(R.color.offWhiteColor))
.border(shape = CircleShape, width = 1.dp, color = colorResource(R.color.whiteSmokeColor))) {
AsyncImage(model = player.headshot, contentDescription = null, modifier = Modifier.clip(CircleShape))
}
Spacer(Modifier.height(6.dp))
Text(
text = player.firstName.default,
style = TextStyle(platformStyle = PlatformTextStyle(includeFontPadding = false)),
fontSize = 15.dp.value.sp,
)
val lastName = player.lastName.default.takeIf { it.length > 9 }?.substring(0, 9)?.plus("..") ?: player.lastName.default
Text(
text = lastName,
fontWeight = FontWeight.Bold,
style = TextStyle(platformStyle = PlatformTextStyle(includeFontPadding = false)),
fontSize = 15.dp.value.sp,
)
Spacer(Modifier.height(6.dp))
Text(
text = "PROFILE",
textAlign = TextAlign.Center,
fontSize = 12.dp.value.sp,
fontWeight = FontWeight.SemiBold,
style = TextStyle(platformStyle = PlatformTextStyle(includeFontPadding = false)),
modifier = Modifier.border(shape = RoundedCornerShape(30.dp), width = 1.dp, color = Color.Black)
.background(Color.Transparent).padding(horizontal = 16.dp, vertical = 2.dp)
.clickable { scope.launch { navController.navigate(PlayerProfile.createRoute(id = player.id)) } }
)
Spacer(Modifier.height(12.dp))
}
}
}
@Preview
the Grid in Android Studio
ShowLazyVerticalGridPlayersScreenPreview
composable, allowing developers to visualize how theShowLazyVerticalGridPlayersScreen
component will look and behave without running the entire app. It uses a@Preview
annotation to specify the preview configuration and provides a sample list of players to populate the grid.
@RequiresApi(Build.VERSION_CODES.O)
@Preview(showBackground = true, showSystemUi = true)
@Composable
private fun ShowLazyVerticalGridPlayersScreenPreview(
@PreviewParameter(ShowLazyVerticalGridPlayersScreenPreviewParameterProvider::class) players: List<Player>
) {
Column {
ShowLazyVerticalGridPlayers(PlayersUiState.Success(players, ""), rememberNavController())
}
}
private class ShowLazyVerticalGridPlayersScreenPreviewParameterProvider : PreviewParameterProvider<List<Player>> {
override val values: Sequence<List<Player>> =
sequenceOf(
listOf(
Player(firstName = Default("Connor"), lastName = Default("McDavid")),
Player(firstName = Default("James"), lastName = Default("van Riemsdyk")),
Player(firstName = Default("John"), lastName = Default("Brackenborough")),
Player(firstName = Default("Sidney"), lastName = Default("Crosby")),
Player(firstName = Default("Bobby"), lastName = Default("Brink")),
Player(firstName = Default("Austin"), lastName = Default("Matthews"))
)
)
}

LazyVerticalGrid
Screen in Android StudioDONโT FORGET TO TEST, TEST, TEST: ๐งช๐งช๐งช
To ensure the getSkatersAndGoalies
function is working correctly, I have written a unit test to verify its behavior. Hereโs a breakdown of the test:
@Test
fun `getSkatersAndGoalies() should emit list of skaters`() = runTest {
// Given
val goalie = Player(positionCode = "G")
val skater = Player(positionCode = "C")
val mockPlayers = Players(forwards = listOf(skater), goalies = listOf(goalie))
val mockSeason = "20232024"
// When
coEvery { mockDateUtilsRepository.getCurrentSeasonInYears() } returns mockSeason
coEvery { mockRepository.getAllNhlPlayers(mockSeason) } returns flowOf(mockPlayers)
viewModel.getSkatersAndGoalies(mockSeason)
advanceUntilIdle()
// Then
val actualPlayers = (viewModel.uiState.value as? PlayersUiState.Success)?.players.orEmpty()
assertEquals(2, actualPlayers.size)
}
Major tech companies (PayPal, Google, Meta, Salesforceโฆ)value engineers who understand the significance of testingfor building reliable and high-quality applications and may help you land that big bank jobby-job. ๐ค๐ฝ๐๐ฐ
**Thatโs a wrap!**WithLazyVerticalGrid
, youโve unlocked the power to build stunning grid layouts in your Jetpack Compose app. Ready to see it in action?Download the NHL Hockey app on Google Playand experience the magic firsthand. Donโt forget to leave a review and let me know what you think!
๐ฃ๏ธ: reach out onX (BrickyardApps
)orInsta (brickyardmobile
)
Info
This article is previously published on proandroiddev
