Blog Infos
Author
Published
Topics
, , , ,
Published
Jetpack Compose 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 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))
        }
    }
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kobweb:Creating websites in Kotlin leveraging Compose HTML

Kobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.
Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kob ...

Jobs

@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"))
            )
        )
}

LazyVerticalGrid Screen in Android Studio
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Β orΒ Insta

Best,
RC

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
Menu