CPS251 Android Development by Scott Shaper

Organizing Navigation with Multiple Files

What We Will Cover

In this lesson, we will look at how to organize your Android app into multiple files and manage different types of data between screens. Think of it like organizing a house - you don't put everything in one room. Instead, you have a kitchen for cooking, a bedroom for sleeping, and a living room for relaxing. Each room has its own purpose and its own stuff.

Why Separate Files?

Imagine trying to find your socks in a house where everything is piled in one giant room. It would be a nightmare! The same thing happens with code when you put everything in one file. Separating your code into different files is like organizing your house into rooms because it:

  • Makes code easier to find (like knowing your socks are in the bedroom)
  • Makes it easier to fix problems (like knowing a leak is in the bathroom)
  • Makes it easier for teams to work together (like having different people work on different rooms)
  • Follows the "one job per file" rule (like each room having one main purpose)
  • Makes testing easier (like testing each room's function separately)

Recommended File Structure

Here's how to organize your files, like organizing rooms in a house:

app/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com.example.myapp/
│   │   │       ├── MainActivity.kt              // The front door of your app
│   │   │       ├── navigation/                  // The hallway connecting rooms
│   │   │       │   ├── AppNavigation.kt         // The map of your house
│   │   │       │   └── NavigationRoutes.kt             // Room names and addresses
│   │   │       ├── viewmodels/                  // The brains of each room
│   │   │       │   ├── shared/
│   │   │       │   │   └── SharedViewModel.kt   // Family information everyone shares
│   │   │       │   └── screens/
│   │   │       │       ├── HomeViewModel.kt     // Home screen's private thoughts
│   │   │       │       ├── ProfileViewModel.kt  // Profile screen's private thoughts
│   │   │       │       └── SettingsViewModel.kt // Settings screen's private thoughts
│   │   │       └── screens/                     // The actual rooms
│   │   │           ├── home/
│   │   │           │   └── HomeScreen.kt        // The living room
│   │   │           ├── profile/
│   │   │           │   └── ProfileScreen.kt     // The bedroom
│   │   │           └── settings/
│   │   │               └── SettingsScreen.kt    // The kitchen

Understanding the Structure

  • navigation/ - Like the hallway and map of your house
    • AppNavigation.kt - Shows how to get from room to room
    • NavigationRoutes.kt - Lists all the room names and addresses
  • viewmodels/ - Like the brains that remember things
    • shared/ - Information that everyone in the house knows
    • screens/ - Information that only one room knows
  • screens/ - The actual rooms where people spend time
    • Each screen is like a separate room with its own purpose
    • Each room has its own furniture (UI) and its own storage (ViewModel)

Required Gradle Dependencies

Like discusssed prevousily in the navigation chapter we need to add the following dependencies to our project:

1. Update Version Catalog (gradle/libs.versions.toml)

[versions]
    # ... existing versions ...
    navigation-compose = "2.7.7"
    lifecycle-viewmodel-compose = "2.7.0"
    
    [libraries]
    # ... existing libraries ...
    androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" }
    androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle-viewmodel-compose" }

2. Update App Dependencies (app/build.gradle.kts)

dependencies {
    // ... existing dependencies ...
    
    // Add these new dependencies for ViewModels and Navigation
    implementation(libs.androidx.lifecycle.viewmodel.compose)
    implementation(libs.androidx.navigation.compose)
}

Understanding the Dependencies

  • lifecycle-viewmodel-compose:
    • Gives us the viewModel() function (like getting a brain for each room)
    • Makes ViewModels work with Compose (like making sure the brain can talk to the room)
    • Keeps data safe when the phone rotates (like making sure nothing falls when you turn the house)
    • Essential for remembering things in our screens
  • navigation-compose:
    • Gives us navigation tools like NavHost and composable (like building hallways between rooms)
    • Makes navigation safe and predictable (like having clear signs pointing to each room)
    • Supports passing data between screens (like passing notes between rooms)
    • Required for moving between different screens

Why Use Version Catalog?

  • One Place to Update: Change a version once, and it updates everywhere
  • Consistency: Makes sure all parts of your app use the same versions
  • Easy Updates: Like updating your shopping list in one place
  • Best Practice: This is how professional developers organize their projects

Understanding Different Types of Data Management

Think of data management like different ways of sharing information in a house. Sometimes you want to pass a note to someone, and sometimes you want to put information on a family bulletin board that everyone can see.

1. Screen-Specific ViewModels (Private Notes)

Use these when information is only needed in one screen, like a private note you keep in your bedroom:

// viewmodels/screens/HomeViewModel.kt
class HomeViewModel : ViewModel() {
    var screenTitle by mutableStateOf("Home")
        private set
    
    fun updateTitle(newTitle: String) {
        screenTitle = newTitle
    }
}

// screens/home/HomeScreen.kt
@Composable
fun HomeScreen(
    homeViewModel: HomeViewModel = viewModel(),
    onProfileClick: (String) -> Unit,
    onSettingsClick: () -> Unit
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = homeViewModel.screenTitle)
        Button(onClick = { onProfileClick("Scott Shaper") }) {
            Text("Go to Profile")
        }
        Button(onClick = onSettingsClick) {
            Text("Go to Settings")
        }
    }
}

2. Shared ViewModels (Family Bulletin Board)

Use these when multiple screens need the same information, like a family calendar that everyone can see:

// viewmodels/shared/SharedViewModel.kt
class SharedViewModel : ViewModel() {
    var currentUser by mutableStateOf(null)
        private set
    
    fun updateUser(user: User) {
        currentUser = user
    }
    
    fun clearUser() {
        currentUser = null
    }
}

3. Navigation Parameters (Passing Notes)

Use these when you want to pass specific information from one screen to another, like passing a note with someone's name:

// When navigating from Home to Profile
onProfileClick("Scott Shaper")  // Passing a specific name

// The Profile screen receives this name
@Composable
fun ProfileScreen(
    userId: String = "Unknown",  // Receiving the passed name
    // ... other parameters
) {
    // Use the userId to load the right profile
    LaunchedEffect(userId) {
        profileViewModel.loadProfile(userId)
    }
}

When to Use Each Approach

Type When to Use Real-World Example
Screen-Specific ViewModels When data is only used in one screen Like keeping your personal diary in your bedroom
Shared ViewModels When multiple screens need the same data Like a family calendar that everyone can see
Navigation Parameters When passing specific data to another screen Like passing a note with someone's name to another room

Complete Example Implementation

Let's build a complete example that shows all three types of data management working together. Think of this like building a small house with three rooms that can communicate with each other.

1. Define Routes (NavigationRoutes.kt)

First, we create a map of our house with room names and addresses:

// navigation/NavigationRoutes.kt
object NavigationRoutes {
    const val HOME = "home"                    // The living room
    const val PROFILE = "profile/{userId}"     // The bedroom (with a specific person's name)
    const val SETTINGS = "settings"            // The kitchen
}

Understanding the {userId} part: This is like having a bedroom that can be for different people. The {userId} is a placeholder that gets replaced with an actual name, like "profile/Scott" or "profile/John".

2. Create ViewModels

Now we create the brains for each room:

// viewmodels/shared/SharedViewModel.kt
class SharedViewModel : ViewModel() {
    var currentUser by mutableStateOf(null)
        private set
    
    fun updateUser(user: User) {
        currentUser = user
    }
    
    fun clearUser() {
        currentUser = null
    }
}

// viewmodels/screens/HomeViewModel.kt
class HomeViewModel : ViewModel() {
    var screenTitle by mutableStateOf("Home")
        private set
    var isLoading by mutableStateOf(false)
        private set
    
    fun updateTitle(newTitle: String) {
        screenTitle = newTitle
    }
    
    fun loadHomeData() {
        isLoading = true
        // Simulate loading data
        isLoading = false
    }
}

// viewmodels/screens/ProfileViewModel.kt
class ProfileViewModel : ViewModel() {
    var profileData by mutableStateOf(null)
        private set
    var isLoading by mutableStateOf(false)
        private set
    
    fun loadProfile(userId: String) {
        isLoading = true
        // Simulate loading profile data for the specific user
        profileData = ProfileData(
            userId = userId,
            description = "Profile for $userId - This is a sample profile description."
        )
        isLoading = false
    }
}

3. Create Screen Composables

Now we create the actual rooms:

// screens/home/HomeScreen.kt
@Composable
fun HomeScreen(
    homeViewModel: HomeViewModel = viewModel(),
    sharedViewModel: SharedViewModel = viewModel(),
    onProfileClick: (String) -> Unit,
    onSettingsClick: () -> Unit
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = homeViewModel.screenTitle)
        
        if (homeViewModel.isLoading) {
            CircularProgressIndicator()
        }
        
        // Show shared user information if available
        sharedViewModel.currentUser?.let { user ->
            Text("Welcome ${user.name}!")
            Text("Email: ${user.email}")
        }
        
        Button(onClick = { onProfileClick("Scott Shaper") }) {
            Text("Go to Profile")
        }
        Button(onClick = onSettingsClick) {
            Text("Go to Settings")
        }
    }
}

// screens/profile/ProfileScreen.kt
@Composable
fun ProfileScreen(
    userId: String = "Unknown",  // This receives the name passed from Home
    profileViewModel: ProfileViewModel = viewModel(),
    sharedViewModel: SharedViewModel = viewModel(),
    onHomeClick: () -> Unit,
    onSettingsClick: () -> Unit
) {
    // Load profile data for the specific user when the screen appears
    LaunchedEffect(userId) {
        profileViewModel.loadProfile(userId)
    }
    
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Profile Screen")
        
        if (profileViewModel.isLoading) {
            CircularProgressIndicator()
        } else {
            profileViewModel.profileData?.let { profile ->
                Text("User: ${profile.userId}")
                Text(profile.description)
            }
        }
        
        // Show shared user information
        sharedViewModel.currentUser?.let { user ->
            Text("Shared User: ${user.name}")
        }
        
        Button(onClick = onHomeClick) {
            Text("Go to Home")
        }
        Button(onClick = onSettingsClick) {
            Text("Go to Settings")
        }
    }
}

// screens/settings/SettingsScreen.kt
@Composable
fun SettingsScreen(
    sharedViewModel: SharedViewModel,
    onHomeClick: () -> Unit,
    onProfileClick: (String) -> Unit
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Settings Screen")
        
        // Show current user information
        sharedViewModel.currentUser?.let { user ->
            Text("Current User: ${user.name}")
            Text("Email: ${user.email}")
        }
        
        // Button to login a sample user
        Button(
            onClick = {
                val sampleUser = User(
                    id = "1",
                    name = "John Doe",
                    email = "john.doe@example.com"
                )
                sharedViewModel.updateUser(sampleUser)
            }
        ) {
            Text("Login Sample User")
        }
        
        // Button to go to profile using shared user name
        Button(
            onClick = { 
                onProfileClick(sharedViewModel.currentUser?.name ?: "Unknown") 
            }
        ) {
            Text("Go to Profile")
        }
        
        Button(onClick = onHomeClick) {
            Text("Go to Home")
        }
    }
}

4. Set Up Navigation (AppNavigation.kt)

Now we connect all the rooms with hallways:

@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    val sharedViewModel: SharedViewModel = viewModel()
    
    NavHost(navController = navController, startDestination = NavigationRoutes.HOME) {
        // Home screen
        composable(NavigationRoutes.HOME) {
            HomeScreen(
                sharedViewModel = sharedViewModel,
                onProfileClick = { userName ->
                    // Navigate to profile with a specific user name
                    navController.navigate("profile/$userName")
                },
                onSettingsClick = {
                    navController.navigate(NavigationRoutes.SETTINGS)
                }
            )
        }
        
        // Profile screen with navigation parameter
        composable(
            route = NavigationRoutes.PROFILE,  // "profile/{userId}"
            arguments = listOf(
                navArgument("userId") { 
                    type = NavType.StringType  // The userId is a string
                }
            )
        ) { backStackEntry ->
            // Extract the userId from the navigation arguments
            val userId = backStackEntry.arguments?.getString("userId") ?: "Unknown"
            
            ProfileScreen(
                userId = userId,  // Pass the extracted userId to ProfileScreen
                sharedViewModel = sharedViewModel,
                onHomeClick = { navController.navigate(NavigationRoutes.HOME) },
                onSettingsClick = { navController.navigate(NavigationRoutes.SETTINGS) }
            )
        }
        
        // Settings screen
        composable(NavigationRoutes.SETTINGS) {
            SettingsScreen(
                sharedViewModel = sharedViewModel,
                onHomeClick = { navController.navigate(NavigationRoutes.HOME) },
                onProfileClick = { userName -> 
                    navController.navigate("profile/$userName") 
                }
            )
        }
    }
}

5. Main Activity (MainActivity.kt)

Finally, we create the front door of our app:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    AppNavigation()  // Start the navigation system
                }
            }
        }
    }
}

Understanding How Data Flows

Let's trace how data moves through our app:

  1. Home → Profile:
    • HomeScreen calls onProfileClick("Scott Shaper")
    • Navigation creates the route "profile/Scott Shaper"
    • ProfileScreen receives userId = "Scott Shaper"
    • ProfileScreen loads profile data for "Scott Shaper"
  2. Home → Settings:
    • HomeScreen calls onSettingsClick()
    • SettingsScreen opens (no parameters needed)
    • SettingsScreen uses SharedViewModel to show current user
  3. Settings → Profile:
    • SettingsScreen calls onProfileClick(sharedViewModel.currentUser?.name)
    • If current user is "John Doe", navigation creates "profile/John Doe"
    • ProfileScreen receives userId = "John Doe"
    • ProfileScreen loads profile data for "John Doe"

This is how the example renders

To try out the code example you can download the NavState project and open it in Android Studio.

This is the home screen when the app first loads

Home Screen

This is the profile screen when clicked from the home screen notice how Scott Shaper is passed to the profile screen.

Home to Profile

This is the home to the settings screen notice how Scott Shaper is not passed to the settings screen. That is because the home screen only passes the string "Scott Shaper" to the profile screen. The settings screen does not need to know the user name because it is not using the shared view model.

Home to Settings

This is the profile screen passed from the settings screen. Notice it is Unknow because the settings screen has put nothing in the shared view model.

Settings to Profile

This is the settings screen when the login button is clicked.

Settings to Login

This is the profile screen coming from the settings screen after the login button is clicked. Notice how the profile screen is now showing the user name John Doe. That is because it is using the shared view model to get the user name that was sent by the settings screen.

Settings to Profile Login

This is the home screen after the login button is clicked. Notice how the home screen is now showing the user name John Doe. That is because it is using the shared view model to get the user name that was sent by the settings screen. The home screen button was clicked from the profile screen but could have also been clicked from the settings screen and the result would be the same.

Home to Settings Login

This is the profile screen after the reload button is clicked. Notice how the profile screen is now showing ReloadedUser and John Doe. The profile screen changed the local profile data but did not change the shared view model.

Profile Reload

This is the home screen again after the reload profile button was clicked. Notice how the information on the home screen is the same as it was before the reload button was clicked. That is because the home screen is using the shared view model.

Home to Settings Login

Benefits of This Structure

  • Organization:
    • Each screen has its own file and ViewModel (like each room having its own purpose)
    • Navigation logic is separated from UI and business logic (like having a map separate from the furniture)
    • Routes are centralized in one place (like having all room names on one sign)
    • State management is clearly separated between shared and screen-specific (like knowing what's private vs. what everyone can see)
  • Maintainability:
    • Easy to find and modify specific screens and their logic (like knowing exactly which room to fix)
    • Changes to one screen don't affect others (like remodeling one room without touching others)
    • Clear separation of concerns between UI, navigation, and state (like having separate contractors for plumbing, electrical, and painting)
    • State changes are predictable and traceable (like knowing exactly who moved the furniture)
  • Scalability:
    • Easy to add new screens with their own ViewModels (like adding new rooms to your house)
    • Simple to implement screen-specific features (like adding a TV to the living room)
    • Better support for team development (like having different people work on different rooms)
    • Easy to add shared state when needed (like adding a family calendar when you have kids)
  • State Management:
    • Clear distinction between shared and screen-specific state (like knowing what's private vs. what's shared)
    • State survives configuration changes (like your furniture staying in place when you rotate the house)
    • Easy to track state changes (like knowing who moved what and when)
    • Predictable state updates through ViewModels (like having a system for organizing everything)

Best Practices

  • File Organization:
    • Keep route names in a central location (NavigationRoutes.kt) - like having all room numbers on one sign
    • Use consistent naming conventions for files - like using the same style for all room names
    • Group related screens and their ViewModels in their own packages - like organizing rooms by floor
    • Keep navigation logic separate from screen composables - like having a map separate from the furniture
  • State Management:
    • Use ViewModels for all screen-specific state - like having a personal organizer in each room
    • Use shared ViewModels for state that needs to be accessed by multiple screens - like having a family calendar
    • Keep state as close as possible to where it's used - like keeping your clothes in your bedroom, not the kitchen
    • Use mutableStateOf for all state that should trigger UI updates - like having automatic lights that turn on when you enter a room
  • Navigation:
    • Use type-safe navigation arguments - like having clear labels on doors
    • Provide the shared ViewModel to all screens that need it - like making sure everyone has access to the family calendar
    • Keep navigation callbacks simple and focused - like having clear, simple directions between rooms
    • Use constants for route names to prevent typos - like using printed room numbers instead of handwritten ones
  • UI Updates:
    • Show loading states during async operations - like having a "please wait" sign while work is being done
    • Handle error states gracefully - like having a backup plan when something goes wrong
    • Keep UI components focused on display and user interaction - like having furniture that's both beautiful and functional
    • Use proper state holders for different types of state - like having the right storage for different types of items

Common Mistakes to Avoid

  • State Management:
    • Putting all state in one ViewModel when it should be split - like putting all your stuff in one giant closet instead of organizing it
    • Not using ViewModels for screen-specific state - like not having any storage in your rooms
    • Forgetting to handle loading and error states - like not having any signs to tell people what's happening
    • Not using mutableStateOf for reactive state - like having manual switches instead of automatic lights
  • Navigation:
    • Scattering route names throughout the code - like having room numbers written in random places
    • Not providing shared ViewModels to screens that need them - like not giving everyone access to the family calendar
    • Using string literals for navigation instead of constants - like writing room names by hand instead of using printed signs
    • Not handling navigation arguments properly - like not reading the name on the note you're passing
  • File Organization:
    • Putting all composables in one file - like putting all your furniture in one giant room
    • Mixing navigation logic with UI code - like putting the house map inside a piece of furniture
    • Not following a clear package structure - like having rooms scattered randomly instead of organized by floor
    • Not separating ViewModels from composables - like putting your personal organizer inside your furniture instead of having it separate

Tips for Success

  • Planning:
    • Start with a clear file structure from the beginning - like drawing a house plan before building
    • Plan your state management strategy early - like deciding where to put storage before moving in
    • Identify which state needs to be shared - like figuring out what everyone needs to know vs. what's private
    • Document your navigation structure - like keeping a map of your house
  • Implementation:
    • Use constants for route names - like using printed room numbers
    • Keep screen composables focused on UI - like keeping furniture focused on being useful and beautiful
    • Use ViewModels for all state management - like having proper storage in every room
    • Follow consistent naming patterns - like using the same style for all room names
  • Testing:
    • Test ViewModels independently of UI - like testing your storage without worrying about the furniture
    • Verify state changes work as expected - like making sure things stay where you put them
    • Test navigation flows thoroughly - like walking through your house to make sure all paths work
    • Ensure state survives configuration changes - like making sure your furniture stays in place when you rotate the house