---
title: "Widgets, State, and Lifecycle"
description: "State and setState, keys, BuildContext, the widget and app lifecycles, InheritedWidget, and const constructors."
tags: [flutter, dart, state, widgets, lifecycle]
---

## State

State in Flutter refers to the data or information that can change over time and affects how a widget renders on the screen. When this data changes, Flutter needs to rebuild the UI to reflect those changes.

### What counts as state

State is fundamental to creating dynamic and interactive applications. Here's what it encompasses:

* **Data that changes**: Variables, user inputs, API responses, or any information that may change during the app's lifecycle
* **UI reactivity**: When state changes, related parts of the UI update automatically
* **User interactions**: Buttons pressed, form inputs, navigation, and other user actions often trigger state changes
* **Application context**: Information about the current status of your application that needs to be maintained

### Ephemeral state vs app state

Flutter generally recognizes two main types of state:

1. **Ephemeral (Local) State**:

   * Contained within a single widget
   * Doesn't need to be shared with other parts of the app
   * Managed using `StatefulWidget` and its associated `State` class
   * Examples: current page in a PageView, animation status, or text field content
2. **App State**:

   * Shared across multiple widgets or the entire app
   * Persists for longer durations
   * Typically managed using state management solutions
   * Examples: user authentication data, shopping cart items, or app preferences

### Ways to manage state

Bloc, Cubit, and Riverpod are all built on streams, which are covered in [streams](https://brain.narayann.dev/notes/engineering/flutter/streams). How state fits
into a layered codebase is in [architecture-and-testing](https://brain.narayann.dev/notes/engineering/flutter/architecture-and-testing).

Flutter offers several approaches to manage state:

* **StatefulWidget**: For simple local state management
* **InheritedWidget**: For passing data down the widget tree
* **Provider**: A wrapper around InheritedWidget that simplifies state management
* **Bloc/Cubit**: For reactive state management using streams
* **Riverpod**: An improved version of Provider with additional features
* **GetX**: A lightweight state management solution
* **Redux**: For predictable state management with a unidirectional data flow
* **MobX**: For reactive state management

Here's a simple example using a `StatefulWidget`:

```dart
class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int count = 0;  // This is the state

  void incrementCount() {
    setState(() {
      count++;  // This changes the state
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $count'),  // UI reflects the state
        ElevatedButton(
          onPressed: incrementCount,
          child: Text('Increment'),
        ),
      ],
    );
  }
}
```

In this example, `count` is the state that changes when the button is pressed. The `setState()` method tells Flutter to rebuild the widget with the updated state value.

Understanding state and choosing the right state management approach is crucial for building responsive, maintainable Flutter applications.

***

***

## Keys

Keys in Flutter are unique identifiers for widgets that help Flutter maintain and preserve widget state when the widget tree changes or when widgets are moved around.

### What a key is

Keys are special objects assigned to widgets that act as unique identifiers. When Flutter rebuilds its widget tree, it uses these keys to identify which widgets should preserve their state and which should be rebuilt from scratch.

Keys are used in Flutter as identifiers for widgets, elements, and semantic nodes. GlobalKeys and LocalKeys are the subclasses of Key. Within the widget tree, keys are responsible for preserving the state of modified widgets. With keys, you can also reorganize and modify collections of widgets that have an equivalent type and defined state. The primary use of keys is to modify a widget tree that contains stateful widgets, not to modify a tree that is totally composed of stateless widgets.

### Why keys matter

Keys are used to preserve the state of widgets and to ensure that widgets are properly identified and maintained across rebuilds.

* **Purpose of Keys:**
  * Keys help Flutter's framework to differentiate between widgets that might otherwise appear identical. They are especially useful when working with lists or any collection of widgets where the position or order might change.
  * They help maintain the state of stateful widgets when their position in the widget tree changes. This is crucial for maintaining consistent behavior, especially in dynamic lists or collections where the widgets may be reordered or replaced.
* **Types of Keys:**
  * **ValueKey:** A `ValueKey` uses a value to identify a widget. If the value changes, Flutter considers the widget as different.
  * **ObjectKey:** An `ObjectKey` uses an object's identity to determine the widget's uniqueness.
  * **UniqueKey:** A `UniqueKey` generates a unique key each time it is created, ensuring that the widget is always treated as unique.
* **Usage:**
  * **Lists:** In a `ListView` or similar widget, you should use keys when the list items can change position or when items are added or removed. This helps Flutter manage the state of each item correctly.
  * **State Management:** Keys help preserve the state of widgets when the widget tree is rebuilt. For example, if you have a form with multiple input fields and need to rearrange or update them, using keys ensures that the input fields retain their values and state.

**Example:**

```dart
ListView(
  children: [
    Text('Item 1', key: ValueKey('item1')),
    Text('Item 2', key: ValueKey('item2')),
    // more items...
  ],
);
```

Here, `ValueKey` is used to uniquely identify each item, ensuring that the framework can keep track of them correctly.

### Types of key

1. **ValueKey**: A key that uses a value, such as a string or number, for identification.

```dart
ValueKey<String>("unique-id")
```

2. **ObjectKey**: Uses an object's identity for uniqueness.

```dart
ObjectKey(myObject)
```

3. **UniqueKey**: Generates a unique identifier each time it's created.

```dart
UniqueKey()
```

4. **GlobalKey**: A key that's unique across the entire app (not just within a parent).

```dart
GlobalKey<FormState>()
```

5. **PageStorageKey**: Used to preserve scroll position when a widget is removed and later added back.

```dart
PageStorageKey<String>("list-key")
```

### When to use keys

Keys are particularly important in the following scenarios:

1. **When reordering widgets in a list**: Without keys, Flutter might not correctly preserve state when items change order.

2. **When stateful widgets are moved in the tree**: Keys help Flutter keep track of which state belongs to which widget.

3. **When widgets of the same type appear and disappear**: Keys help Flutter identify which widgets are new and which have reappeared.

4. **When using GlobalKey**: To access a widget's state from anywhere in the app.

Here's a practical example showing the difference between using keys and not using keys in a list:

```dart
// Without keys - can cause state issues when reordering
ListView(
  children: myItems.map((item) =>
    StatefulWidget()
  ).toList(),
)

// With keys - preserves state correctly
ListView(
  children: myItems.map((item) =>
    StatefulWidget(key: ValueKey(item.id))
  ).toList(),
)
```

### Best practices

1. Use keys sparingly, they add overhead.
2. When using keys in lists, use something unique to the item (like an ID) as the key value.
3. Choose the most appropriate key type for your use case.
4. Only use GlobalKeys when absolutely necessary as they're more expensive.

Keys are an essential tool for ensuring your Flutter app behaves correctly when its widget structure changes, helping to maintain state across rebuilds and making your UI more predictable.

***

## BuildContext and lifecycle

`BuildContext` is a widget's handle on its own position in the tree. The lifecycle methods are the callbacks that fire as that position is created, updated, and destroyed.

### BuildContext

#### What BuildContext is

`BuildContext` is a reference to the location of a widget in the widget tree. Every widget receives its own `BuildContext` when it's built, and this context contains important information about the widget's position in the tree and provides access to various services and inherited widgets.

#### Key features

1. **Widget Location**: Represents the widget's position in the widget tree
2. **Access to Inherited Widgets**: Allows widgets to access data from ancestor widgets
3. **Service Locator**: Provides access to services like theme data, media queries, and navigation
4. **Widget Identification**: Helps Flutter identify which widget needs to be rebuilt when state changes

#### Common uses

```dart
// Accessing Theme data
final theme = Theme.of(context);
final primaryColor = theme.primaryColor;

// Accessing Screen dimensions
final screenSize = MediaQuery.of(context).size;
final screenWidth = screenSize.width;

// Navigation
Navigator.of(context).push(
  MaterialPageRoute(builder: (context) => SecondScreen()),
);

// Accessing inherited widgets like Provider
final userModel = Provider.of<UserModel>(context);
```

#### Things to watch out for

1. **Context Validity**: A context is only valid as long as its widget is in the tree
2. **Async Operations**: Never use `context` after an asynchronous gap unless you check if it's still mounted
3. **Context Dependency**: Some Flutter widgets require a specific ancestor widget to be present in the tree

### The lifecycles

Flutter apps have multiple layers of lifecycle management:

#### App lifecycle

The `AppLifecycleState` enum represents the application's current state on the device:

* **resumed**: App is visible and responding to user input
* **inactive**: App is not receiving user input (iOS only when app is in the foreground but not receiving events)
* **paused**: App is not visible, not responding to user input, but running in the background
* **detached**: App is in a suspended state (neither launched nor resumed)

You can listen to these changes using the `WidgetsBindingObserver`:

```dart
class MyAppState extends State<MyApp> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.resumed:
        print("App is resumed and visible");
        break;
      case AppLifecycleState.inactive:
        print("App is inactive");
        break;
      case AppLifecycleState.paused:
        print("App is in background");
        break;
      case AppLifecycleState.detached:
        print("App is detached");
        break;
    }
  }
}
```

#### StatefulWidget lifecycle

`StatefulWidget`s have their own lifecycle methods that are called at different stages:

1. **createState()**:

   * Called when the StatefulWidget is inserted into the tree
   * Returns a State object for the widget
2. **initState()**:

   * Called once when the State object is created
   * Used for one-time initializations
   * Always call `super.initState()` first
3. **didChangeDependencies()**:

   * Called immediately after `initState()`
   * Called whenever the widget's dependencies change
   * Called when an `InheritedWidget` that this widget depends on changes
4. **build()**:

   * Called whenever the widget needs to be rebuilt
   * Returns the widget tree that represents the UI
   * Called after `initState()`, `didChangeDependencies()`, `setState()`, and `didUpdateWidget()`
5. **didUpdateWidget(oldWidget)**:

   * Called when the parent widget changes and this widget needs to be rebuilt
   * The `oldWidget` parameter contains the previous widget
   * Use to compare with `widget` to respond to changes
6. **setState()**:

   * Not a lifecycle method per se, but triggers a rebuild
   * Notifies the framework that the widget's state has changed
7. **deactivate()**:

   * Called when the State is removed from the tree, but might be reinserted
   * Happens during page navigation or when a widget is moved in the tree
8. **dispose()**:

   * Called when the State is removed from the tree permanently
   * Used for cleanup (canceling timers, closing streams, etc.)
   * Always call `super.dispose()` last

```text
StatefulWidget
    ↓ createState()
State object created
    ↓ initState()
State initialized
    ↓ didChangeDependencies()
Dependencies updated
    ↓ build()
Widget builds UI
    ↓ setState() [multiple times]
State updates → rebuild() called
    ↓ deactivate()
State removed from tree temporarily
    ↓ dispose()
State destroyed, resources freed
```

```dart
class MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    print("Widget initialized");
    // Initialize controllers, listeners, etc.
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("Dependencies changed");
    // Access inherited widgets safely here
  }

  @override
  Widget build(BuildContext context) {
    print("Widget built");
    return Container(); // Your widget UI
  }

  @override
  void didUpdateWidget(MyWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("Widget updated");
    if (widget.someProperty != oldWidget.someProperty) {
      // Handle property change
    }
  }

  @override
  void deactivate() {
    print("Widget deactivated");
    super.deactivate();
  }

  @override
  void dispose() {
    print("Widget disposed");
    // Clean up resources
    super.dispose();
  }
}
```

#### Route lifecycle

Flutter's navigation system also has lifecycle hooks for pages:

* **didPush**: Called when a new route is pushed
* **didPop**: Called when a route is popped
* **didPushNext**: Called when a new route is pushed on top
* **didPopNext**: Called when a route above is popped

#### Putting the lifecycles to use

* **Resource Management**: Initialize resources in `initState()`, clean up in `dispose()`
* **Network Operations**: Pause network operations when the app goes to background
* **User Session Management**: Track user active status based on app lifecycle
* **Data Persistence**: Save data when the app is paused or inactive
* **UI Updates**: Refresh data when the app returns to the foreground

Understanding these lifecycle methods is crucial for building efficient Flutter applications that properly manage resources and respond to system events.

***

## InheritedWidget

InheritedWidget shares data down a widget subtree without passing it through every constructor. Provider and most other state management packages are built on it.

### Core concept

InheritedWidget is a special type of widget in Flutter's framework that allows descendant widgets to efficiently access data provided by an ancestor widget without explicitly passing it through each level of the widget tree.

### Key features

1. **Data Propagation Down the Tree**: Provides data to all descendants without manual passing through constructors
2. **Rebuild Optimization**: Only widgets that explicitly depend on the data get rebuilt when it changes
3. **Context-Based Access**: Data can be accessed using the BuildContext
4. **Dependency Tracking**: Flutter tracks which widgets depend on which InheritedWidgets

### Basic implementation

Here's how to create a custom InheritedWidget:

```dart
class MyInheritedData extends InheritedWidget {
  final int data;
  final Widget child;

  const MyInheritedData({
    Key? key,
    required this.data,
    required this.child,
  }) : super(key: key, child: child);

  static MyInheritedData? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedData>();
  }

  @override
  bool updateShouldNotify(MyInheritedData oldWidget) {
    return data != oldWidget.data;
  }
}
```

### Core methods

1. **dependOnInheritedWidgetOfExactType**: Establishes a dependency relationship that triggers rebuilds when data changes

   ```dart
   final myData = context.dependOnInheritedWidgetOfExactType<MyInheritedData>();
   ```

2. **getElementForInheritedWidgetOfExactType**: Retrieves the data without establishing a dependency (no automatic rebuilds)

   ```dart
   final myDataElement = context.getElementForInheritedWidgetOfExactType<MyInheritedData>();
   final myData = myDataElement?.widget as MyInheritedData;
   ```

3. **findAncestorWidgetOfExactType**: Alternative method to find widgets without dependency tracking

   ```dart
   final myData = context.findAncestorWidgetOfExactType<MyInheritedData>();
   ```

### `updateShouldNotify`

This method determines when dependent widgets should rebuild:

```dart
@override
bool updateShouldNotify(MyInheritedData oldWidget) {
  // Compare old and new values to determine if dependents should rebuild
  return data != oldWidget.data;
}
```

### Common usage pattern

The standard pattern involves three components:

1. **Data Class**: Holds the actual data and possibly methods to manipulate it
2. **InheritedWidget**: Provides access to the data through the widget tree
3. **State Management Widget**: Typically a StatefulWidget that manages the data and rebuilds the InheritedWidget when needed

Here's a complete example demonstrating a typical usage pattern:

```dart
// 1. Data class
class CounterData {
  int count;
  CounterData({this.count = 0});

  void increment() {
    count++;
  }
}

// 2. InheritedWidget
class CounterInheritedWidget extends InheritedWidget {
  final CounterData data;

  const CounterInheritedWidget({
    Key? key,
    required this.data,
    required Widget child,
  }) : super(key: key, child: child);

  static CounterInheritedWidget of(BuildContext context) {
    final result = context.dependOnInheritedWidgetOfExactType<CounterInheritedWidget>();
    assert(result != null, 'No CounterInheritedWidget found in context');
    return result!;
  }

  @override
  bool updateShouldNotify(CounterInheritedWidget oldWidget) {
    return data.count != oldWidget.data.count;
  }
}

// 3. State Manager Widget
class CounterProvider extends StatefulWidget {
  final Widget child;

  const CounterProvider({Key? key, required this.child}) : super(key: key);

  @override
  _CounterProviderState createState() => _CounterProviderState();
}

class _CounterProviderState extends State<CounterProvider> {
  final counterData = CounterData();

  void incrementCounter() {
    setState(() {
      counterData.increment();
    });
  }

  @override
  Widget build(BuildContext context) {
    return CounterInheritedWidget(
      data: counterData,
      child: widget.child,
    );
  }
}

// Using the inherited widget
class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('InheritedWidget Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Using the inherited widget data
            Text(
              'Count: ${CounterInheritedWidget.of(context).data.count}',
              style: TextStyle(fontSize: 24),
            ),

            // Button to modify the data
            ElevatedButton(
              onPressed: () {
                // Find the state manager and call its method
                final state = context.findAncestorStateOfType<_CounterProviderState>();
                state?.incrementCounter();
              },
              child: Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

// Setting up the app
void main() {
  runApp(
    MaterialApp(
      home: CounterProvider(
        child: CounterScreen(),
      ),
    ),
  );
}
```

### A generic InheritedWidget

For more flexible use cases, you can create a generic InheritedWidget:

```dart
class DataProvider<T> extends InheritedWidget {
  final T data;

  const DataProvider({
    Key? key,
    required this.data,
    required Widget child,
  }) : super(key: key, child: child);

  static T of<T>(BuildContext context) {
    final result = context.dependOnInheritedWidgetOfExactType<DataProvider<T>>();
    assert(result != null, 'No DataProvider<$T> found in context');
    return result!.data;
  }

  @override
  bool updateShouldNotify(DataProvider<T> oldWidget) {
    return data != oldWidget.data;
  }
}
```

### InheritedModel for finer grained dependencies

InheritedModel is a specialized version of InheritedWidget that allows for more selective rebuilds:

```dart
class UserModel extends InheritedModel<String> {
  final String name;
  final int age;

  const UserModel({
    Key? key,
    required this.name,
    required this.age,
    required Widget child,
  }) : super(key: key, child: child);

  static UserModel of(BuildContext context, {String? aspect}) {
    return InheritedModel.inheritFrom<UserModel>(context, aspect: aspect)!;
  }

  @override
  bool updateShouldNotify(UserModel oldWidget) {
    return name != oldWidget.name || age != oldWidget.age;
  }

  @override
  bool updateShouldNotifyDependent(UserModel oldWidget, Set<String> dependencies) {
    return (dependencies.contains('name') && name != oldWidget.name) ||
           (dependencies.contains('age') && age != oldWidget.age);
  }
}

// Usage:
// For name-dependent widgets:
final userName = UserModel.of(context, aspect: 'name').name;

// For age-dependent widgets:
final userAge = UserModel.of(context, aspect: 'age').age;
```

### InheritedWidget vs Provider

InheritedWidget is the foundation upon which Provider and other state management solutions are built. Provider adds:

1. **Simplified API**: Easier to use than raw InheritedWidget
2. **Disposal Logic**: Automatic cleanup of resources
3. **Dependency Injection**: More structured approach to providing dependencies
4. **Multiple Providers**: Easy composition of multiple data sources
5. **Specialized Providers**: Variants like ChangeNotifierProvider, StreamProvider, etc.

### Performance considerations

1. **Minimize InheritedWidget Data**: Keep data small and focused
2. **Be Selective with Dependencies**: Only establish dependencies when needed
3. **Proper Tree Structure**: Place InheritedWidgets at appropriate levels in the tree
4. **Immutable Data**: Consider using immutable data structures

### Common pitfalls

1. **Missing Assert Checks**: Always verify the widget is found in context
2. **Overly Broad Updates**: Improperly implemented `updateShouldNotify` can cause unnecessary rebuilds
3. **Misused Dependency Tracking**: Using methods that don't track dependencies when you need rebuilds
4. **Overly Large InheritedWidgets**: Putting too much data in a single InheritedWidget

Writing an InheritedWidget by hand is worth doing once, to see what Provider is doing for you. After that, reach for the package unless you need something it does not cover.

***

## Why Container can't always be const

The `Container` widget in Flutter often cannot be declared with the `const` keyword due to several important reasons:

### Non-const constructors in children

If a `Container` contains child widgets with non-const constructors, the entire widget cannot be const. For example:

```dart
// This won't work
const Container(
  child: DateTime(2023, 3, 15), // DateTime constructor is not const
)
```

### Runtime determined properties

`Container` often uses properties whose values are determined at runtime:

```dart
// This won't work
const Container(
  width: MediaQuery.of(context).size.width * 0.5, // Runtime value
)
```

### Default behaviour and implicit constraints

Even when not explicitly specified, `Container` often adapts its size and layout based on:

* Parent constraints
* Child size requirements
* Default alignment behavior

These runtime calculations make it non-const.

### Complex internal structure

The `Container` widget is a convenience widget that combines multiple other widgets:

* `Padding`
* `Alignment`
* `DecoratedBox`
* `ConstrainedBox`
* `Transform`

If any of these internal widgets cannot be const, the entire `Container` cannot be const.

### Decoration objects

Many decorations used with `Container` don't have const constructors:

```dart
// This won't work
const Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(...), // Not const constructable
  ),
)
```

### When Container can be const

A `Container` can be const when:

1. All its properties have const values
2. Its child (if any) is also const
3. It doesn't use any runtime-dependent values

Example of a valid const Container:

```dart
const Container(
  width: 100,
  height: 100,
  color: Colors.blue,
  child: const Text('Hello'),
)
```

### Use SizedBox instead

If you're mainly concerned with sizing, `SizedBox` is often a better choice when you need a const widget:

```dart
const SizedBox(
  width: 100,
  height: 100,
  child: const Text('Hello'),
)
```

The `SizedBox` widget is simpler, more focused, and more likely to be usable with the const constructor.
