Jetpack Compose: State Management for Flutter Developers

Yayınlanma: 30 Ekim 2025 6 dk okuma

If you’ve been developing with Flutter, you already understand the core idea of declarative UI — “UI = function of state.” Jetpack Compose is built on the same philosophy.

But here’s the twist: while Compose and Flutter share this reactive mindset, the way they manage and scope state differs in key ways.

In this guide, we’ll explore how to manage state in Jetpack Compose, explained through the eyes of a Flutter developer.
We’ll also compare Compose concepts to their Flutter equivalents so you can transfer your mental model smoothly.

The Shared Philosophy: Declarative UIs

In both Compose and Flutter:

  • You never imperatively change the UI.
  • Instead, you update state, and the framework recomposes (rebuilds) affected parts of the UI tree.

In Flutter you might write:

setState(() {
count++;
});

In Compose, you do:

count++

and the UI automatically re-renders — because count is wrapped in a state holder. Let’s see how this works in practice.

Composables vs Widgets

Think of a Composable function like a StatelessWidget in Flutter —
except it can also hold transient state without needing a separate StatefulWidget class.

@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }

Button(onClick = { count++ }) {
Text("Count: $count")
}
}

This is very similar to:

class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
int count = 0;

@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => setState(() => count++),
child: Text("Count: $count"),
);
}
}

So in Compose:

  • remember { mutableStateOf(…) } ≈ StatefulWidget + setState
  • The framework automatically keeps this state alive across recompositions.

Practical Ceoncepts Examples

  • ViewModel as a source of truth (like Flutter’s Provider/Riverpod)
  • Local UI state with remember (like setState)
  • Reactive streams via StateFlow (similar to StreamProvider)
  • Lifecycle-aware collection (avoiding memory leaks)
  • Reusable UI logic via StateHolder (like ChangeNotifier + Mixin)
1. ViewModel:

In Flutter, you often lift state up using Provider or Riverpod (or whatever you use). In Compose, we use ViewModel as the central state owner.

@HiltViewModel
class TodoViewModel @Inject constructor(
private val repository: TodoRepository
) : ViewModel() {

private val _todos = MutableStateFlow<List<Todo>>(emptyList())
val todos = _todos.asStateFlow()

fun addTodo(title: String) {
_todos.update { it + Todo(title) }
}
}

Think of this like:

final todosProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) => TodoNotifier());

The ViewModel’s state survives configuration changes (like rotation), just like a Riverpod StateNotifier survives rebuilds.

2. Collecting State in Composables

In Flutter, you’d use Consumer or ref.watch() to listen to state. In Compose, you use collectAsStateWithLifecycle():

@Composable
fun TodoScreen(viewModel: TodoViewModel = hiltViewModel()) {
val todos by viewModel.todos.collectAsStateWithLifecycle()

TodoList(
todos = todos,
onAdd = viewModel::addTodo
)
}

Equivalent Flutter pattern:

final todos = ref.watch(todosProvider);

But Compose integrates lifecycle awareness by default — meaning it automatically stops observing when your composable is not active.

3. Local UI State: remember vs setState

In Flutter, setState updates the widget’s local state. In Compose, we use remember (and rememberSaveable to persist across configuration changes).

@Composable
fun TodoInput(onAdd: (String) -> Unit) {
var text by rememberSaveable { mutableStateOf("") }

Row {
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text("New Todo") }
)
Button(onClick = { onAdd(text); text = "" }) {
Text("Add")
}
}
}

Flutter equivalent in StatefullWidget:

String text = '';

TextField(
onChanged: (v) => setState(() => text = v),
);
4. Reusable UI Logic: The StateHolder Pattern

In Jetpack Compose, the StateHolder pattern is a clean way to organize and reuse UI logic while keeping your composables stateless.

If you’re coming from Flutter, think of it like a combination of StateNotifier and its State object — where all the state and business logic live inside the ViewModel, and the composables simply observe and react to it.

The most idiomatic way to apply this pattern in Compose is by defining a ui state data class inside your ViewModel. This UiState represents a snapshot of everything the screen needs to render. It’s immutable, meaning that instead of mutating values directly, you create a new copy whenever the state changes.

data class CounterUiState(
val count: Int = 0,
val isLoading: Boolean = false
)

class CounterViewModel : ViewModel() {
private val _uiState = MutableStateFlow(CounterUiState())
val uiState = _uiState.asStateFlow()

fun increment() {
_uiState.update { it.copy(count = it.count + 1) }
}

fun reset() {
_uiState.value = CounterUiState()
}
}

The uiState here plays a similar role to a State object in Flutter’s StateNotifier or Bloc pattern — it’s a single source of truth for the entire screen.

Your composable then becomes a pure function of this state:

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val state by viewModel.uiState.collectAsState()

Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Count: ${state.count}")
Button(onClick = { viewModel.increment() }) { Text("Increment") }
Button(onClick = { viewModel.reset() }) { Text("Reset") }
}
}

StateHolder pattern in Compose is the equivalent of ViewModel + immutable uiState, mirroring Flutter’s StateNotifier + State design — a clean, declarative approach that scales beautifully as your app grows.

5. Lifecycle and Side Effects

In Flutter, you use initState() for one-time actions. In Compose, that’s what LaunchedEffects does. But unlike Flutter’s initState(), which always runs only once when a widget is created, Compose’s LaunchedEffect is key-driven. It takes a key parameter, and the effect will re-run every time that key changes. If you pass a constant key such as Unit (similar to void in other languages), it will only execute once, similar to initState() in Flutter.

@Composable
fun TodoScreen(viewModel: TodoViewModel = hiltViewModel()) {
val todos by viewModel.todos.collectAsStateWithLifecycle()

LaunchedEffect(Unit) {
viewModel.loadTodos()
}

TodoList(todos)
}

Flutter equivalent:

@override
void initState() {
super.initState();
WidgetsBindingInstance.addPostframeCallback((_){
context.read(todosProvider.notifier).loadTodos();
});
}

If you understand how Flutter rebuilds widgets, you already know half of Compose. The remaining half is just learning where to keep each state and how recomposition works. But Compose gives you finer control over scoping, lifecycle, and performance — without needing external packages like Provider or Riverpod.

So if you’re a Flutter developer stepping into the Android world, Jetpack Compose will feel surprisingly familiar — just with Kotlin’s flavor. Happy coding 🎈.

Jetpack Compose: State Management for Flutter Developers

Kategori: android-app-developmentEtiketler: android-app-development, jetpack-compose, flutter-app-development, androiddev, flutter