How ViewModels Are Scoped and Shared Across Feature Modules in gsygithubappcompose
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, the ViewModel is obtained as a default parameter scoped to the current navigation entry:
@Composable
fun ProfileScreen(
viewModel: ProfileViewModel = hiltViewModel() // ← scoped to this NavBackStackEntry
) { … }
(source: ProfileScreen.kt)
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.
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val userRepository: UserRepository,
…
) : BaseProfileViewModel( … )
(source: ProfileViewModel.kt)
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.
// Located in data module, shared across all features
abstract class BaseViewModel<State> : ViewModel() {
// Common pagination, error handling, and toast logic
}
(source: BaseViewModel.kt)
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.
@Composable
fun <VM : BaseViewModel<*>> BaseScreen(viewModel: VM, content: @Composable () -> Unit) {
// Toast handling and other common UI concerns
content()
}
(source: BaseScreen.kt)
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:
class MainViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
val isLoggedIn: Flow<Boolean> = userRepository.isLoggedIn()
}
(source: MainViewModel.kt)
Feature Module Implementation
Feature ViewModels declare their dependencies through constructor injection, receiving the same singleton repository instances:
@HiltViewModel
class SearchViewModel @Inject constructor(
private val repository: SearchRepository,
prefs: UserPreferencesDataStore,
stringRes: StringResourceProvider
) : BaseViewModel<SearchUiState>( … )
(source: SearchViewModel.kt)
When used in the UI layer, the ViewModel is scoped to the navigation entry while leveraging the shared repository:
@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)
Summary
- Navigation-scoped lifecycles: ViewModels are bound to
NavBackStackEntryviahiltViewModel(), ensuring automatic cleanup when users navigate away. - Inheritance for logic sharing:
BaseViewModeland specialized variants provide pagination, error handling, and toast functionality to all feature modules without sharing ViewModel instances. - Composition for UI consistency: The
BaseScreengeneric composable wraps every screen to handle common UI concerns like toast display. - Singleton repositories for data: Repositories defined in
RepositoryModulewith@Singletonscope 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →