Week 7: Navigation Compose, Truck List & Real‐Time Data - Codeman239/TruckMate GitHub Wiki

Overview

Week 7 replaces any remaining XML navigation with Jetpack Navigation Compose, adds a Truck List and Truck Detail flow backed by Firestore real-time listeners, and includes a simple Settings screen persisted with SharedPreferences.

This page documents:

  • Gradle changes needed
  • File locations
  • Nav graph + NavHost setup
  • TruckRepository (Firestore realtime)
  • Compose screens: TruckListScreen, TruckDetailScreen, SettingsScreen
  • Firestore document schema
  • Test/verification steps
  • Changelog

1. Gradle / Dependencies

Project-level (build.gradle.kts)

Ensuref the Google Services plugin is present. No change required here except keeping versions consistent.

App-module (app/build.gradle.kts)

Added / verified these dependencies:

plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) id("com.google.gms.google-services") }

android { namespace = "com.codyhassey.truckmate" compileSdk = 36 defaultConfig { /* ... */ } buildFeatures { compose = true } kotlinOptions { jvmTarget = "11" } }

dependencies { // Compose + Activity implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.activity.compose) implementation(libs.androidx.ui) implementation(libs.androidx.ui.tooling)

// Navigation Compose implementation("androidx.navigation:navigation-compose:2.7.0")

// Firebase implementation("com.google.firebase:firebase-auth-ktx:22.1.0") implementation("com.google.firebase:firebase-firestore-ktx:24.7.0")

// Coroutines (used by repository) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") }

  1. Project structure (new files) app/src/main/java/com/codyhassey/truckmate/ ├─ MainActivity.kt ├─ data/ │ └─ repository/ │ └─ TruckRepository.kt ├─ model/ │ └─ Truck.kt ├─ ui/ │ ├─ nav/ │ │ └─ NavGraph.kt │ ├─ trucks/ │ │ ├─ TruckListScreen.kt │ │ └─ TruckDetailScreen.kt │ └─ settings/ │ └─ SettingsScreen.kt

  2. Data model & repository 3.1 Truck.kt (model) Path: app/src/main/java/com/codyhassey/truckmate/model/Truck.kt package com.codyhassey.truckmate.model

import com.google.firebase.Timestamp

data class Truck( val id: String = "", val name: String = "", val status: String = "", // e.g. "online", "idle", "maintenance" val location: String = "", // free-text or "lat,lng" val lastSeen: Timestamp? = null )

3.2 TruckRepository.kt (Firestore realtime) Path: app/src/main/java/com/codyhassey/truckmate/data/repository/TruckRepository.kt package com.codyhassey.truckmate.data.repository

import com.codyhassey.truckmate.model.Truck import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.ListenerRegistration import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow

class TruckRepository { private val db = FirebaseFirestore.getInstance() private val _trucks = MutableStateFlow<List<Truck>>(emptyList()) val trucks = _trucks.asStateFlow()

private var listener: ListenerRegistration? = null

// Start listening to the 'trucks' collection in real-time
fun startListening() {
    listener = db.collection("trucks")
        .addSnapshotListener { snapshot, error -&gt;
            if (error != null) {
                // optionally handle error
                return@addSnapshotListener
            }
            val list = snapshot?.documents?.mapNotNull { doc -&gt;
                doc.id.let { id -&gt;
                    val name = doc.getString("name") ?: ""
                    val status = doc.getString("status") ?: ""
                    val location = doc.getString("location") ?: ""
                    val lastSeen = doc.getTimestamp("lastSeen")
                    Truck(id = id, name = name, status = status, location = location, lastSeen = lastSeen)
                }
            } ?: emptyList()
            _trucks.value = list
        }
}

fun stopListening() {
    listener?.remove()
    listener = null
}

// Single truck fetch (one-time)
fun getTruckById(truckId: String, callback: (Truck?) -&gt; Unit) {
    db.collection("trucks").document(truckId).get()
        .addOnSuccessListener { doc -&gt;
            if (doc.exists()) {
                val truck = Truck(
                    id = doc.id,
                    name = doc.getString("name") ?: "",
                    status = doc.getString("status") ?: "",
                    location = doc.getString("location") ?: "",
                    lastSeen = doc.getTimestamp("lastSeen")
                )
                callback(truck)
            } else callback(null)
        }.addOnFailureListener { _ -&gt; callback(null) }
}

// Write/update truck
fun updateTruck(truckId: String, updates: Map&lt;String, Any&gt;, onComplete: (Boolean) -&gt; Unit) {
    db.collection("trucks").document(truckId).update(updates)
        .addOnSuccessListener { onComplete(true) }
        .addOnFailureListener { onComplete(false) }
}

}

  1. Navigation (NavGraph) 4.1 NavGraph.kt app/src/main/java/com/codyhassey/truckmate/ui/nav/NavGraph.kt package com.codyhassey.truckmate.ui.nav

import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.codyhassey.truckmate.data.repository.TruckRepository import com.codyhassey.truckmate.ui.auth.LoginScreen import com.codyhassey.truckmate.ui.home.HomeScreen import com.codyhassey.truckmate.ui.settings.SettingsScreen import com.codyhassey.truckmate.ui.trucks.TruckDetailScreen import com.codyhassey.truckmate.ui.trucks.TruckListScreen

@Composable fun NavGraph( startDestination: String = "home", onSignOut: () -> Unit, onLoginSuccess: (/* FirebaseUser or userEmail type as desired */ String) -> Unit ) { val navController = rememberNavController() val repo = remember { TruckRepository() }

NavHost(navController = navController, startDestination = startDestination) {
    composable("home") {
        HomeScreen(
            userEmail = "", // supply actual value if needed
            onNavigateToTrucks = { navController.navigate("trucks") },
            onNavigateToSettings = { navController.navigate("settings") },
            onSignOut = { onSignOut() }
        )
    }

    composable("trucks") {
        // Start listening when the list screen is shown
        repo.startListening()
        TruckListScreen(
            trucksFlow = repo.trucks,
            onSelectTruck = { id -&gt; navController.navigate("trucks/detail/$id") }
        )
    }

    composable(
        route = "trucks/detail/{truckId}",
        arguments = listOf(navArgument("truckId") { type = NavType.StringType })
    ) { backStackEntry -&gt;
        val truckId = backStackEntry.arguments?.getString("truckId") ?: ""
        TruckDetailScreen(truckId = truckId, repo = repo, onBack = { navController.popBackStack() })
    }

    composable("settings") {
        SettingsScreen(onBack = { navController.popBackStack() })
    }
}

}

Notes: NavGraph is flexible — startDestination may be set to "home" or "auth" depending on your auth flow. HomeScreen here should expose navigation callbacks onNavigateToTrucks, etc.). If you prefer a single-activity auth gating, handle that in MainActivity.

  1. Compose screens 5.1 TruckListScreen.kt Path: app/src/main/java/com/codyhassey/truckmate/ui/trucks/TruckListScreen.kt package com.codyhassey.truckmate.ui.trucks

import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Card import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.StateFlow import com.codyhassey.truckmate.model.Truck

@Composable fun TruckListScreen( trucksFlow: StateFlow<List<Truck>>, onSelectTruck: (String) -> Unit ) { val trucks = trucksFlow.collectAsState().value

LazyColumn(modifier = Modifier.fillMaxSize().padding(8.dp)) {
    items(trucks) { truck -&gt;
        Card(modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
            .clickable { onSelectTruck(truck.id) }
        ) {
            Column(modifier = Modifier.padding(12.dp)) {
                Text(text = truck.name)
                Spacer(modifier = Modifier.height(4.dp))
                Text(text = "Status: ${truck.status}")
                Spacer(modifier = Modifier.height(2.dp))
                Text(text = "Location: ${truck.location}")
            }
        }
    }
}

}

5.2 TruckDetailScreen.kt Path: app/src/main/java/com/codyhassey/truckmate/ui/trucks/TruckDetailScreen.kt package com.codyhassey.truckmate.ui.trucks

import androidx.compose.foundation.layout.* import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.material3.OutlinedTextField import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.codyhassey.truckmate.data.repository.TruckRepository import com.codyhassey.truckmate.model.Truck

@Composable fun TruckDetailScreen(truckId: String, repo: TruckRepository, onBack: () -> Unit) { var truck by remember { mutableStateOf<Truck?>(null) } var status by remember { mutableStateOf("") } var location by remember { mutableStateOf("") }

LaunchedEffect(truckId) {
    repo.getTruckById(truckId) { t -&gt;
        t?.let {
            truck = it
            status = it.status
            location = it.location
        }
    }
}

Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
    truck?.let { t -&gt;
        Text(text = t.name)
        Spacer(Modifier.height(8.dp))
        OutlinedTextField(value = status, onValueChange = { status = it }, label = { Text("Status") })
        Spacer(Modifier.height(8.dp))
        OutlinedTextField(value = location, onValueChange = { location = it }, label = { Text("Location") })
        Spacer(Modifier.height(16.dp))
        Row {
            Button(onClick = {
                repo.updateTruck(t.id, mapOf("status" to status, "location" to location)) { success -&gt;
                    // optionally show feedback
                }
                onBack()
            }) { Text("Save") }
            Spacer(Modifier.width(8.dp))
            Button(onClick = { onBack() }) { Text("Back") }
        }
    } ?: Text("Loading...")
}

}

5.3 SettingsScreen.kt Path: app/src/main/java/com/codyhassey/truckmate/ui/settings/SettingsScreen.kt package com.codyhassey.truckmate.ui.settings

import android.content.Context import androidx.compose.foundation.layout.* import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.Button import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.platform.LocalContext

@Composable fun SettingsScreen(onBack: () -> Unit) { val ctx = LocalContext.current val prefs = ctx.getSharedPreferences("truckmate_prefs", Context.MODE_PRIVATE) var showOffline by remember { mutableStateOf(prefs.getBoolean("show_offline", true)) }

Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
    Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
        Text("Show offline trucks")
        Spacer(Modifier.width(8.dp))
        Switch(checked = showOffline, onCheckedChange = {
            showOffline = it
            prefs.edit().putBoolean("show_offline", it).apply()
        })
    }
    Spacer(Modifier.height(16.dp))
    Button(onClick = onBack) { Text("Back") }
}

}

  1. MainActivity + NavGraph wiring You can host the NavGraph from MainActivity. Replace or augment your current setContent block with: MainActivity setContent { TruckMateTheme { val auth = FirebaseAuth.getInstance() val prefs = getSharedPreferences("truckmate_prefs", Context.MODE_PRIVATE) val initialUser = if (prefs.getBoolean("is_logged_in", false)) auth.currentUser else null var user by remember { mutableStateOf(initialUser) }

    if (user == null) { LoginScreen { firebaseUser -> prefs.edit().putBoolean("is_logged_in", true).apply() user = firebaseUser } } else { NavGraph( startDestination = "home", onSignOut = { auth.signOut() prefs.edit().putBoolean("is_logged_in", false).apply() user = null }, onLoginSuccess = { _ -> /* not used when signed in here */ } ) } } }

  2. Firestore document schema & sample Collection: trucks Sample document (ID =truck-001)

"name": "Truck 001", "status": "online", "location": "40.7128,-74.0060", "lastSeen": <Firestore timestamp; }

Create a few sample docs in the Firestore console to test real-time updates.

⚠️ **GitHub.com Fallback** ⚠️