How it worked before
In the beginning there was a navigation like startActivity (Activityname::class.java) whenever we wanted to show a new screen, alternatively you would use fragments with the FragmentManager
and FragmentTransactions
, manage the backstack(s), remember to use the ChildFragmentManager
, too, whenever you had to, remember there is an “old” FragmentManager
and a SupportFragmentManager
that you could mix up etc. To overcome this Google has decided to developed the navigation component, giving us navigation graphs and a NavController
that has all the power of combined. In this post, we’ll explore the Navigation component’s support for Jetpack Compose and take a look.
Quick Review about Navigation in Android
Navigation helps you in understanding how your app moves across different screens in your Application.
The Navigation Component is made up of three parts:
- Navigation Graph: The navigation graph provides a visual representation of the app’s navigation flow, allowing developers to design and visualize the user’s journey through the app.
- NavHost: NavHost essentially acts as a placeholder for swapping in and out different fragments or activities as the user navigates through the app. It provides a consistent and structured way to manage app navigation, making it easier to implement and maintain complex navigation flows.
- NavController: NavController is a component of the Navigation Architecture Component that manages app navigation within a NavHost. It is responsible for handling navigation within an app based on the defined navigation graph.
Lets jump into Code
Before getting started, we’ll add a dependency on navigation-compose
, the Navigation component’s artifact for Compose support.
implementation ("androidx.navigation:navigation-compose:2.7.7")
Next is how we can setup with all three components with Compose
First, we create and memoize a NavController
using the rememberNavController
method. rememberNavController
returns a NavHostController
which is a subclass of NavController
that offers some additional APIs that a NavHost
can use. When referring to the NavController
and using it later on, we will use it as NavController
as we don’t need to know about these additional APIs ourselves; it’s just important for the NavHost
.
@Composable
fun mainContent(){
val mainViewModel : MainViewModel = koinViewModel()
NavigationExampleTheme {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Routes.LIST_SCREEN) {
composable(Routes.LIST_SCREEN) {
}
composable(Routes.DETAIL_SCREEN)
}
}
}
}
I have defined routes & its names in one File named Routes.kt
object Routes {
const val LIST_SCREEN="listScreen"
const val DETAIL_SCREEN = "detailScreen"
}
Into your MainActivity.kt file. call this mainContent function
setContent {
mainContent()
}
the Navigation Component requires that you follow the Principles of Navigation and use a fixed starting destination. You should not use a composable value for the startDestination route.
Here the demo is for a composable list UI & by clicking on the any item it will show the details screen for it, to achieve this we will have to send argument to the detail screen for mapping
How arguments will work for Jetpack Compose navigation
Navigation Compose supports passing arguments between composable destinations. In order to do this, you have to add argument placeholders to your route
Here we will use a simple argument, By default, all arguments are parsed as strings. The arguments
parameter of composable()
accepts a list of NamedNavArguments. You can quickly create a
NamedNavArgument
using the navArgument method and then specify its exact
type
:
@Composable
fun mainContent(){
val mainViewModel : MainViewModel = koinViewModel()
NavigationExampleTheme {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Routes.LIST_SCREEN) {
composable(Routes.LIST_SCREEN) {
RecipesScreen(navigation= navController, mainViewModel)
}
composable(
Routes.DETAIL_SCREEN,arguments= listOf(navArgument("idValue"){
type = NavType.IntType
})
) {backStackEntry->
RecipeDetailScreen(navController,mainViewModel, backStackEntry.arguments?.getInt(Routes.Values.IDVALUE,0))
}
}
}
}
Above example shows Integer type Argument, so we have defined type
NavType.IntType
Here we will extract arguments from the NavBackStackEntry that is available in the lambda of the
composable()
function.
backStackEntry.arguments?.getInt(Routes.Values.IDVALUE,0)
Our Modified version of Routes.kt
object Routes {
const val LIST_SCREEN="listScreen"
const val DETAIL_SCREEN = "detailScreen/{${Values.IDVALUE}}"
fun getSecondScreenPath(idValue: Int?): String =
// to avoid null and empty strings
if (idValue != null) "detailScreen/$idValue" else "detailScreen/Empty"
object Values {
const val IDVALUE = "idValue"
}
}
When you navigate from one screen to another, We will simply call
navigation.navigate(Routes.getSecondScreenPath("your int value"))
for Dependency Injection Koin, You can go through on my this blog!!
Job Offers
for UI state updates like success, error & loading using StateFlow , Here is the ViewModel class
class MainViewModel(private val repository: Repository, application: Application): BaseViewModel(application) {
val _uiStateReceipeList = MutableStateFlow<UiState<Receipes>>(UiState.Loading)
val uiStateReceipeList: StateFlow<UiState<Receipes>> = _uiStateReceipeList
val _uiStateReceipeDetail = MutableStateFlow<UiState<Receipes.Recipe>>(UiState.Loading)
val uiStateReceipeDetail: StateFlow<UiState<Receipes.Recipe>> = _uiStateReceipeDetail
fun getReceipesList() = viewModelScope.launch {
repository.getReceipes(context).collect {
when (it) {
is UiState.Success -> {
_uiStateReceipeList.value = UiState.Success(it.data)
}
is UiState.Loading -> {
_uiStateReceipeList.value = UiState.Loading
}
is UiState.Error -> {
//Handle Error
_uiStateReceipeList.value = UiState.Error(it.message)
}
}
}
}
fun getReceipeDetail(id:Int?) = viewModelScope.launch {
repository.getReceipesDetail(context,id).collect {
when (it) {
is UiState.Success -> {
_uiStateReceipeDetail.value = UiState.Success(it.data)
}
is UiState.Loading -> {
_uiStateReceipeDetail.value = UiState.Loading
}
is UiState.Error -> {
//Handle Error
_uiStateReceipeDetail.value = UiState.Error(it.message)
}
}
}
}
}
For Dependency Injection here is the list of modules defined for Datasource, Retrofit & ViewModel
val remoteDataSourceModule= module {
factory { RemoteDataSource(get()) }
}
fun provideHttpClient(): OkHttpClient {
return OkHttpClient
.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.build()
}
fun provideConverterFactory(): GsonConverterFactory =
GsonConverterFactory.create()
fun provideRetrofit(
okHttpClient: OkHttpClient,
gsonConverterFactory: GsonConverterFactory
): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(gsonConverterFactory)
.build()
}
fun provideService(retrofit: Retrofit): ApiService =
retrofit.create(ApiService::class.java)
val networkModule= module {
single { provideHttpClient() }
single { provideConverterFactory() }
single { provideRetrofit(get(),get()) }
single { provideService(get()) }
}
val repositoryModule = module {
factory { Repository(get()) }
}
val viewModelModule= module {
viewModel{ MainViewModel(get(),get()) }
}
Combine all modules that we have, inject it into our Application class that we have covered on previous article!!
startKoin {
androidContext(this@MyApplication)
androidLogger()
modules(networkModule, remoteDataSourceModule, repositoryModule, viewModelModule)
}
Finally Screen UI for list of cards using LazyColumns
@Composable
fun RecipesScreen(navigation: NavController, mainViewModel: MainViewModel) {
Scaffold(
topBar = {
CustomToolbarScreen(navController = navigation, title = "Home", false)
}
)
{ innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.padding(10.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//add your code
LaunchedEffect(key1 = Unit) {
getReceipesListAPI(mainViewModel)
}
val state = mainViewModel.uiStateReceipeList.collectAsState()
when (state.value) {
is UiState.Success -> {
ProgressLoader(isLoading = false)
(state.value as UiState.Success<Receipes>).data?.let {
it.recipes?.let { it1 ->
RecipeList(recipes = it1) { recipe ->
// Handle recipe click here
navigation.navigate(Routes.getSecondScreenPath(recipe.id))
}
}
}
}
is UiState.Loading -> {
ProgressLoader(isLoading = true)
}
is UiState.Error -> {
ProgressLoader(isLoading = false)
//Handle Error
}
}
}
}
}
@Composable
fun RecipeListCard(recipe: Receipes.Recipe, onRecipeClick: (Receipes.Recipe) -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { onRecipeClick(recipe) },
shape = RoundedCornerShape(10),
elevation = CardDefaults.cardElevation(
defaultElevation = 4.dp
)
) {
Row(
modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = rememberAsyncImagePainter(recipe.image),
contentDescription = null,
modifier = Modifier
.size(100.dp)
.clip(RoundedCornerShape(10.dp)),
contentScale = ContentScale.Crop
)
Column(
modifier = Modifier
.padding(start = 8.dp)
.weight(1f)
) {
Text(
text = recipe.name ?: "",
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
Text(
text = "Prep Time: ${recipe.prepTimeMinutes} mins",
fontSize = 14.sp,
color = Color.Black
)
Text(
text = "Cook Time: ${recipe.cookTimeMinutes} mins",
fontSize = 14.sp,
color = Color.Black
)
Text(
text = "Servings: ${recipe.servings}",
fontSize = 14.sp,
color = Color.Black
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecipeList(recipes: List<Receipes.Recipe>, onRecipeClick: (Receipes.Recipe) -> Unit) {
LazyColumn {
items(recipes) { recipe ->
RecipeListCard(recipe = recipe, onRecipeClick = onRecipeClick)
}
}
}
private fun getReceipesListAPI(mainViewModel: MainViewModel) {
// Call the function to fetch recipes
mainViewModel.getReceipesList()
}
That’s it, You can find complete implementation on My Github Repository, Hope this article will helpful, Thank you.
This article is previously published on proandroiddev.com