Skip to content

sergenes/restaurantdiscovery

Repository files navigation

Lunchtime Restaurant Discovery

License: MIT Platform Language Latest Tag

Lunchtime: A Restaurant Discovery App Showcasing Android Best Practices (Playground)

Brief Project Description

Lunchtime is a modern Android application built with best practices and latest technologies. This project was created using the default Android Studio New App template (Empty Activity) and implemented as a Kotlin Jetpack Compose project.

Original SPEC (Required Features)

  • The app will use the Google Places API as its data source.
  • The app will prompt the user for permission to access their current location.
  • Upon launch, the app will execute a search and display nearby restaurants.
  • A search feature will allow the user to search for specific restaurants.
  • Users can choose to display search results as a list or as pins on a map.
  • Selecting a search result will display basic information about the restaurant.
  • Third-party libraries may be used at the developer's discretion, but the Google Places API client must not be used.
  • Users can flag restaurants as favorites, with favorite status reflected in current and future search results.

Build Instruction

For security best practices, sensitive information such as the Google Places API key is stored in local.properties (referenced via BuildConfig) and excluded from GitHub.

How to obtain the API key:

  1. Go to the Google Cloud Console.
  2. Create or select a project.
  3. Enable the Places API (New).
  4. Go to APIs & Services > Credentials and create an API key.
  5. Add google.places.api.key=YOUR_API_KEY to your local.properties file.

Environment

  • Android Studio Ladybug | 2024.2.1 Patch 2
  • Gradle Version: 8.9
  • Android Gradle Plugin: 8.13.2
  • Kotlin: 2.0.0
  • Compile SDK: 36
  • Min SDK: 24
  • Target SDK: 36

Testing

The project includes comprehensive test coverage focusing on meaningful tests rather than just coverage metrics:

Unit Tests (Fast, no device needed):

./gradlew test
  • GetRestaurantsUseCaseTest - Business logic (distance sorting)
  • SearchViewModelTest - Debouncing behavior and search flow
  • BaseViewModelTest - Shared loading/error handling utilities
  • FavoritesViewModelTest - Toggle favorites logic
  • DestinationsTest - Navigation serialization
  • LocationViewModelTest - Location update flow and refresh behavior
  • LocationPermissionViewModelTest - Permission state machine and callbacks

Android Instrumentation Tests (Requires device/emulator):

./gradlew connectedAndroidTest
  • FavoritesDataSourceTest - Real DataStore persistence and concurrent operations

Project Structure

The project is organized into three Gradle modules following Clean Architecture:

:app          — Presentation layer + DI wiring (depends on :domain and :data)
:domain       — Pure business logic, no Android framework imports (no dependencies)
:data         — Data sources and repository implementations (depends on :domain)

:domain module

domain/src/main/java/com/nes/lunchtime/domain/
├── Restaurant.kt                              # Domain models (Restaurant, PlaceDetails)
├── RestaurantsRepository.kt                   # Repository interface (contract)
└── GetRestaurantsUseCase.kt                   # Business logic: fetch & sort by distance

:data module

data/src/main/java/com/nes/lunchtime/
├── data/
│   ├── remote/
│   │   ├── RestaurantsRepository.kt           # RestaurantsRepositoryImpl
│   │   ├── GooglePlacesClient.kt              # Ktor HTTP client for Places API
│   │   └── model/
│   │       ├── Request.kt                     # API request payloads
│   │       └── Response.kt                    # API response models
│   ├── local/
│   │   ├── FavoritesDataSource.kt             # DataStore read/write
│   │   └── FavoritesRepository.kt             # Favorites data access
│   └── location/
│       ├── LocationRepository.kt              # GPS state via FusedLocationProvider
│       ├── LocationPermissionManager.kt       # Runtime permission helpers
│       └── LocationUtils.kt                   # Distance calculation utilities
└── di/
    ├── DataStoreModule.kt                     # DataStore preferences
    └── LocationModule.kt                      # FusedLocationProviderClient

:app module

app/src/main/java/com/nes/lunchtime/
├── app/
│   └── LunchTimeApp.kt                        # Hilt Application class
├── data/
│   └── remote/
│       └── RestaurantsRepository.kt           # Binds RestaurantsRepository interface → impl
├── di/
│   ├── AppModule.kt                           # App-level Hilt module
│   ├── NetworkModule.kt                       # HttpClient (Ktor)
│   └── ApiKeyModule.kt                        # API key from BuildConfig
└── ui/                                        # Presentation layer (Compose + MVVM)
    ├── MainActivity.kt
    ├── base/
    │   └── BaseViewModel.kt                   # Shared executeWithLoading helper
    ├── components/                            # Reusable composables
    │   ├── BrandedAppHeader.kt
    │   ├── RestaurantCard.kt
    │   ├── RestaurantImage.kt
    │   ├── CircularIndicator.kt
    │   └── ViewSwitcherButton.kt
    ├── home/
    │   ├── HomeScreen.kt
    │   ├── nearby/NearByViewModel.kt          # SharedFlow + transformLatest pattern
    │   ├── search/SearchViewModel.kt          # 500ms debounce
    │   ├── favorites/FavoritesViewModel.kt
    │   ├── list/RestaurantListView.kt
    │   └── map/RestaurantMapView.kt
    ├── details/
    │   ├── DetailsScreen.kt
    │   └── DetailsViewModel.kt
    ├── location/
    │   ├── LocationPermissionDialog.kt
    │   ├── LocationPermissionViewModel.kt     # Activity-scoped, permission state machine
    │   └── LocationViewModel.kt               # HomeScreen-scoped, continuous GPS updates
    ├── navigation/
    │   └── Destinations.kt                    # Type-safe nav routes (Kotlin Serialization)
    └── theme/
        ├── Color.kt
        ├── Type.kt
        ├── Dimens.kt
        └── Theme.kt

Module dependency rules:

  • :domain has zero dependencies on :data or :app
  • :data depends on :domain (implements its repository interfaces)
  • :app depends on :domain (use cases, models) and :data (for Hilt wiring)
  • UI code in :app never imports :data classes directly — only domain types cross the boundary

Architecture & Design Patterns

  • Clean Architecture with clear separation of concerns:

    • Data Layer (Repository Pattern)
    • Domain Layer (Use Cases/Business Logic)
    • Presentation Layer (MVVM with ViewModels)
  • Type-Safe Navigation: Uses the latest Jetpack Navigation (2.8.0+) with Kotlin Serialization for compile-time safe routing

  • Single Activity architecture using Jetpack Compose

  • Unidirectional Data Flow using StateFlow, and actions are passed up via lambdas

  • State Management using sealed classes for UI states

  • State Encapsulation in ViewModels (MutableStateFlow field is always private)

  • Error Handling with Result and centralized error state management

  • Dependency Injection: Powered by Hilt for modular and testable code

  • BaseViewModel Pattern: Eliminates code duplication with shared executeWithLoading helper for consistent loading/error handling

  • Centralized Design System: Dimens.kt for consistent spacing and eliminating magic numbers

  • Search Debouncing: 500ms debounce to reduce API calls while user types

  • Scoped ViewModel Separation: Permission and location concerns are split into two focused ViewModels with different lifetimes:

    • LocationPermissionViewModel (Activity-scoped) — owns the permission state machine; MainActivity only recomposes when permission state changes (grant/deny), which is rare
    • LocationViewModel (HomeScreen-scoped) — owns continuous GPS updates; starts immediately on composition since permission is already guaranteed by the time HomeScreen is shown
  • Compose Recomposition Scoping: Location ticks only recompose the composables that actually consume location:

    • HomeScreenLayout is a pure layout composable with a restaurantContent slot — it has no location parameter and is unaffected by GPS updates
    • RestaurantContent collects locationState directly from LocationViewModel; only the map pin position (RestaurantMapView) redraws on each tick — the list, search bar, and scaffold are skipped by Compose
    • currentViewType (list vs. map) is hoisted to HomeScreenContent with rememberSaveable, preserving the user's view choice across NearBy loading cycles and location refreshes
  • Fresh GPS on Every Session: LocationRepository uses getCurrentLocation() (not lastLocation) as the initial emission, avoiding the stale OS-level cache that persists across app restarts

1. Navigation & Permission Flow

graph TD
    MA[MainActivity] --> PVM[LocationPermissionViewModel]
    PVM -- Granted --> NavHost[NavHost / Type-Safe Routes]
    PVM -- Loading/Denied/Error --> PermUI[Permission & Error UI]
    NavHost --> HS[HomeScreen]
    NavHost --> DS[DetailsScreen]
Loading

2. Architecture Overview

graph TB
    subgraph Presentation["Presentation Layer (UI)"]
        HS[HomeScreen<br/>Jetpack Compose]
        DS[DetailsScreen<br/>Jetpack Compose]
        VM1[NearByViewModel]
        VM2[SearchViewModel]
        VM3[FavoritesViewModel]
        VM4[DetailsViewModel]
        VM5[LocationViewModel]
        VM6[LocationPermissionViewModel]

        HS --> VM1
        HS --> VM2
        HS --> VM3
        HS --> VM5
        DS --> VM4
    end

    subgraph Domain["Domain Layer (Business Logic)"]
        UC[GetRestaurantsUseCase]
        M1[Restaurant Model]
        M2[PlaceDetails Model]
        RI[RestaurantsRepository<br/>Interface]

        VM1 --> UC
        VM2 --> UC
        VM4 --> RI
        UC --> RI
    end

    subgraph Data["Data Layer (Repositories)"]
        RP[RestaurantsRepositoryImpl]
        FR[FavoritesRepository]
        LR[LocationRepository]

        VM3 --> FR
        VM5 --> LR
        RI -. impl .-> RP
    end

    subgraph DataSources["Data Sources"]
        GPC[GooglePlacesClient<br/>Ktor HTTP]
        FDS[FavoritesDataSource<br/>DataStore]
        FLP[FusedLocationProviderClient<br/>Location API]

        RP --> GPC
        FR --> FDS
        LR --> FLP
    end

    subgraph External["External Services"]
        API[Google Places API<br/>REST]
        GPC --> API
    end

    style Presentation fill:#e3f2fd
    style Domain fill:#fff9c4
    style Data fill:#f3e5f5
    style DataSources fill:#e8f5e9
    style External fill:#ffebee
Loading

Key Technologies & Libraries

  • UI:
    • Jetpack Compose for declarative UI
    • Material Design 3 components
    • Google Maps Compose
  • Networking:
    • Ktor Client for HTTP requests
      • Native coroutines support
      • Lightweight and flexible compared to Retrofit
      • Easy configuration and interceptors
    • Coil Compose for image loading and caching
      • Built specifically for Compose with native integration
      • Memory and disk caching out of the box
      • Coroutines-based image loading
      • Smaller footprint compared to Glide/Picasso
    • Kotlin Serialization for JSON parsing
      • Compile-time type safety
      • Better performance than Gson/Moshi
      • Native Kotlin support with less boilerplate
      • Direct integration with Ktor
  • Dependency Injection:
    • Hilt
  • Asynchronous Operations:
    • Kotlin Coroutines & Flow
  • Data Persistence:
    • DataStore for lightweight favorites storage
  • Location-based services:
  • Testing:
    • Unit tests with JUnit4
    • MockK for mocking
    • Coroutines test utilities
    • Custom test rules for coroutines testing

Screenshots

List View Map View Details View

References to Documentation for Libraries and APIs Used

Walkthrough of the Implementation

At its core, this app uses the Google Places API to fetch and display nearby restaurants based on the user's location.

When you launch the app, it starts by asking for location permission in a user-friendly way, explaining why it's needed. Once permission is granted, the app fetches nearby restaurants and displays them. There's also a real-time search feature, so users can find specific places, and they can switch between a list view and a map view, with pins placed on the map.

Selecting a restaurant brings up basic details like name, address, and ratings. A cool bonus: users can favorite restaurants, and that status is saved.

The app follows Clean Architecture principles, splitting the codebase into three layers:

  • Data Layer using the Repository Pattern for clean data handling.
  • Domain Layer, where all the business logic lives in reusable use cases.
  • Presentation Layer, built with Jetpack Compose and MVVM, which keeps the UI reactive and maintainable.
  • For state management, StateFlow is used with a unidirectional data flow, which simplifies updates and keeps things predictable. Errors are managed using a Result wrapper, and sealed classes help define clear UI states.

Location handling is split into two focused ViewModels. LocationPermissionViewModel lives at the Activity scope and manages the permission lifecycle — the Activity only recomposes when permission state actually changes, which is rare. Once permission is granted, LocationViewModel takes over at the HomeScreen scope and drives continuous GPS updates via a callbackFlow-wrapped FusedLocationProviderClient. Each GPS tick only recomposes RestaurantContent (where the map pin lives), leaving the scaffold, search bar, and restaurant list untouched.

The tech stack includes:

  • Ktor Client for networking—lightweight, flexible, and coroutine-friendly.
  • Coil for image loading—optimized for Compose with caching baked in.
  • DataStore for simple favorites persistence.
  • Hilt for dependency injection.
  • FusedLocationProvider for battery-efficient location tracking.

The implementation intentionally keeps things simple—no Room database for caching, no offline support—but these would be straightforward to add if needed. The architecture is designed to support these extensions. Testing was also a focus. Unit tests are written with JUnit4 and dependencies are mocked with MockK, using coroutine testing tools for asynchronous workflows. In summary, this app demonstrates best practices in production-quality Android apps using modern tools and patterns. It's clean, maintainable, performant, and thoroughly tested.

Contact

Connect and follow me on LinkedIn: Sergey N

About

Lunchtime: A Restaurant Discovery App Showcasing Android Best Practices (Playground)

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages