Why, God, Why?

God Activity, God ViewModel — Why, God, Why?

Debug Labs
4 min readJan 30, 2025

Android development has come a long way, evolving from the days of bloated, all-knowing Activity classes to a cleaner, more structured architecture. But in our pursuit of better separation of concerns, we’ve merely shifted the burden from one place to another — enter the God ViewModel.

In this article, we’ll first explore how we ended up with God Activities, the various strategies used to address them, and how we inadvertently created God ViewModels. Finally, we’ll discuss practical ways to avoid ViewModel bloat and refactor existing God ViewModels to design more maintainable applications.

The Era of the God Activity

In the early days of Android, Activities (and later, Fragments) were responsible for everything — handling UI updates, managing business logic, database interactions, network calls, and even handling navigation. This led to massive, unmanageable classes that:

  • Were difficult to read and debug
  • Violated the Single Responsibility Principle
  • Were tightly coupled, making testing and maintenance a nightmare

A typical legacy Activity might look something like this:

class GodActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
        val database = AppDatabase.getInstance(this)
val apiService = ApiService.create()

button.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
val data = apiService.fetchData()
database.dataDao().insert(data)
withContext(Dispatchers.Main) {
textView.text = data.name
}
}
}
}
}

This God Activity is doing too much — handling UI updates, making network calls, performing database operations, and managing concurrency. It quickly becomes a monolithic mess as the app grows.

The Shift to ViewModels: Did We Actually Fix the Problem?

To solve the God Activity problem, developers embraced MVVM (Model-View-ViewModel) and extracted logic into a ViewModel:

class MyViewModel(private val repository: MyRepository) : ViewModel() {
private val _data = MutableLiveData<String>()
val data: LiveData<String> get() = _data
    fun fetchData() {
viewModelScope.launch {
val result = repository.getData()
_data.value = result.name
}
}
}

And in our Activity/Fragment, we now only observe data and trigger actions:

class MyFragment : Fragment() {
private val viewModel: MyViewModel by viewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.data.observe(viewLifecycleOwner) {
textView.text = it
}
button.setOnClickListener { viewModel.fetchData() }
}
}

Mission accomplished, right? Not quite.

The Rise of the God ViewModel

While moving logic to ViewModels solved the bloated Activity problem, it created a new issueGod ViewModels.

Symptoms of a God ViewModel:

  1. ViewModel is now doing everything — handling UI state, business logic, database operations, network calls, error handling, and navigation.
  2. It grows too big — with hundreds (or thousands) of lines of code.
  3. It becomes hard to test and maintain — since dependencies and logic are all crammed into one place.

A God ViewModel might look something like this:

class GodViewModel(
private val repository: MyRepository,
private val analyticsTracker: AnalyticsTracker,
private val navigator: AppNavigator
) : ViewModel() {
    private val _uiState = MutableLiveData<UiState>()
val uiState: LiveData<UiState> get() = _uiState
fun fetchData() {
viewModelScope.launch {
try {
val data = repository.getData()
analyticsTracker.trackEvent("DataFetched")
_uiState.value = UiState.Success(data)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
fun onButtonClicked() {
repository.performComplexOperation()
navigator.navigateToNextScreen()
}
}

This God ViewModel is just as bad as the God Activity, except now we have moved the problem instead of solving it.

How to Prevent and Refactor a God ViewModel

Back to Basics: The Single Responsibility Principle

To resolve the God ViewModel, we need to return to the core principle that led us to MVVM in the first place — Single Responsibility Principle (SRP). Instead of cramming everything into a single class, we abstract away concerns, layer by layer.

1. Move Business Logic to a Repository

Don’t put data-fetching logic inside the ViewModel. Instead, delegate it to a Repository:

class MyRepository(private val api: ApiService, private val database: AppDatabase) {
suspend fun getData(): Data {
return api.fetchData().also { database.dataDao().insert(it) }
}
}

ViewModel now only calls the repository:

class MyViewModel(private val repository: MyRepository) : ViewModel() {
val data = liveData { emit(repository.getData()) }
}

2. Break Down ViewModels into Smaller Ones

Instead of having one massive ViewModel managing multiple concerns, break it down into feature-specific ViewModels.

Example: A screen that displays a user profile and user posts can use two separate ViewModels:

class UserProfileViewModel(private val repository: UserRepository) : ViewModel() { /* ... */ }
class UserPostsViewModel(private val repository: PostRepository) : ViewModel() { /* ... */ }

3. Separate UI State Management

Use a UI state holder to separate state management from business logic.

data class UiState(val isLoading: Boolean, val data: Data?, val error: String?)
class UiStateHolder : ViewModel() {
private val _uiState = MutableLiveData<UiState>()
val uiState: LiveData<UiState> get() = _uiState
fun setLoading() { _uiState.value = UiState(true, null, null) }
fun setData(data: Data) { _uiState.value = UiState(false, data, null) }
fun setError(error: String) { _uiState.value = UiState(false, null, error) }
}

4. Introduce UseCases to Encapsulate Business Logic

Instead of handling complex business rules inside the ViewModel, move them to UseCases (interactor classes).

class FetchUserDataUseCase(private val repository: UserRepository) {
suspend fun execute() = repository.getUserData()
}

Now, ViewModel delegates work:

class UserViewModel(private val fetchUserDataUseCase: FetchUserDataUseCase) : ViewModel() {
val userData = liveData { emit(fetchUserDataUseCase.execute()) }
}

Final Thoughts

To fix a God ViewModel, we go back to the basics — the Single Responsibility Principle — and systematically extract responsibilities into separate layers.

  • Repositories handle data
  • UseCases handle business logic
  • UI State holders manage UI updates
  • Multiple ViewModels divide complex screens

By applying these layered abstractions, we can finally break free from the cycle of God components and build scalable, maintainable Android apps.

If this reminds you to have cleaner architecture and actually follow that through, throughout your feature development, bug fixes, across teams. That is it. That is the point :)

--

--

Debug Labs
Debug Labs

Written by Debug Labs

🚀 Android Dev (13+ yrs) | Jetpack Compose | AI & ML Enthusiast | Writing on Background Work, Room DB, Clean Architecture & more | Simplifying dev concepts

No responses yet