raamcosta / compose-destinations

Annotation processing library for type-safe Jetpack Compose navigation with no boilerplate.
https://composedestinations.rafaelcosta.xyz
Apache License 2.0
3.21k stars 132 forks source link

Trying to add multiple bottom navigation bars to app #141

Closed Hassankhd closed 2 years ago

Hassankhd commented 2 years ago

Hello @raamcosta!

Before I post a question, I want to thank you for this amazing library, it has helped me loads with developing my app!

I'm very new to Android Development, and I'm using Jetpack Compose since I'm very proficient with SwiftUI. I'm trying to define 2 bottom navigation bars for my app but I'm finding it difficult to do so. There are 2 different types of users within my app (employers and job seekers) and they have a completely different way of using the app.

I'm successfully able to define one bottom nav bar in the MainActivity class like this:

setContent {
            JOBSEEQRTheme {
                val navController = rememberNavController()

                Scaffold(
                    modifier = Modifier.fillMaxSize(),
                    bottomBar = {
                        BottomBarJS(navController = navController)
                    }
                ) {

                }

                DestinationsNavHost(
                    navGraph = NavGraphs.root,
                    navController = navController
                )
            }
        }

And everything works perfectly. I just don't know where or how I'll have to add the second bottom nav bar. I've experimented with many methods, the closest I got was including the Scaffold in one of the composables that are inside the second bottom nav bar, but the app crashed every time I clicked on it. I would really appreciate help with this, thanks in advance!

raamcosta commented 2 years ago

Hi!

From what I gathered, maybe just an if expression?

if (isJSUser) JSBottomBar else EmployersBottomBar

This inside the bottomBar = of Scaffold. Sorry for the lack of formatting, I’m on the smartphone 😁

Hassankhd commented 2 years ago

Hello again!

I just tried your suggested solution by defining isJSUser as a property, but it seems like I cannot implement it inside the MainActivity class. I'm utilizing Firebase, and in this case, I have a loading screen that checks whether a user is logged in or not:

1) If the user is logged in, a quick lookup into Firebase Realtime Database is done to determine if the user is a job seeker or an employer:

   a) If the user is a job seeker, the app takes them to the job seeker home page

   b) If the user is an employer, the app would redirect them to the employer home page

2) if the user is NOT logged in, they will be taken to a screen where they're asked if they'd like to access the app as an employer or job seeker

@RootNavGraph(start = true)
@Destination
@Composable
fun EntryPage(
    navigator: DestinationsNavigator
) {
    LaunchedEffect(Unit) {
        var auth = Firebase.auth
        val currentUser = auth.currentUser

// if the user is logged in
        if(currentUser != null) {
            // check in database if user is a job seeker
            navigator.navigate(
               HomeJSDestination()
            )
           // check in database if user is an employer
            navigator.navigate(
               HomeEmplDestination()
            )
        } 

// if the user is not logged in
else {
            navigator.navigate(
                ChoosePageDestination()
            )
        }
    }
...
}

Here's my MainActivity class for reference (including your suggested solution):

class MainActivity : ComponentActivity() {
    @ExperimentalMaterialApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JOBSEEQRTheme {
                val navController = rememberNavController()

                Scaffold(
                    modifier = Modifier.fillMaxSize(),
                    bottomBar = {
                        if (isJSUser) {
                            BottomBarJS(navController = navController)
                        } else {
                            BottomBarEmpl(navController = navController)
                        }
                    }
                ) {

                }

                DestinationsNavHost(
                    navGraph = NavGraphs.root,
                    navController = navController
                )
            }
        }
    }
}

How would your solution change given what I just mentioned and given that I'm defining the Scaffold inside the MainActivity class, or is there something I'm missing? (if so, I really apologize, I'm still new to this)

raamcosta commented 2 years ago

Now I see πŸ™‚

Ok. What you need is a way to know in the MainActivity what type of user is logged in. I’m a reactive way like a StateFlow. Then you can collectAsState on that flow and check at the BottomBar lambda which user type is logged in and show the corresponding bottom bar.

Another possible way is to check via navigation. For example if the current destination belongs to a nav graph that is meant for employers. Or if the main employers screen is in the back stack.

I’d have to know much more of your app logic to know for sure which is the best way, but you can’t go wrong with the first option. You just need to maybe have a common view model with that state (you can check the sample app on this repository how I do that for login state in the MainActivity)

raamcosta commented 2 years ago

I'll close this, but let me know below how that is going :)

Hassankhd commented 2 years ago

Hello @raamcosta!

I still couldn't manage to get it to work. I'm trying to do your solution in the sample (but with some changes), but the app keeps crashing (I'm getting a "No dependency container provided!" message in Firebase's Crashlytics, from LocalDependencyContainer).

Instead of having two bottom bars, I tried going for one, in which I check via view model if the current user is a job seeker or an employer:

` @Composable fun BottomBar( navController: NavController, vm: StartFcnsViewModel ) { val currentDestination: co.jobseeqr.JOBSEEQR.destinations.Destination? = navController.currentBackStackEntryAsState() .value?.appDestination()

val isJobSeeker by vm.isJobSeekerFlow.collectAsState()

if(isJobSeeker) {
    // job seeker bar
    BottomNavigation {
        BottomBarDestinationJS.values().forEach { destination ->
            BottomNavigationItem(
                selected = currentDestination == destination.direction,
                onClick = {
                    navController.navigate(destination.direction, fun NavOptionsBuilder.() {
                        launchSingleTop = true
                    })
                },
                icon = { Icon(destination.icon, contentDescription = destination.lbl) },
                label = { Text(destination.lbl) },
            )
        }
    }
} else {
    // employer bar
    BottomNavigation {
        BottomBarDestinationEmpl.values().forEach { destination ->
            BottomNavigationItem(
                selected = currentDestination == destination.direction,
                onClick = {
                    navController.navigate(destination.direction, fun NavOptionsBuilder.() {
                        launchSingleTop = true
                    })
                },
                icon = { Icon(destination.icon, contentDescription = destination.lbl)},
                label = { Text(destination.lbl) },
            )
        }
    }
}

} ` I cannot test if this solution would work or not because the app keeps crashing on launch.

For reference, I will attach my MainActivity.kt and StartFunctionsRepo.kt files. Let me know if any other files would be of any assistance to you. Archive.zip

Hassankhd commented 2 years ago

This is the StartFcnsViewModel I created as well: (ignore the sign up functions)

`class StartFcnsViewModel(val startFunctionsRepo: StartFunctionsRepo): ViewModel() {

fun checkUserTypeOnLogIn() = startFunctionsRepo.checkUserTypeOnLogIn()

fun signUpJobSeeker(firstName: String, lastName: String,
                        email: String, password: String,
                        desiredJob: String) = startFunctionsRepo.signUpJobSeeker(firstName, lastName, email, password, desiredJob)

fun signUpEmployer(companyName: String,
                   city: String, email: String,
                   password: String, description: String,
                   basedIn: String) = startFunctionsRepo.signUpEmployer(companyName, city, email, password, description, basedIn)

fun setToEmployer() = startFunctionsRepo.setToEmployer()

fun setToJobSeeker() = startFunctionsRepo.setToJobSeeker()

val isJobSeekerFlow = startFunctionsRepo.isJobSeeker

val isJobSeeker get() = isJobSeekerFlow.value

}`

raamcosta commented 2 years ago

Hey!

From this message "No dependency container provided!", it seems like you copied my dependency container logic from my sample apps. This is for manual dependency injection.. are you not using some DI framework like Hilt? If yes, then you don't need LocalDependencyContainer at all. If you're not, this error means you're not providing any DependencyContainer.

On my samples I do:

val LocalDependencyContainer = staticCompositionLocalOf<DependencyContainer> {
    error("No dependency container provided!") // πŸ‘ˆ The error you're getting because you haven't provided any real `DependencyContainer` instance
}

class MainActivity : ComponentActivity() {

    private val dependencyContainer = DependencyContainer()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // πŸ‘‡πŸ‘‡πŸ‘‡πŸ‘‡πŸ‘‡ NOTICE THIS LINE: It makes all LocalDependencyContainer.current calls, provide dependencyContainer defined in the activity.
            CompositionLocalProvider(LocalDependencyContainer provides dependencyContainer) {
                PlaygroundApp(
                    //...
                )
            }
        }
    }
Hassankhd commented 2 years ago

@raamcosta Quick update: The app works now, thanks for your quick response! However, sadly the solution I tried did not work. Apparently, BottomBar isn't getting the new isJobSeeker value when I come to access the app as an employer (if you recall, I set that value to false when I click on the "an Employer" button in the ChoosePage composable via the shared viewModel), and it keeps the bottom nav bar as that of job seekers instead of changing it to that of employers. I don't know what's wrong now, but I would sincerely appreciate some help with solving this once and for all.

Hassankhd commented 2 years ago

I also tried this:

`@Composable fun JOBSEEQRApp() { val engine = rememberAnimatedNavHostEngine() val navController = engine.rememberNavController()

val vm: StartFcnsViewModel = activityViewModel<StartFcnsViewModel>()

val startRoute = EntryPageDestination()

val isJobSeeker by vm.isJobSeekerFlow.collectAsState()

if(isJobSeeker) {
    Scaffold(
        modifier = Modifier.fillMaxSize(),
        bottomBar = {
            BottomBarJS(navController = navController)
        }
    ) {

    }
} else {
    Scaffold(
        modifier = Modifier.fillMaxSize(),
        bottomBar = {
            BottomBarEmpl(navController = navController)
        }
    ) {

    }
}

DestinationsNavHost(
    engine = engine,
    navGraph = NavGraphs.root,
    navController = navController,
    startRoute = startRoute
)

}`

And still no luck. How could I approach this issue properly?

raamcosta commented 2 years ago

The best way is to debug it. For example: add a log right after this line val isJobSeeker by vm.isJobSeekerFlow.collectAsState() does it get called when you change the state on the VM?

Also, log the instance of the vm on both places: where you are calling the method that changes the isJobSeekerFlow and where you are observing it. Does it have the same hash after the "@"? If not you have multiple ViewModels, so you're probably not creating/accessing it in the right way.

I don't have time to delve in your source code sorry, but this is where I would start. Because it most definitely should work :)