Week 7: Navigation Compose, Truck List & Real‐Time Data - Codeman239/TruckMate GitHub Wiki
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
Ensuref the Google Services plugin is present. No change required here except keeping versions consistent.
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") }
-
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
-
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 ->
if (error != null) {
// optionally handle error
return@addSnapshotListener
}
val list = snapshot?.documents?.mapNotNull { doc ->
doc.id.let { id ->
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?) -> Unit) {
db.collection("trucks").document(truckId).get()
.addOnSuccessListener { doc ->
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 { _ -> callback(null) }
}
// Write/update truck
fun updateTruck(truckId: String, updates: Map<String, Any>, onComplete: (Boolean) -> Unit) {
db.collection("trucks").document(truckId).update(updates)
.addOnSuccessListener { onComplete(true) }
.addOnFailureListener { onComplete(false) }
}
}
- 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 -> navController.navigate("trucks/detail/$id") }
)
}
composable(
route = "trucks/detail/{truckId}",
arguments = listOf(navArgument("truckId") { type = NavType.StringType })
) { backStackEntry ->
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.
- 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 ->
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 ->
t?.let {
truck = it
status = it.status
location = it.location
}
}
}
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
truck?.let { t ->
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 ->
// 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") }
}
}
-
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 */ } ) } } }
-
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.