Phase 2 step-by-step

Build a multi-screen business catalogue.

Outcome: a junior-level app with list, detail, favourites, settings, search, filters, and call/map/share actions.

Add navigation dependency

Open app/build.gradle.kts, add Navigation Compose, sync Gradle, and run the app once.

dependencies {
    implementation("androidx.navigation:navigation-compose:2.9.8")
}
Gradle sync succeeds. If Android Studio suggests a newer stable version, use that.

Create the catalogue model and sample data

Add a data class and five local records. Keep data local first so navigation and UI are easy to debug.

data class Business(
    val id: Int,
    val name: String,
    val category: String,
    val city: String,
    val phone: String,
    val address: String
)

val sampleBusinesses = listOf(
    Business(1, "Asha Clinic", "Clinic", "Shivamogga", "9876543210", "MG Road"),
    Business(2, "Future Steps School", "School", "Davanagere", "9876500001", "PB Road"),
    Business(3, "RankPlus Coaching", "Coaching", "Mysuru", "9876500002", "VV Mohalla")
)

Build the list screen first

Render the list without navigation. This proves the data model and row UI work.

@Composable
fun BusinessListScreen(
    businesses: List<Business>,
    onOpen: (Int) -> Unit
) {
    LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        items(businesses, key = { it.id }) { business ->
            Card(modifier = Modifier.fillMaxWidth().clickable { onOpen(business.id) }) {
                Column(modifier = Modifier.padding(12.dp)) {
                    Text(business.name, fontWeight = FontWeight.Bold)
                    Text("${business.category} • ${business.city}")
                }
            }
        }
    }
}
Checkpoint: all sample businesses render in a scrollable list.

Add NavHost and detail route

Move from one screen to two screens. Pass only the id through the route.

@Composable
fun CatalogueApp() {
    val navController = rememberNavController()
    NavHost(navController, startDestination = "list") {
        composable("list") {
            BusinessListScreen(sampleBusinesses) { id ->
                navController.navigate("detail/$id")
            }
        }
        composable("detail/{id}") { entry ->
            val id = entry.arguments?.getString("id")?.toIntOrNull()
            val business = sampleBusinesses.firstOrNull { it.id == id }
            BusinessDetailScreen(business, onBack = { navController.popBackStack() })
        }
    }
}
Tapping a row opens detail; Back returns to the list.

Add search and category filters

Keep filtering as a pure function so it can be tested later.

fun filterBusinesses(items: List<Business>, query: String, category: String?): List<Business> {
    val clean = query.trim().lowercase()
    return items.filter {
        val queryMatch = clean.isBlank() ||
            it.name.lowercase().contains(clean) ||
            it.city.lowercase().contains(clean)
        val categoryMatch = category == null || it.category == category
        queryMatch && categoryMatch
    }
}
Checkpoint: searching “mys” shows Mysuru rows; selecting Clinic hides non-clinic rows.

Add favourites

Use a set of ids. Persist it later with Room; first make the flow correct.

var favourites by remember { mutableStateOf(setOf<Int>()) }

fun toggleFavourite(id: Int) {
    favourites = if (id in favourites) favourites - id else favourites + id
}
Rows can be saved and the favourites screen shows only saved businesses.

Add device actions

Use external intents for actions Android already supports.

fun openDialer(context: Context, phone: String) {
    context.startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:$phone")))
}

fun openMap(context: Context, address: String) {
    val uri = Uri.parse("geo:0,0?q=${Uri.encode(address)}")
    context.startActivity(Intent(Intent.ACTION_VIEW, uri))
}
Checkpoint: detail screen has Call, Map, and Share buttons. Missing apps on emulator are documented as known limitations.

Add app states

Do not show a blank area. A junior app needs visible success, empty, and error states.

sealed interface CatalogueState {
    data object Loading : CatalogueState
    data class Success(val businesses: List<Business>) : CatalogueState
    data object Empty : CatalogueState
    data class Error(val message: String) : CatalogueState
}

Common failures

Navigation symbols unresolvedSync Gradle after adding navigation-compose and import navigation.compose APIs.
Back button exits appUse navController.popBackStack() on detail back action.
Favourites resetExpected for this step. Phase 3 introduces persistence and repository ownership.
Map does not openThe emulator may not have a map app. Test real device or handle ActivityNotFoundException later.

Checkpoint quiz

Open quiz lab