State hoisting means moving state out of a composable to its caller, making the composable reusable and testable ("stateless"). For whole screens, state lives in a ViewModel.
Hoisted (stateless) composable
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
TextField(value = query, onValueChange = onQueryChange)
}
// caller owns the state and passes it down + handles changes
Screen state in a ViewModel
class FeedViewModel : ViewModel() {
private val _posts = MutableStateFlow<List<Post>>(emptyList())
val posts: StateFlow<List<Post>> = _posts
fun load() { /* update _posts */ }
}
@Composable
fun FeedScreen(vm: FeedViewModel = viewModel()) {
val posts by vm.posts.collectAsState()
LazyColumn { items(posts) { Text(it.title) } }
}
Pattern: state flows down, events flow up (UDF — Unidirectional Data Flow).
Tip: Read StateFlow with
collectAsState() (or collectAsStateWithLifecycle()) so the UI updates automatically.Summary
Hoist state to make composables reusable; keep screen state in a ViewModel exposed as StateFlow. State down, events up.