← Back to list

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…

Oliver Straszynski · 2026-06-10 02:19 · 10 claps · 3.6 min read
#android-app-development #gradle #viewmodel #mvvm #unit-testing
Open on Medium ↗

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 hilt and ksp plugins in the appropriate build.gradle.kts files.
  • 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