Build Stunning Grids in Minutes with LazyVerticalGrid | š š š š |
Build Stunning Grids in Minutes with LazyVerticalGrid | š š š š | ź“ė Ø
Want to create stunning grid layouts in your Jetpack Compose app? Look no further thanĀ LazyVerticalGrid
. This powerful toolĀ simplifiesĀ the process of designing and implementing efficient grid-based interfaces. In this comprehensive tutorial, Iāll share my insights and experience usingĀ LazyVerticalGrid
Ā in a real-worldĀ productionĀ app 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)
}
}
)
}
}
TheĀ LazyVerticalGrid
Ā component creates a grid layout with 3 columns. It applies padding around the grid and its content, and populates the grid withĀ PlayerCell
Ā components based on theĀ players
Ā 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 theĀ ShowLazyVerticalGridPlayersScreen
Ā 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"))
)
)
}
DONā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 testingĀ for building reliable and high-quality applications and may help you land that big bank jobby-job. š¤š½šš°
Thatās a wrap!Ā WithĀ LazyVerticalGrid
, 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 PlayĀ and experience the magic firsthand. Donāt forget to leave a review and let me know what you think!
š£ļø: reach out onĀ X (BrickyardApps
)Ā orĀ Insta (brickyardmobile
)
Info
This article is previously published on proandroiddev.com