Understanding ViewModel Scoping in Jetpack Compose

Hello everyone! 👋
Many people have misconceptions about the scope of the ViewModel. In this short article, I’ll clear them up and explain ViewModel scoping in a simple way. 🧩
Let’s dive in! 🚀
🔍 Understanding Where ViewModel Instances Are Created
There are two distinct places where the ViewModel is instantiated with a new reference instead of reusing the same instance:
1️⃣ Activity Level
👉🏻 Uses by viewModels()
to create a ViewModel scoped to the Activity’s lifecycle.
👉🏻 Managed by the Activity’s ViewModelStore
.
2️⃣ Composables Within the NavHost
👉🏻 Uses hiltViewModel()
to create ViewModels scoped to navigation destinations (e.g., NavBackStackEntry
).
👉🏻 Each destination may get its own instance unless explicitly shared.
Cool? Let’s move forward! 🚀
📝 Let’s Dive Into the Code!
Let’s look at the code first and then break it down for a clear understanding. This will be super easy! 😃
Note: This is just an example of ViewModel’s scope. ⚠️ Logging inside a composable is not a best practice. 🚫
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val viewmodel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Scaffold { paddingValues ->
Box(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
) {
NavDemo(viewmodel)
}
}
}
}
}
@Composable
fun NavDemo(navDemo: MainViewModel = hiltViewModel()) {
Log.e("NavDemo-Viewmodel", navDemo.hashCode().toString())
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "main") {
composable("main") {
MainScreen(onItemClick = { navController.navigate("details") })
}
composable("details") {
DetailsScreen(onBack = { navController.popBackStack() })
}
}
}
@Composable
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), onItemClick: () -> Unit) {
Log.e("MainScreen-Viewmodel", viewModel.hashCode().toString())
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Main Screen",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue, shape = RoundedCornerShape(8.dp))
.clickable { onItemClick() }
)
PlayerA()
PlayerB()
}
}
@Composable
fun PlayerA(viewModel: MainViewModel = hiltViewModel()) {
Log.e("PlayerA-Viewmodel", viewModel.hashCode().toString())
Text(text = "text", fontSize = 12.sp, color = Color.Magenta)
}
@Composable
fun PlayerB(viewModel: MainViewModel = hiltViewModel()) {
Log.e("PlayerB-Viewmodel", viewModel.hashCode().toString())
Text(text = "text", fontSize = 12.sp, color = Color.Magenta)
}
@Composable
fun DetailsScreen(viewModel: MainViewModel = hiltViewModel(), onBack: () -> Unit) {
Log.e("DetailsScreen-Viewmodel", viewModel.hashCode().toString())
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Details Screen",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
Box(
modifier = Modifier
.size(200.dp) // Different size for transition effect
.background(Color.Blue, shape = RoundedCornerShape(8.dp))
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onBack) {
Text("Go Back")
}
}
}
Output:-

🧐 Explanation:-
If you look closely, you’ll notice that three different instances of MainViewModel
have been created. 🔄👀
🤔 Why Three Instances?
From the logs, you can see three distinct hashCode()
values:
🔹 Instance A: Created using by viewModels()
in MainActivity
, scoped to the Activity.
🔹 Instance B: Created using hiltViewModel()
in MainScreen
, PlayerA
, and PlayerB
, scoped to the "main" destination.
🔹 Instance C: Created using hiltViewModel()
in DetailsScreen
, scoped to the "details" destination.
🔍 The Reason — Scoping Mismatch
✅ by viewModels()
→ Scopes the ViewModel to the Activity.
✅ hiltViewModel()
in a NavHost
→ Scopes the ViewModel to navigation destinations (NavBackStackEntry
).
✅ Each navigation destination gets its own ViewModel instance unless explicitly shared. 🔄What if you want the same instance of ViewModel across the app?
🔄 Sharing the Same ViewModel Instance
If you want to reuse the same ViewModel instance, you can scope it at the Activity level. But that’s not our focus here — it’s pretty straightforward! ✅
You can achieve this using the following code:
val activity = LocalActivity.current as ComponentActivity
val viewModel: MainViewModel = viewModel(activity)
This ensures the ViewModel is tied to the Activity’s lifecycle, making it accessible across different composables. 🔗
If you have any questions, just drop a comment, and I’ll get back to you ASAP. 💬✨
We’ll be diving deeper into Jetpack Compose soon, so stay tuned! 🚀
Until then, happy coding! 🎉👨💻