BaseViewModel Pattern for Multi-Module Android Apps
A common pattern in Android applications is the “base” ViewModel. It contains functionality that all, or many, ViewModels share, and that…
BaseViewModel Pattern for Multi-Module Android Apps

A common pattern in Android applications is the “base” ViewModel. It contains functionality that all, or many, ViewModels share, and that we don’t want to rewrite. However, this logic often requires injected dependencies, and modern dependency injection frameworks like Hilt don’t support direct injection into subclasses.
How can we implement this core logic without exposing every dependency to each child ViewModel implementation? And how can we keep these ViewModels testable without adding significant overhead to our test classes?
In this article we’ll break down whether your app needs a BaseViewModel, and examine a practical way to implement this pattern in multi-module apps.
Should I have a BaseViewModel in my app?
Not every app needs a BaseViewModel. In fact, this pattern can be considered a code smell when implemented incorrectly.
You should have a base ViewModel when:
- All ViewModels share a specific function, or set of functions.
- All ViewModels need (or should have) access to a set of core dependencies.
- You’re worried about long-term maintainability, scalability, and potential refactors.
You should NOT have a base ViewModel when:
- Your ViewModels don’t share any specific functions or callbacks.
- Your ViewModels don’t share dependencies.
- Scalability and large-scale refactors aren’t immediate concerns (smaller projects).
Preliminary Setup (Hilt, Compose)
This example uses Hilt dependency injection. If you haven’t used Hilt before, I recommend reading this tutorial.
For our app, we take the following steps:
- Create a
@HiltApplication, and add its name to AndroidManifest.xml. - Make our MainActivity an
@AndroidEntryPoint - Apply the
hiltandkspplugins in the appropriatebuild.gradle.ktsfiles. - Import Hilt as a dependency and annotation processor in the required modules.
@HiltAndroidApp
class BaseViewModelApp : Application()
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
BaseviewmodelTheme {
Scaffold(modifier = Modifier.fillMaxSize()) {
HomeScreen()
}
}
}
}
}
Gradle module structure
To demonstrate this pattern, we need to create three modules:
**:ui:feature**
Contains our feature-specific code. Requires Hilt DI.
**:ui:core**
Contains our BaseViewModel, alongside other shared UI components and utilities.
**:ui:test**
Contains helper functions for testing feature ViewModels. We create a separate module for testing to avoid importing Mockito into :ui:core as an implementation dependency. This isn't strictly necessary, but it is a good practice.
rootProject.name = "baseviewmodel"
include(":app")
include(":ui:feature")
include(":ui:core")
include(":ui:test")
**:ui:test should import `:ui:core`**.
**:ui:feature should import both `:ui:test** and:ui:core`.
dependencies {
implementation(project(":ui:core"))
testImplementation(project(":ui:test"))
implementation("com.google.dagger:hilt-android:2.59.2")
ksp("com.google.dagger:hilt-android-compiler:2.59.2")
implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
}
Note: This pattern can be implemented in a single-module app, but is more applicable to larger-scale projects.
Define BaseViewModel and its dependencies
Once our app is set up, we create two classes in :ui:core: BaseViewModel and BaseViewModelDependencies.
class BaseViewModelDependencies @Inject constructor(
val analyticsManager: AnalyticsManager,
val dialogEventLauncher: GlobalDialogManager,
val defaultDispatcher: CoroutineDispatcher
)
abstract class BaseViewModel(
private val dependencies: BaseViewModelDependencies
) : ViewModel() {
val analyticsManager = dependencies.analyticsManager
val defaultDispatcher = dependencies.defaultDispatcher
private val parentViewModelName = this::class.simpleName ?: "Unknown Screen"
fun onLaunch() {
analyticsManager.onScreenShown(screenName = parentViewModelName)
}
fun showGlobalErrorDialog(message: String) {
dependencies.dialogEventLauncher
}
}
Create a child ViewModel
The next step is to create one or more child ViewModels, realistically one for each screen. These should extend our base ViewModel and be created via Hilt dependency injection. BaseViewModelDependencies should be injected here and passed to the superclass constructor.
@HiltViewModel
class FeatureViewModel @Inject constructor(
dependencies: BaseViewModelDependencies
) : BaseViewModel(dependencies) {
data class ScreenState(
val number: Int = 0,
val loading: Boolean = false
)
private val _screenState = MutableStateFlow(ScreenState())
val screenState: StateFlow<ScreenState> = _screenState
fun addOneToCurrentState() {
val currentNumber = _screenState.value.number
if (currentNumber < 11) {
_screenState.update { it.copy(loading = true) }
viewModelScope.launch(defaultDispatcher) {
delay(1000)
_screenState.update {
it.copy(
number = it.number + 1,
loading = false
)
}
}
} else {
showGlobalErrorDialog("Max counter is 10")
}
}
}
Choose which base dependencies to expose
We can expose dependencies as BaseViewModel members. This should only be done for dependencies where child ViewModels may need broad access to the dependency’s functions.
abstract class BaseViewModel(
private val dependencies: BaseViewModelDependencies
) : ViewModel() {
val analyticsManager = dependencies.analyticsManager
val defaultDispatcher = dependencies.defaultDispatcher
fun showGlobalErrorDialog(message: String) {
dependencies.dialogEventLauncher
}
}
Provide mocked BaseViewModel dependencies in tests
In :ui:test we create a helper for mocking BaseViewModelDependencies. This helper simplifies test setup in feature modules and lets us override functionality in specific tests when required.
fun BaseViewModelDependencies.initForViewModelTest(
analyticsManager: AnalyticsManager = mock(),
dialogEventLauncher: GlobalDialogManager = mock(),
defaultDispatcher: CoroutineDispatcher = StandardTestDispatcher()
) {
`when`(this.analyticsManager).thenReturn(analyticsManager)
`when`(this.dialogEventLauncher).thenReturn(dialogEventLauncher)
`when`(this.defaultDispatcher).thenReturn(defaultDispatcher)
}
Test BaseViewModel functionality
Since BaseViewModel is, ideally, an abstract class, we need to implement it to test it. We can either create a bare-bones implementation specifically for testing, or we can add tests for the base ViewModel in a child ViewModel’s test class. I prefer the former approach.
class BaseViewModelTest {
private val dependencies: BaseViewModelDependencies = mock()
private val analyticsManager: AnalyticsManager = mock()
private class TestViewModel(
dependencies: BaseViewModelDependencies
) : BaseViewModel(dependencies)
@Before
fun setup() {
dependencies.initForViewModelTest(
analyticsManager = analyticsManager
)
}
@Test
fun onLaunch_tracksScreenShown() {
val viewModel = TestViewModel(dependencies)
viewModel.onLaunch()
verify(analyticsManager).onScreenShown("TestViewModel")
}
}
Conclusion
A BaseViewModel isn’t always required, can break SOLID principles, and can make testing more difficult. However, it can also improve maintainability and reduce overhead when creating many complex ViewModels that share functionality.
Hopefully this pattern helps you create more scalable, testable ViewModels in your multi-module Android app.
Github Repository
A complete sample project demonstrating the pattern used in this article can be found at the following link github.com/broliver12/baseviewmodel
Happy Coding!
메타데이터
- post_id
- cd5a08a0ef87
- slug
- baseviewmodel-pattern-for-multi-module-android-apps-cd5a08a0ef87
- url
- https://medium.com/@ostraszynski/baseviewmodel-pattern-for-multi-module-android-apps-cd5a08a0ef87
- canonical_url
- https://medium.com/@ostraszynski/baseviewmodel-pattern-for-multi-module-android-apps-cd5a08a0ef87
- author_url
- https://medium.com/@ostraszynski
- status
- ok
- fetched_at
- 2026-06-20 20:29:01