# How ViewModels Are Scoped and Shared Across Feature Modules in gsygithubappcompose

> Discover how gsygithubappcompose scopes and shares ViewModels across feature modules using Hilt. Learn about BaseViewModel inheritance and singleton repositories for efficient code sharing.

- Repository: [Shuyu Guo/gsygithubappcompose](https://github.com/carguo/gsygithubappcompose)
- Tags: internals
- Published: 2026-02-26

---

**In gsygithubappcompose, each feature module's ViewModel is scoped to the navigation back-stack entry via Hilt's `hiltViewModel()` composable, while common logic is shared through a `BaseViewModel` inheritance hierarchy and cross-feature data flows through singleton-scoped repositories.**

The gsygithubappcompose project demonstrates a modular architecture where each screen resides in its own feature module with its own ViewModel. Understanding how ViewModels are scoped and shared across these modules is essential for maintaining clean separation of concerns while avoiding memory leaks. This article examines the specific implementation patterns used in the repository to manage ViewModel lifecycles and share state between features using Hilt and Jetpack Compose Navigation.

## Scoping ViewModels to Navigation Destinations

Each screen in gsygithubappcompose lives in a dedicated feature module (e.g., `profile`, `search`, `list`). The ViewModel for a screen is created with **Hilt** using the `@HiltViewModel` annotation and obtained inside the composable via the `hiltViewModel()` helper function.

### The NavBackStackEntry Lifecycle

The `hiltViewModel()` function is a **Compose-aware** utility that binds the ViewModel to the **navigation back-stack entry** of the composable's `NavHost`. The ViewModel exists while its screen is on the back stack and is automatically cleared when the user navigates away. This guarantees a fresh instance per navigation destination and prevents memory leaks.

In [`ProfileScreen.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/ProfileScreen.kt), the ViewModel is obtained as a default parameter scoped to the current navigation entry:

```kotlin
@Composable
fun ProfileScreen(
    viewModel: ProfileViewModel = hiltViewModel()   // ← scoped to this NavBackStackEntry
) { … }

```

*(source: [ProfileScreen.kt](/carguo/gsygithubappcompose/blob/master/feature/profile/src/main/java/com/shuyu/gsygithubappcompose/feature/profile/ProfileScreen.kt#L24-L28))*

### Hilt Integration

All feature ViewModels are annotated with `@HiltViewModel`, which instructs Hilt to provide them as **AndroidX ViewModel** instances that live in the same scope as the associated `NavBackStackEntry`.

```kotlin
@HiltViewModel
class ProfileViewModel @Inject constructor(
    private val userRepository: UserRepository,
    …
) : BaseProfileViewModel( … )

```

*(source: [ProfileViewModel.kt](/carguo/gsygithubappcompose/blob/master/feature/profile/src/main/java/com/shuyu/gsygithubappcompose/feature/profile/ProfileViewModel.kt#L17-L23))*

## Sharing Common Logic Through Base Classes

While feature modules do not share the same ViewModel instance, they reuse common functionality through inheritance and composition.

### BaseViewModel for Shared Behavior

The `BaseViewModel` class implements pagination, error handling, and toast emission logic. All feature ViewModels inherit from this base class (or specialized variants like `BaseProfileViewModel`). This eliminates code duplication across modules while keeping each ViewModel scoped to its specific screen.

```kotlin
// Located in data module, shared across all features
abstract class BaseViewModel<State> : ViewModel() {
    // Common pagination, error handling, and toast logic
}

```

*(source: [BaseViewModel.kt](/carguo/gsygithubappcompose/blob/master/data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/vm/BaseViewModel.kt#L1-L30))*

### BaseScreen Composable Wrapper

The generic `BaseScreen` composable receives any `BaseViewModel` subclass and automatically connects toast messages and loading states. This wrapper ensures consistent UI behavior across all feature modules without requiring each screen to manually implement common UI concerns.

```kotlin
@Composable
fun <VM : BaseViewModel<*>> BaseScreen(viewModel: VM, content: @Composable () -> Unit) {
    // Toast handling and other common UI concerns
    content()
}

```

*(source: [BaseScreen.kt](/carguo/gsygithubappcompose/blob/master/data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/vm/BaseScreen.kt#L16-L30))*

## Cross-Feature Data Sharing via Singleton Repositories

When two features need the same data (e.g., the current logged-in user), they read from the same injected repository rather than from each other's ViewModel. Repositories like `UserRepository` and `NotificationRepository` are defined as **singleton-scoped** Hilt bindings in `RepositoryModule`. Every ViewModel receives the same repository instance, so data fetched in one feature is instantly available to another without explicit ViewModel sharing.

### Repository Injection Pattern

The `MainViewModel` demonstrates how ViewModels access shared data through singleton repositories:

```kotlin
class MainViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {
    val isLoggedIn: Flow<Boolean> = userRepository.isLoggedIn()
}

```

*(source: [MainViewModel.kt](/carguo/gsygithubappcompose/blob/master/app/src/main/java/com/shuyu/gsygithubappcompose/MainViewModel.kt#L9-L15))*

### Feature Module Implementation

Feature ViewModels declare their dependencies through constructor injection, receiving the same singleton repository instances:

```kotlin
@HiltViewModel
class SearchViewModel @Inject constructor(
    private val repository: SearchRepository,
    prefs: UserPreferencesDataStore,
    stringRes: StringResourceProvider
) : BaseViewModel<SearchUiState>( … )

```

*(source: [SearchViewModel.kt](/carguo/gsygithubappcompose/blob/master/feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchViewModel.kt#L1-L8))*

When used in the UI layer, the ViewModel is scoped to the navigation entry while leveraging the shared repository:

```kotlin
@Composable
fun SearchScreen(
    viewModel: SearchViewModel = hiltViewModel()   // scoped to NavBackStackEntry
) {
    val uiState by viewModel.uiState.collectAsState()
    BaseScreen(viewModel = viewModel) {
        // UI that consumes uiState
    }
}

```

*(source: [SearchScreen.kt](/carguo/gsygithubappcompose/blob/master/feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt#L20-L27))*

## Summary

- **Navigation-scoped lifecycles**: ViewModels are bound to `NavBackStackEntry` via `hiltViewModel()`, ensuring automatic cleanup when users navigate away.
- **Inheritance for logic sharing**: `BaseViewModel` and specialized variants provide pagination, error handling, and toast functionality to all feature modules without sharing ViewModel instances.
- **Composition for UI consistency**: The `BaseScreen` generic composable wraps every screen to handle common UI concerns like toast display.
- **Singleton repositories for data**: Repositories defined in `RepositoryModule` with `@Singleton` scope enable cross-feature data sharing without coupling ViewModels directly.

## Frequently Asked Questions

### How long does a ViewModel live in gsygithubappcompose?

A ViewModel lives as long as its associated screen remains on the navigation back stack. When the user navigates away and the `NavBackStackEntry` is destroyed, Hilt automatically clears the ViewModel to prevent memory leaks.

### Can two feature modules share the same ViewModel instance?

No, each feature module maintains its own ViewModel instance scoped to its specific navigation destination. Instead of sharing ViewModels, features share data through singleton-scoped repositories like `UserRepository` that are injected into each ViewModel.

### How does the app handle common UI logic like loading states and toast messages?

Common UI logic is handled through the `BaseViewModel` inheritance hierarchy for state management and the `BaseScreen` composable wrapper for UI presentation. `BaseScreen` automatically connects toast messages from any `BaseViewModel` subclass, eliminating duplicate code across feature modules.

### Where are the repository bindings defined?

Repository bindings are defined in `RepositoryModule` using Hilt's dependency injection framework. They are annotated with `@Singleton` and `@InstallIn(SingletonComponent::class)` to ensure that all ViewModels across different feature modules receive the same repository instance.