Blog Infos
Author
Published
Topics
,
Published

Photo by Pawel Czerwinski on Unsplash

 

I’ve recently delved into how to add support for navigation in Android using Jetpack Compose. To do so, I created a small app that consumes an API and has a Main -> Detail screen. In this post, we’ll go through the basic setup of the project, its key elements to handle navigation, and finally, we’ll add some transitions to it using the latest navigation library. Let’s go through it together.

Mechanics Overview

The app itself is fairly basic and it uses a simple yet clean architecture (I might write a separate post on this at some point). It’s a top-rated movies list that you can scroll through, and if you click on any of the posters, it takes you to a detailed view, which is just the poster expanded to use the whole screen.

The Movies List and the Movie Detail views

I’m using the TMDB API to retrieve the movie list and regardless is a pagination-friendly api, I’m not paginating the results for this sample. As usual to do so I’m using Retrofit.

interface MoviesAPI {
@GET("movie/top_rated")
suspend fun getTopRatedMovies(
@Query("api_key") apiKey: String,
@Query("language") language: String = "en-US",
@Query("page") page: Int
): TMDBResponse<MovieResponse>
}
view raw MovieAPI.kt hosted with ❤ by GitHub

The list of movies is populated using StateFlow into the ViewModel and collectAsStateWithLifecycle() by the core Composable of the app.

I’m transforming the response from the API into a less bulky MovieUI element that has just the information we need to display it in this sample.

data class MovieUI(val movieId: Int,
val movieName: String,
val moviePosterUrl: String)
view raw MovieUI.kt hosted with ❤ by GitHub
navigation-compose

The main goal is to be able to respond to the click of the movie poster, navigating to a detailed view of it. In this case, the detailed view simply consists of an expanded version of the poster. When diving deeper into the code, we have our main screen called MoviesListScreen and the detailed screen called MovieDetailsScreen. What we need is to pass the selectedMovie that was clicked to the detail screen, so that we can display the appropriate poster.

@Composable
fun MoviesListScreen(
onMovieDetails: (Int) -> Unit = {},
movies: State<List<MovieUI>>
) {
MoviesListUI(movies.value, onMovieDetails)
}
val mockMovieList = listOf(MovieUI(1, "Batman", ""))
@Composable
@Preview
private fun MoviesListUI(
movies: List<MovieUI> = mockMovieList,
onMovieDetails: (Int) -> Unit = {}
) {
LazyVerticalGrid(columns = GridCells.Fixed(2), content = {
items(count = movies.size, itemContent = { index ->
MovieUiListItem(movieUi = movies[index], onMovieDetails)
})
})
}
@Composable
@Preview
fun MovieDetailsScreen(
selectedMovie: MovieUI? = MovieUI(1, "Test", "")
) {
AsyncImage(
modifier = Modifier
.fillMaxSize()
.padding(5.dp),
model = ImageRequest.Builder(LocalContext.current)
.data(selectedMovie?.moviePosterUrl)
.crossfade(true).build(),
contentDescription = "TV Show Poster",
contentScale = ContentScale.Crop,
)
}

To do so with Jetpack Compose we can make use of the navigation library:

implementation 'androidx.navigation:navigation-compose:2.7.0-beta02'

*I’ll get into the justification of why I’m using the beta02 later on

This AndroidX library allows us to define with precision the navigation of our app (if you are familiar with the Navigation component and the navigation graph It’s basically the same (https://developer.android.com/guide/navigation/get-started))

The way I approached this is by creating a new “RootComposable” that will hold the definition of our graph:

private const val MOVIE_LIST = "movieList"
private const val MOVIE_DETAIL = "movieDetail"
private const val MOVIE_ID = "movieId"
@Composable
fun MoviesAppNavigationView(
moviesState: StateFlow<List<MovieUI>>
) {
val movies = moviesState.collectAsStateWithLifecycle()
val navController = rememberNavController()
NavHost(navController = navController, startDestination = MOVIE_LIST) {
composable(route = MOVIE_LIST) {
MoviesListScreen(
onMovieDetails = { navController.navigate("$MOVIE_DETAIL/$it") },
movies = movies
)
}
composable(
route = "$MOVIE_DETAIL/{$MOVIE_ID}",
arguments = listOf(navArgument("movieId") { type = NavType.IntType })
) { backStackEntry ->
val selectedMovie =
movies.value.firstOrNull {
it.movieId == backStackEntry.arguments?.getInt(MOVIE_ID)
}
MovieDetailsScreen(selectedMovie = selectedMovie)
}
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

In the MovieDetail destination I’ve added logic so that It supports passing an argument from the list screen into it, this is going to be the id of the clicked movie so that MovieDetailsScreen can go fetch the correct poster path.

This is a key bit of the implementation as Is where we pass the listener that has to react to the poster click. In its implementation It calls the navController and navigates to the required path passing as argument the movieId:

onMovieDetails = { navController.navigate("$MOVIE_DETAIL/$it") },

And our MainActivity uses when calling setContent:

class MainActivity : ComponentActivity() {
private val viewModel: MoviesViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MoviesListTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MoviesAppNavigationView(viewModel.moviesState)
}
}
}
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

We first call rememberNavController which will add a new navigator into our app and It will be remembered along our composition.

val navController = rememberNavController()

After that we need to define our graph through the use of a NavHost. It will receive as parameters our navController and the first destination that It should use by default:

NavHost(navController = navController, startDestination = MOVIE_LIST) {

Into it we need to define our screens using the NavGraphBuilder.composable() function. Inside It allows us to call our composables that should be displayed in each destination:

public fun NavGraphBuilder.composable(
composable(route = MOVIE_LIST) {
MoviesListScreen(
onMovieDetails = { navController.navigate("$MOVIE_DETAIL/$it") },
movies = movies
)
}

And with all of this in place, the app now successfully navigates from the list to the detail view.

NavHost(navController = navController, startDestination = MOVIE_LIST) {
composable(
route = MOVIE_LIST,
enterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left,
animationSpec = tween(700)
)
},
exitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left,
animationSpec = tween(700)
)
},
popEnterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right,
animationSpec = tween(700)
)
},
popExitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right,
animationSpec = tween(700)
)
}) {
MoviesListScreen(
onMovieDetails = { navController.navigate("$MOVIE_DETAIL/$it") },
movies = movies
)
}
composable(
route = "$MOVIE_DETAIL/{$MOVIE_ID}",
arguments = listOf(navArgument("movieId") { type = NavType.IntType }),
enterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left,
animationSpec = tween(700)
)
},
exitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left,
animationSpec = tween(700)
)
},
popEnterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right,
animationSpec = tween(700)
)
},
popExitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right,
animationSpec = tween(700)
)
}
) { backStackEntry ->
val selectedMovie =
movies.value.firstOrNull {
it.movieId == backStackEntry.arguments?.getInt(MOVIE_ID)
}
MovieDetailsScreen(selectedMovie = selectedMovie)
}
}

slideIntoContainer and popEnterTransition only support EnterTransition types while exitTransition and popExitTransition only support ExitTransition types.

On top of this we need to pass in its arguments which is the direction of the animaiton we want it to have when the state is triggered and finally an animationSpec which allows us to easily control our animation duration in a Compose-friendly way.

slideIntoContainer(
  towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left,
  animationSpec = tween(700)
)

And now our transitions look like this 💫

Final Thoughts

While this is all in beta for now and exclusively supported in compileSdk 34, It might not be the tool of choice for most projects but Its very exciting to see in which direction the navigation management is going.

That said as this is in Kotlin and Compose It brings a lot more of opportunities for devs to handle transitions and animations as with the rest of Jetpack.

Have a nice day and I hope your transitions are looking awesome now 🧉

This article was 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

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu