Skip to main content

Implementing State Restoration

1. Understanding State Restoration Concept

State restoration in Flutter allows an app to preserve and restore the state of its UI when the app is closed and reopened. This ensures a seamless user experience by maintaining the app's state across sessions.

Concept Description Details
State Restoration Process of saving and restoring UI state Enables apps to maintain user context across app sessions
Automatic Restoration Flutter automatically restores basic UI state Includes scroll positions, text field values, and toggle states
Manual Restoration Developer-controlled state restoration Used for complex state like navigation stacks or custom data models

Example: Enabling State Restoration

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      restorationScopeId: 'my_app',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with RestorationMixin {
  String? _restoredValue;

  @override
  void restoreState(RestorationBucket? bucket, bool isReset) {
    _restoredValue = bucket?.read<String>('value');
  }

  @override
  void saveState(RestorationBucket bucket) {
    bucket.write('value', _restoredValue);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('State Restoration')),
      body: Center(
        child: Text(_restoredValue ?? 'No value restored'),
      ),
    );
  }
}
Note: The restorationScopeId is crucial for grouping related state. It must be unique within the app to prevent conflicts.

2. Enabling State Restoration (restorationScopeId)

Property Description Values
restorationScopeId Identifier for the state restoration scope String

Example: Enabling State Restoration

class MyStatefulWidget extends StatefulWidget {
  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> with RestorationMixin {
  final String restorationId = 'my_widget_restoration_id';

  @override
  void restoreState(Snapshot? snapshot, restorationId) {
    // Restore state logic here
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Text('State restored!'),
        ),
      ),
    );
  }
}

3. Using RestorationMixin

The RestorationMixin is a powerful tool in Flutter that simplifies state restoration by automatically managing the lifecycle of restoration data for widgets.

Property Description Usage
restorationId Unique identifier for the widget's restoration state. String or RestorationId
restoredState Callback to handle restored state values. void Function(RestoredState)

Example: Using RestorationMixin

class MyWidget extends StatefulWidget with RestorationMixin {
  @override
  final String restorationId = 'my_widget';

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> with RestorationMixin {
  int counter = 0;

  @override
  void restoreState(RestoredState? restored, bool isInitialRestore) {
    if (restored != null) {
      counter = restored.getInt('counter') ?? 0;
    }
  }

  void incrementCounter() {
    setState(() {
      counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Restored Counter')),
      body: Center(
        child: Text('Counter: $counter'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: incrementCounter,
        child: Icon(Icons.add),
      ),
    );
  }
}
Note: The restorationId must be unique within the scope of the widget tree to avoid conflicts in state restoration.
Warning: Ensure that all state variables are properly restored to prevent inconsistent UI states.

4. Creating RestorableProperties

Restorable properties in Flutter allow you to save and restore the state of widgets across app sessions. They are particularly useful for maintaining user input, selections, and other transient data.

Property Type Description Example
RestorableProperty<T> Generic Base class for restorable properties. Subclasses define specific types like RestorableString, RestorableInt. final _counter = RestorableInt(0);
RestorableString String Used to restore string values like user input or text fields. final _textFieldValue = RestorableString('');
RestorableInt Integer Used to restore integer values like counters or indices. final _selectedIndex = RestorableInt(0);
RestorableDouble Double Used to restore double values like slider positions or decimal inputs. final _sliderValue = RestorableDouble(0.5);
RestorableBool Boolean Used to restore boolean values like toggle switches or flags. final _isSwitchOn = RestorableBool(false);

Example: Using RestorableProperty in a StatefulWidget

class MyRestorableWidget extends StatefulWidget {
  @override
  _MyRestorableWidgetState createState() => _MyRestorableWidgetState();
}

class _MyRestorableWidgetState extends State<MyRestorableWidget> with RestorationMixin {
  final _counter = RestorableInt(0);
  final _textFieldValue = RestorableString('');

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    registerForRestoration(_counter, 'counter');
    registerForRestoration(_textFieldValue, 'text_field_value');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Restorable Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Counter: ${_counter.value}'),
            ElevatedButton(
              onPressed: () {
                _counter.value++;
              },
              child: Text('Increment'),
            ),
            TextField(
              controller: TextEditingController(text: _textFieldValue.value),
              onChanged: (value) {
                _textFieldValue.value = value;
              },
            ),
          ],
        ),
      ),
    );
  }
}
Note: Make sure to call registerForRestoration in the restoreState method to ensure that your properties are properly restored.
Warning: If a property is not registered for restoration, it will not be restored when the app is restarted.

5. Registering Properties

In Flutter, state restoration involves registering properties that should be persisted across app sessions. This is typically done using the RestorationMixin or by implementing custom restoration logic.

Property Type Description Default Value
restorationId String? Unique identifier for the restoration state. If not provided, a default ID is generated. null
restorableProperties List<RestorableProperty> List of properties to be restored. These must implement RestorableProperty. []

Example: Registering a Property

class MyWidget extends StatefulWidget with RestorationMixin {
  @override
  void restoreState(RestorationBucket? bucket, bool initialRestore) {
    // Restore state from the bucket
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      // Example usage of restoration ID
      restorationId: 'my_widget',
    );
  }
}

6. Using RestorableRouteFuture

RestorableRouteFuture is a utility class in Flutter that helps manage the restoration of asynchronous operations when a route is restored from a saved state. It ensures that futures initiated before the app was suspended or navigated away are properly resumed when the route is reactivated.

Property Description Usage
future The asynchronous operation to be restored. RestorableRouteFuture<String>(future: fetchData())
onError Callback to handle errors during the future’s execution. onError: (error) => print('Error: $error')
onSuccess Callback to handle successful completion of the future. onSuccess: (result) => setState(() => data = result)

Example: Using RestorableRouteFuture in a Route

class MyRoute extends StatefulWidget {
  @override
  State<MyRoute> createState() => _MyRouteState();
}

class _MyRouteState extends State<MyRoute> with RestorableRouteFuture<String> {
  late RestorableRouteFuture<String> _fetchFuture;

  @override
  void initState() {
    super.initState();
    _fetchFuture = RestorableRouteFuture<String>(future: fetchData());
  }

  Future<String> fetchData() async {
    await Future.delayed(Duration(seconds: 2));
    return 'Fetched Data';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Restorable Route')),
      body: Center(
        child: FutureBuilder<String>(
          future: _fetchFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              return Text(snapshot.data!);
            }
            return CircularProgressIndicator();
          },
        ),
      ),
    );
  }
}
Note: RestorableRouteFuture is particularly useful in apps that rely heavily on asynchronous data fetching and need to maintain state across app suspensions or route navigations.

7. Implementing Form State Restoration

Property Description Usage
restorationId Unique identifier for restoring state Assign to form widgets to enable restoration
autovalidateMode Controls when validation occurs Set to AutovalidateMode.onUserInteraction for real-time feedback

Example: Form with Restoration

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> with RestorationMixin {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();

  @override
  String get restorationId => 'my_form';

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    _nameController.text = restorationBucket?.readString('name') ?? '';
    _emailController.text = restorationBucket?.readString('email') ?? '';
  }

  @override
  void saveState(RestorationBucket bucket) {
    bucket.writeString('name', _nameController.text);
    bucket.writeString('email', _emailController.text);
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      restorationId: 'my_form',
      child: Column(
        children: [
          TextFormField(
            controller: _nameController,
            restorationId: 'name',
            decoration: InputDecoration(labelText: 'Name'),
          ),
          TextFormField(
            controller: _emailController,
            restorationId: 'email',
            decoration: InputDecoration(labelText: 'Email'),
          ),
        ],
      ),
    );
  }
}
Note: Ensure all form fields have restorationId to capture their values.
Warning: RestorationMixin must be applied to the state class, not the widget.

8. Restoring Scroll Position

Property Description Usage
primaryScrollController Controls the primary scroll position for a scrollable widget. primaryScrollController: ScrollController()
scrollDirection Defines the direction of scrolling (vertical or horizontal). scrollDirection: Axis.vertical

Example: Restoring Scroll Position

class ScrollPositionRestorationExample extends StatefulWidget {
  @override
  _ScrollPositionRestorationExampleState createState() => _ScrollPositionRestorationExampleState();
}

class _ScrollPositionRestorationExampleState extends State<ScrollPositionRestorationExample> with RestorationMixin {
  late RestorableInt _selectedIndex = RestorableInt(0);
  late RestorableScrollController _scrollController = RestorableScrollController();

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    registerForRestoration(_scrollController, 'scroll_controller');
    registerForRestoration(_selectedIndex, 'selected_index');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        controller: _scrollController,
        itemCount: 100,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('Item $index'),
          );
        },
      ),
    );
  }
}
Note: The RestorationMixin is required to restore scroll position across app sessions.

9. Testing State Restoration

Example: Testing with WidgetTester

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

void main() {
  testWidgets('State restoration test', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        restorationScopeId: 'my_scope',
        home: Scaffold(
          body: CounterWidget(),
        ),
      ),
    );

    // Simulate state change
    await tester.tap(find.byType(ElevatedButton));
    await tester.pumpAndSettle();

    // Verify state is preserved
    expect(find.text('1'), findsOneWidget);
  });
}
Test Method Description Notes
testWidgets Flutter's testing utility for widget tests Used to test UI components and their state behavior
pumpWidget Builds and mounts a widget tree Essential for initializing the test environment
tap Simulates a tap gesture on a widget Used to simulate user interaction that changes state
pumpAndSettle Waits for all animations and microtasks to complete Ensures state changes are fully processed before assertions
Note: State restoration tests should verify that the app correctly restores state after being terminated and restarted.
Warning: Ensure that restorationScopeId is consistent across test runs to properly simulate state restoration.

Testing Best Practices

  • Use testWidgets for comprehensive widget testing
  • Simulate user interactions with tap and enterText
  • Use pumpAndSettle to ensure all state changes are processed
  • Verify that state is correctly restored after app termination

10. Handling Restoration on Different Platforms

Platform Restoration Support Notes
Android NEW Full Supports state restoration through AutomaticKeepAliveClientMixin and PageStorage.
iOS NEW Full Similar to Android, with AutomaticKeepAliveClientMixin and PageStorage.
Web Partial State restoration is limited due to browser behavior and session management.
Desktop (macOS, Windows, Linux) Experimental State restoration is not fully supported in desktop Flutter apps.
Web (PWA) Partial State can be restored using SharedPreferences or IndexedDB.

Example: Platform-specific State Restoration

import &#x27;package:flutter/material.dart&#x27;;

class PlatformStateRestoration extends StatefulWidget {
  @override
  _PlatformStateRestorationState createState() => _PlatformStateRestorationState();
}

class _PlatformStateRestorationState extends State<PlatformStateRestoration> with AutomaticKeepAliveClientMixin {
  int counter = 0;

  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context); // Required for AutomaticKeepAliveClientMixin
    return Scaffold(
      appBar: AppBar(title: Text("State Restoration")),
      body: Center(
        child: Text("Counter: $counter"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            counter++;
          });
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
Note: On web, consider using SharedPreferences or IndexedDB for persistent state storage.
Warning: Desktop Flutter apps may not fully support state restoration. Use PageStorage cautiously.

Was this article helpful?