0

I am a beginner in Android development, and I couldn't find a good solution to this. So, I use Scaffold as a general screen composable:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HabitsTrackerApp() {
    val navHostController = rememberNavController()
    val currentScreenContent = when (navHostController.currentBackStackEntry?.destination?.route) {
        Screen.HabitTracks.route -> listOf(Screen.HabitTracks.icon, Screen.HabitTracks.title)
        else -> listOf(Screen.HabitTracks.icon, Screen.HabitTracks.title)
    }
    Scaffold(
        topBar = {
            TopBar(currentScreenContent[0], currentScreenContent[1])
        },
        bottomBar = {
            BottomBar(navHostController = navHostController)
        }) {
        Column(
            modifier = Modifier
                .padding(it)
                .padding(dimensionResource(id = R.dimen.default_screen_padding))
        ) {
            NavGraph(navHostController)
        }
    }
}

In the topBar parameter, I use custom TopBar composable, which is contained in another file:

@Composable
fun TopBar(@DrawableRes icon: Int, @StringRes text: Int) {
    Row(
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .fillMaxWidth()
            .padding(dimensionResource(id = R.dimen.padding_small))
    ) {
        Icon(
            imageVector = Icons.Rounded.ArrowBackIos,
            contentDescription = stringResource(id = R.string.backward_icon_content_description)
        )
        Icon(
            painter = painterResource(id = icon),
            contentDescription = null,
            modifier = Modifier
                .size(dimensionResource(id = R.dimen.default_top_bar_icon_size))
                .padding(end = dimensionResource(id = R.dimen.padding_medium))
        )
        Text(text = stringResource(id = text), style = MaterialTheme.typography.headlineLarge)
        Icon(
            imageVector = Icons.Rounded.ArrowForwardIos,
            contentDescription = stringResource(id = R.string.forward_icon_content_description)
        )
    }
}

This TopBar composable requires the icon and header of the current screen. Right now, I use when statement (1st code block) to get current screen icon and text depending on the current navHostController route, but I'm sure that there's a much better solution. There's no way that writing when branches for each screen manually is a good way to solve this problem

NavGraph is also contained in a separate file, as well as each screen composable, if it is important

I initially thought about some kind of viewModel, which could contain the current icon and text of the composable as state, but, as far as i know, there's no way to get the same object of this exact vm and change the state in each screen composable unless I pass it as a parameter, which is also a bad solution

I also tried to google it and found only this, but maybe something changed 2 years later How to change topBar corresponding screen in Jetpack Compose

Or maybe the best way is to somehow change the project structure or something else? And how exactly?

P.S. sorry for my bad english

Slizness
  • 13
  • 4

2 Answers2

0

My own thoughts

Let’s say there is a screen in your app has extra actions button staying on the TopBar (for example: share button, info button). In my opinion, it is the responsibility of that screen to handle the buttons i mentioned, so in my case, i will manually create a topbar on each of my screens, instead of using a common like you did. This will bring you an addition benefits when in some case you want to hide the top bar, which is considered take more logics in your way.

How to update the top bar based on the current screen

The answer is: You may observe to the screen change with NavController, update in correspoinding way. Also, to have your top bar up to date, you have to use State, which is a concept introduced in the very first lessons of Jetpack Compose official webpage.

VanSuTuyDuyen
  • 335
  • 2
  • 11
  • You mean, observe like I did with when statement and add a viewmodel to hold state? – Slizness Aug 24 '23 at 19:25
  • what you are doing in your `when` block is a linear (going through 1st, 2nd... to the final line) then, that's end! If your `currentBackStackEntry` changes, your code block will not be retrigger, so your TopBar won't have any changes. So, check out the [androidx.navigation.compose](https://developer.android.com/reference/kotlin/androidx/navigation/compose/package-summary), you may find something useful – VanSuTuyDuyen Aug 25 '23 at 01:41
  • also, ViewModel is not necessary in this case – VanSuTuyDuyen Aug 25 '23 at 01:42
0

I have worked on many projects where this kind of requirement can be seen.

You can take this as the best example with navigation best practices and changing the title of Scaffold as per BottomNavigation Selection.

Full Example

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController

@Composable
fun Ex11(
) {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = NavRoute.Home.route,
        modifier = Modifier.fillMaxSize()
    ) {
        composable(route = NavRoute.Home.route) {
            HomeScreen() {
                navController.navigate(it)
            }
        }
        composable(route = NavRoute.Settings.route) {
            SettingsScreen()
        }
    }
}

@Composable
fun Home(navigateTo: (route: String) -> Unit) {
    Column(modifier = Modifier.fillMaxSize()) {
        Button(onClick = {
            navigateTo(NavRoute.Settings.route)
        }) {
            Text(text = "Click Me")
        }
    }
}

@Composable
fun SettingsScreen() {
    Column(modifier = Modifier.fillMaxSize()) {
        Text(text = "Settings Screen", fontSize = 20.sp)
    }
}


@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
    navigateTo: (route: String) -> Unit
) {

    val topLevelDestinations = listOf(
        TopLevelDestination(
            route = NavRoute.Screen1.route,
            selectedIcon = R.drawable.home,
            unselectedIcon = R.drawable.home_outline,
            iconText = "Home"
        ), TopLevelDestination(
            route = NavRoute.Screen2.route,
            selectedIcon = R.drawable.bulb,
            unselectedIcon = R.drawable.bulb_outline,
            iconText = "Generate"
        ), TopLevelDestination(
            route = NavRoute.Screen3.route,
            selectedIcon = R.drawable.my_files,
            unselectedIcon = R.drawable.my_files_outline,
            iconText = "My Files"
        )
    )

    val showBottomBar = remember { mutableStateOf(true) }
    var title = remember {
        mutableStateOf("AI Art")
    }
    val navController = rememberNavController()

    Scaffold(
        topBar = {
            CenterAlignedTopAppBar(title = {
                Text(
                    text = title.value,
                    color = Color.White,
                    fontWeight = FontWeight.Bold,
                    fontSize = 20.sp,
                )
            }, colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
                containerColor = MaterialTheme.colorScheme.primary
            ), actions = {
                Icon(
                    imageVector = Icons.Filled.Settings,
                    tint = MaterialTheme.colorScheme.onPrimary,
                    contentDescription = "Settings",
                    modifier = Modifier
                        .clickable {
                            navigateTo(NavRoute.Settings.route)
                        }
                        .padding(8.dp)
                )
            })
        },
        bottomBar = {
            if (showBottomBar.value) {
                HomeBottomBar(destinations = topLevelDestinations,
                    currentDestination = navController.currentBackStackEntryAsState().value?.destination,
                    onNavigateToDestination = {
                        title.value = when (it) {
                            "sc1" -> "Screen 1"
                            "sc2" -> "Screen 2"
                            else -> {
                                "Screen 3"
                            }
                        }
                        navController.navigate(it) {
                            popUpTo(navController.graph.findStartDestination().id) {
                                saveState = true
                            }
                            restoreState = true
                            launchSingleTop = true
                        }
                    })
            }
        }
    ) {
        Column(
            modifier = Modifier
                .padding(it)
                .fillMaxSize()
        ) {
            HomeNavHost(
                modifier = Modifier
                    .fillMaxSize()
                    .weight(1f),
                navController = navController,
                startDestination = NavRoute.Screen1.route,
                navigateTo = navigateTo
            )

        }
    }


}


@Composable
fun HomeNavHost(
    modifier: Modifier,
    navController: NavHostController,
    startDestination: String,
    navigateTo: (route: String) -> Unit
) {

    NavHost(
        navController = navController, startDestination = startDestination, modifier = modifier
    ) {

        composable(route = NavRoute.Screen1.route) {
            Screen1()
        }
        composable(route = NavRoute.Screen2.route) {
            Screen2()
        }
        composable(route = NavRoute.Screen3.route) {
            Screen3()
        }


    }
}


@Composable
fun Screen1() {
    Column(modifier = Modifier.fillMaxSize()) {
        Text(text = "One Screen", fontSize = 20.sp)
    }
}

@Composable
fun Screen2() {
    Column(modifier = Modifier.fillMaxSize()) {
        Text(text = "Two Screen", fontSize = 20.sp)
    }
}

@Composable
fun Screen3() {
    Column(modifier = Modifier.fillMaxSize()) {
        Text(text = "Three Screen", fontSize = 20.sp)
    }
}


@Composable
private fun HomeBottomBar(
    destinations: List<TopLevelDestination>,
    currentDestination: NavDestination?,
    onNavigateToDestination: (route: String) -> Unit
) {

        NavigationBar(
            modifier = Modifier
                .windowInsetsPadding(
                    WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)
                )
                .height(70.dp),
        ) {
            destinations.forEach { destination ->
                val selected =
                    currentDestination?.hierarchy?.any { it.route == destination.route } == true
                NavigationBarItem(
                    selected = selected,
                    onClick = { onNavigateToDestination(destination.route) },
                    icon = {
                        val icon = if (selected) {
                            destination.selectedIcon
                        } else {
                            destination.unselectedIcon
                        }
                        Icon(
                            imageVector = ImageVector.vectorResource(icon),
                            modifier = Modifier.size(16.dp),
                            contentDescription = null
                        )
                    },
                    label = {
                        Text(
                            text = destination.iconText
                        )
                    })
            }
        }

}

Preview

preview

Chirag Thummar
  • 665
  • 6
  • 16