iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

setState

setState is Flutter’s built-in way for a StatefulWidget to tell the framework “rebuild my subtree.” It’s the foundation every other state-management approach builds on; mastering when (and when not) to use it sharpens your sense for re-renders and rebuilds.

StatefulWidget, lifecycle, scope, alternatives

EXAMPLE
import 'package:flutter/material.dart';

// 1) StatefulWidget + State
class CounterPage extends StatefulWidget {
    const CounterPage({super.key});
    @override
    State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
    int _count = 0;

    void _increment() {
        setState(() {
            _count++;
        });
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: const Text('Counter')),
            body: Center(child: Text('Count: $_count')),
            floatingActionButton: FloatingActionButton(
                onPressed: _increment,
                child: const Icon(Icons.add),
            ),
        );
    }
}

// setState's callback is where you MUTATE; build() reads the new values.
// Flutter then re-runs build() and re-renders the subtree.

// 2) When NOT to call setState
//   • Outside the widget tree (after dispose) → throws 'setState called after dispose'
//   • From build() itself → infinite rebuild loop
//   • From initState only to set initial values that don't depend on inherited widgets
//     (just set the field directly)
//   • For async results — guard with 'if (mounted)'

Future<void> _load() async {
    final data = await api.fetchData();
    if (!mounted) return;          // widget may have been disposed
    setState(() => _data = data);
}

// 3) State lifecycle
class _MyWidgetState extends State<MyWidget> {
    @override
    void initState()  { super.initState();    /* one-time setup */ }
    @override
    void didChangeDependencies() { super.didChangeDependencies(); /* inheritable changed */ }
    @override
    void didUpdateWidget(MyWidget old) { super.didUpdateWidget(old); /* props changed */ }
    @override
    void dispose()    { /* cancel subscriptions, controllers */ super.dispose(); }

    @override
    Widget build(BuildContext context) { /* called many times */ }
}

// 4) Scope — setState rebuilds THIS state object's subtree
//   • Smaller stateful widgets = smaller rebuilds
//   • Hoist state UP, not DOWN; or push state DOWN into a child if siblings don't need it
//   • Use Builder() to scope rebuild to a sub-tree without making a whole new widget

class _AppState extends State<App> {
    int _count = 0;
    @override
    Widget build(BuildContext context) {
        return Column(
            children: [
                const ExpensiveHeader(),    // never rebuilds; counter doesn't live here
                Text('Count: $_count'),
                ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+1')),
            ],
        );
    }
}

// 5) Common controllers
class _MyFormState extends State<MyForm> {
    final _emailCtrl = TextEditingController();
    final _scrollCtrl = ScrollController();

    @override
    void dispose() {
        _emailCtrl.dispose();
        _scrollCtrl.dispose();
        super.dispose();
    }

    @override
    Widget build(BuildContext context) {
        return Column(
            children: [
                TextField(controller: _emailCtrl),
                ElevatedButton(
                    onPressed: () => print(_emailCtrl.text),
                    child: const Text('Print'),
                ),
            ],
        );
    }
}

// Controllers are mutable; you don't need setState to read their text/values.
// You CAN trigger setState in onChanged if other UI depends on the value.

// 6) Lifting state up
class _ParentState extends State<Parent> {
    int _value = 0;
    void _set(int v) => setState(() => _value = v);
    @override
    Widget build(BuildContext context) => Column(
        children: [
            CounterDisplay(value: _value),
            CounterInput(value: _value, onChanged: _set),
        ],
    );
}

// 7) Async + futures + streams
class _Future extends State<F> {
    late Future<Data> _future;
    @override
    void initState() { super.initState(); _future = api.load(); }
    @override
    Widget build(BuildContext context) {
        return FutureBuilder<Data>(
            future: _future,
            builder: (ctx, snap) {
                if (snap.connectionState != ConnectionState.done) return const CircularProgressIndicator();
                if (snap.hasError) return Text('Error: ${snap.error}');
                return DataView(data: snap.data!);
            },
        );
    }
}

// 8) When to graduate from setState
// • Many widgets need the same state → InheritedWidget, Provider, Riverpod, Bloc, Redux
// • Complex async logic → BLoC pattern, Riverpod's async providers
// • Persistence across navigation → state in a higher-up store
// • Multi-screen forms / wizards → controllers + a model class
//
// For small apps, setState is plenty. Adding a state library too early is over-engineering.

// 9) Riverpod alternative (recommended for new Flutter apps)
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider = StateProvider<int>((ref) => 0);

class CounterPageRp extends ConsumerWidget {
    const CounterPageRp({super.key});
    @override
    Widget build(BuildContext context, WidgetRef ref) {
        final count = ref.watch(counterProvider);
        return Scaffold(
            body: Center(child: Text('Count: $count')),
            floatingActionButton: FloatingActionButton(
                onPressed: () => ref.read(counterProvider.notifier).state++,
                child: const Icon(Icons.add),
            ),
        );
    }
}

// 10) Avoiding excessive rebuilds — const + key
// • Mark immutable widgets as 'const' — they skip rebuild entirely
// • Use ValueListenableBuilder, AnimatedBuilder for fine-grained reactivity without full rebuild
// • Profile with the 'Performance' overlay + DevTools timeline to find rebuild hot spots

// 11) Common bugs
// • setState() called after dispose() → 'setState() called after dispose'; guard with 'if (mounted)'
// • Mutating state OUTSIDE the setState callback → UI may not update on the next frame
// • Heavy work inside the setState callback → freezes UI; do async work first, then setState with results
// • Calling setState during build → 'setState() or markNeedsBuild() called during build'
// • Forgetting to dispose controllers → memory leaks; always pair init + dispose
// • Keys missing on list rows that reorder → state attaches to wrong widget on rebuild
// • Treating widget.props as mutable — they're not; use State for mutable values
// • Premature state lib — start with setState, graduate when complexity demands it

Why it matters

setState is the foundation: mutate inside the callback, let Flutter rebuild. Keep stateful widgets small so rebuilds stay scoped, dispose controllers, guard async results with if (mounted), and graduate to Riverpod / BLoC only when multiple widgets need to share or persist the state.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
setState(() {
    count += 1;
});
Try it Yourself »

Discussion

Loading…