Why does this codelab at android developers passes a "onNextButtonClicked: () -> Unit," instead of just passing the "navController: NavController,"... it seems simpler.. is there a reason for this?
2 Answers
This is called state hoisting. You could read more on official documentation.
A quick answer to your question though: The way you mentioned it will work. But by hoisting
state — that is, to move a composable’s state outside of the composable and push it further up, by making the composable stateless
it results in components that are easier to reuse and test!

- 2,838
- 28
- 39
Passing the nav controller to screens is not a good practice, one of the previous answers.
Test writing: When you reach the command you want through callback to the highest level; You are now comfortable for writing tests and you don't need to mock the nav controller.
Therefore, only by checking the callbacks that you have used, you can ensure the correctness of the functionality of your composables.
Consider the following example:
PostScreen
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@Composable
fun PostScreen(
navigateToPostDetail: (PostId: PostId) -> Unit
) {
val posts = remember {
mutableListOf<Post>().apply {
repeat(10) {
add(
Post(
id = "$it", title = "Title $it"
)
)
}
}
}
PostScreenContent(
posts = posts,
navigateToPostDetail = navigateToPostDetail
)
}
data class Post(
val id: String, val title: String
)
@Composable
internal fun PostScreenContent(
posts: List<Post>,
navigateToPostDetail: (PostId: PostId) -> Unit
) {
LazyColumn {
items(items = posts, key = { item -> item.id }) { item ->
Text(text = item.title)
Button(onClick = {
navigateToPostDetail(PostId(item.id))
}
) {
Text(text = "Navigate To Detail")
}
}
}
}
@JvmInline
value class PostId(val value: String)
PostScreenTest
import androidx.activity.ComponentActivity
import androidx.compose.runtime.remember
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import org.junit.Rule
import org.junit.Test
internal class PostScreenTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun clickOnPostItem() {
var expectedPostId = "your expected id"
var actualPostId: String
with(composeTestRule) {
setContent {
val posts = remember {
mutableListOf<Post>().apply {
repeat(10) {
add(
Post(
id = "$it",
title = "Title $it"
)
)
}
}
}
PostScreenContent(
posts = posts,
navigateToPostDetail = {
actualPostId = it.value
}
)
}
assertThat(actualPostId).isEqualTo(expectedPostId)
}
}
}
However, if you want to test the navigation logic of the whole app, it is enough to test your highest layer, the place where you used NavHost.

- 546
- 4
- 17
-
1To add to this answer, passing the NavController complicates the Screen with details of popping the Backstack, and handling NavArguments, if needed. By using state hoisting, the Screen is freed from those concerns, and all of the navigation details are in the calling context. – Mike Aug 31 '23 at 05:03