Navigation in Jetpack

This tutorial explores the concept of navigation using a real world movie application example.Furthermore,it simplifies key navigation principles such as routes and NavHost in a precise manner.

· 5 min read
Wilson Ochieng

Wilson Ochieng

Android Expert developing mobile applications with 4+ Years of Experience.

topics

Introduction

Jetpack Compose is a modern framework for building native Android user interfaces. Android UI development is streamlined and expedited by its declarative paradigm. Most mobile applications utilize navigation or the ability to flip between screens. Jetpack Compose has a navigation library that streamlines, simplifies, and improves this process.

Using Jetpack Compose and a free movie API, we will create a basic movie app in this article. This application will feature three screens: a list of movies, an overview of the most current film, and a list of movies that users have viewed most frequently. We will explore important aspects of navigation including destinations, routes, NavHost,NavGraph and NaVController.

A NavController aids in navigating between your application's screens, or destinations.Additionally, a NavGraph maps the destinations of the composables to navigate to.On the other hand,a NavHost is a composable container that shows the NavGraph's current destination.

 A navController is a NavHostController class instance. This object can be used to move between screens, for instance, by invoking the navigate() method to move to a different location. Calling rememberNavController() from a composable function will yield the NavHostController.Also, a startDestination is a string route that specifies the default destination displayed by the application when the NaVHost is initially displayed.

Setup

Prerequisites

-Install the latest Android Studio.

-Laptop with 8GB ram and above with Windows,Linux or MacOS installed.

-Sufficient SSD or HDD memory.

Addition of Dependencies

Add navigation dependency to aid in the navigation process,retrofit dependencies that allows fetching of data from apis.

dependencies {

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    implementation("androidx.compose.material3:material3:1.2.1")
    implementation("androidx.compose.material3:material3-window-size-class:1.2.1")
    implementation("androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0-beta03")
    implementation ("com.github.bumptech.glide:compose:1.0.0-beta01")

    //navigation
    implementation("androidx.navigation:navigation-compose:2.7.7")
    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    // Retrofit with Scalar Converter
    implementation("com.squareup.retrofit2:converter-scalars:2.9.0")
    implementation("io.coil-kt:coil-compose:1.4.0")

    // Retrofit with Gson
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    //Data Logging
    implementation("com.squareup.okhttp3:logging-interceptor:4.9.3")

Setting up Retrofit

Add a package called network that will have four classes to control network requests.These classes include Movie.kt,MovieApiService.kt,RetrofitInstance.kt and MovieResponse.kt.

Movie.kt

The Movie Kotlin data class is defined by this code and is part of the com.example.movieapp.network package. Encapsulating different movie properties, the Movie data class models the structure of a movie object.

package com.example.movieapp.network

data class Movie(val id: Int, val title: String, val overview: String,val director:String,val vote_average:Double, val backdropPath: String )

MovieApiService.kt

The MovieApiService interface in the MovieApp specifies the methods and API endpoints for communicating with the movie API. It sends HTTP requests using Retrofit.

package com.example.movieapp.network
import retrofit2.http.GET
import retrofit2.http.Query

interface MovieApiService {
    @GET("movie/popular")
    suspend fun getPopularMovies(@Query("api_key") apiKey: String): MovieResponse

}

MovieResponse.kt

A Kotlin class called MovieResponse provides a model for the response that the movie API returns when a collection of popular movies is retrieved.

package com.example.movieapp.network

data class MovieResponse(val results: List<Movie>)


RetrofitInstance.kt

package com.example.movieapp.network

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory

object RetrofitInstance {
    private const val BASE_URL = "https://api.themoviedb.org/3/"

    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }

    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .build()

    val api: MovieApiService by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)  // Add the OkHttpClient with logging interceptor
            .addConverterFactory(ScalarsConverterFactory.create())  // Add the Scalars converter first
            .addConverterFactory(GsonConverterFactory.create())  // Add the Gson converter
            .build()
            .create(MovieApiService::class.java)
    }
}

Add a viewModel called MovieViewModel in a packaged named viewmodel.

ViewModel.kt

The task of retrieving and storing movie data from an external API is handled by the MovieViewModel in the MovieApp application.

package com.example.movieapp.viewmodel
import androidx.lifecycle.*
import com.example.movieapp.network.Movie
import com.example.movieapp.network.RetrofitInstance
import kotlinx.coroutines.launch

class MovieViewModel : ViewModel() {
    private val _movies = MutableLiveData<List<Movie>>()
    val movies: LiveData<List<Movie>> get() = _movies

    init {
        fetchMovies()
    }


    private fun fetchMovies() {
        viewModelScope.launch {
            val response = RetrofitInstance.api.getPopularMovies("056526ee8518b470c52775f1dad739d7")
            _movies.postValue(response.results)
        }
    }


}

Defining routes for destinations in Movie App

A route is a string that maps to a destination and acts as its unique identifier. It is important to understand that a route is a string that corresponds to a destination, which is analogous to the idea of a URL. For example, a different URL maps to a different page on a website. A destination is usually a single composable or group of composables that correspond to what the user sees. The Movie app requires destinations for the movieList screen and MovieDetailsScreen.

Add the required composables for different screens for the movie app.

Create a package called navigation to add the files that will enable moving from one screen to another.Within the package add a Navigraph.kt file.

NavGraph.kt

package com.example.movieapp.navigation

import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.example.movieapp.ui.MovieDetailsScreen
import com.example.movieapp.ui.MovieListScreen
import com.example.movieapp.viewmodel.MovieViewModel

@Composable
fun NavGraph(navController: NavHostController, viewModel: MovieViewModel) {
    val movies = viewModel.movies.value ?: emptyList()

    NavHost(navController = navController, startDestination = "movieList") {
        composable("movieList") {
            MovieListScreen(navController, movies)
        }
        composable(
            "details/{movieId}",
            arguments = listOf(navArgument("movieId") { type = androidx.navigation.NavType.IntType })
        ) { backStackEntry ->
            val movieId = backStackEntry.arguments?.getInt("movieId")
            val movie = movies.find { it.id == movieId }
            movie?.let { MovieDetailsScreen(it) }
        }
    }
}

In the code above,the NavHost composable displays other composable destinations, based on a given route.There are two routes named movieList and details/{movieId}.The movieList route has no parameters while the details/{movieId} has a parameter movieId that identifies each movie with their respective ids.

Finally,import the Navgraph component in MainActivity.kt

MainActivity.kt

package com.example.movieapp

import android.annotation.SuppressLint
import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.rememberNavController
import com.example.movieapp.navigation.NavGraph
import com.example.movieapp.ui.theme.MovieAppTheme
import com.example.movieapp.viewmodel.MovieViewModel

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3Api::class)
    @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MovieAppTheme {
                // A surface container using the 'background' color from the theme
                Scaffold(
                    topBar = {
                        TopAppBar(
                            modifier = Modifier.fillMaxWidth(),
                            title = {
                                Box(modifier = Modifier.fillMaxWidth()) {
                                    Text(
                                        text = "Movie App",
                                        modifier = Modifier.padding(horizontal = 16.dp), // Adjust padding as needed
                                        color = MaterialTheme.colorScheme.primary, // Text color
                                    )
                                }
                            }
                        )
                    }
                )

                {
                    val navController = rememberNavController()
                    val viewModel: MovieViewModel by viewModels()
                    NavGraph(navController = navController, viewModel = viewModel)
                }
            }
        }
    }
}




@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MovieAppTheme {
        val navController = rememberNavController()
        val viewModel = MovieViewModel()
        NavGraph(navController = navController, viewModel = viewModel)
    }
}

Navigation in action

MovieListScreen

Image

MovieDetailsScreen

Image

Conclusion

An essential feature of any application with several screens is navigation, which provides clear paths to important destinations such as the information and movie list displays. By leading users through many components that influence their interactive experience, these routes function as distinctive identifiers. The use of NavHost and NavController guarantees smooth screen transitions. This configuration not only makes it easier for users to navigate, but it also makes it easier for future updates and maintenance, giving movie buffs a stable and user-friendly app experience.








share

Wilson Ochieng

Android Expert developing mobile applications with 4+ Years of Experience.