Skip to content

go_router - RestorationScope doesn't work inside a ShellRoute #174935

@zamesilyasa

Description

@zamesilyasa

Steps to reproduce

  1. Create a GoRouter instance
  2. Put a Page inside a GoRoute
  3. Add restoration scope somewhere inside the page
  4. Wrap the GoRoute with A ShellRoute

Expected results

Restoration logic serializes and deserializes the data

Actual results

Restoration logic only serializes the data, the page starts from scratch when android restores the state. The logic works fine in case we remove the ShellRoute

Code sample

Code sample
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(const MyApp());
}

final routerConfig = GoRouter(
  restorationScopeId: 'go_router_scope_id',
  initialLocation: '/',
  routes: [
    ShellRoute(
      restorationScopeId: 'go_router_scope_id_for_shell_route',
      routes: [
        GoRoute(
          path: '/',
          builder: (context, state) => const MyHomePage(),
        ),
      ],
      builder: (context, state, child) {
        return RestorationScope(
          restorationId: 'custom_restoration_scope',
          child: child,
        );
      },
    ),
  ],
);

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      restorationScopeId: 'app_scope',
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      routerConfig: routerConfig,
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with RestorationMixin {
  final RestorableCounterState _counter = RestorableCounterState();

  @override
  String? get restorationId => 'home_page';

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

  void _incrementCounter() {
    _counter.value.counter = CounterDto(
      _counter.value.counter.counter + 1,
      _counter.value.counter.counterName,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider.value(
      value: _counter.value,
      child: Scaffold(
        appBar: AppBar(title: const Text('Title')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.all(16),
                child: TextFormField(
                  decoration: const InputDecoration(
                    labelText: 'Counter name',
                  ),
                  onChanged: (value) {
                    _counter.value.counter = CounterDto(
                      _counter.value.counter.counter,
                      value,
                    );
                  },
                ),
              ),
              const Text('You have pushed the button this many times:'),
              Consumer<CounterState>(
                builder: (context, state, _) {
                  return Text(
                    '${state.counter.counter}',
                    style: Theme.of(context).textTheme.headlineMedium,
                  );
                },
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: _incrementCounter,
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

class RestorableCounterState extends RestorableChangeNotifier<CounterState> {
  @override
  CounterState createDefaultValue() {
    print('RestorableCounterState -> Creating default value');
    return CounterState();
  }

  @override
  CounterState fromPrimitives(Object? data) {
    print('RestorableCounterState -> Restoring $data');
    final map = data as Map;
    final json = map.cast<String, dynamic>();
    return CounterState.fromValue(CounterDto.fromJson(json));
  }

  @override
  Object? toPrimitives() {
    print('RestorableCounterState -> Serializing ${value.counter}');
    return value.counter.toJson();
  }
}

class CounterState extends ChangeNotifier {
  CounterState();
  CounterState.fromValue(this._counter);

  CounterDto _counter = CounterDto(0, 'N/A');

  CounterDto get counter => _counter;

  set counter(CounterDto value) {
    _counter = value;
    notifyListeners();
  }
}

class CounterDto {
  final int counter;
  final String counterName;

  CounterDto(this.counter, this.counterName);

  @override
  String toString() => 'counter: $counter; name: $counterName';

  factory CounterDto.fromJson(Map<String, dynamic> json) => CounterDto(
    json['counter'],
    json['counterName'],
  );

  Map<String, dynamic> toJson() => {
    'counter': counter,
    'counterName': counterName,
  };
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.35.2, on macOS 15.6.1 24G90 darwin-arm64, locale en-US) [827ms]
    • Flutter version 3.35.2 on channel stable at /Users/ilias/Library/Flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 05db968908 (10 days ago), 2025-08-25 10:21:35 -0700
    • Engine revision a8bfdfc394
    • Dart version 3.9.0
    • DevTools version 2.48.0
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations,
      enable-lldb-debugging

[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [2.0s]
    • Android SDK at /Users/ilias/Library/Android/sdk
    • Emulator version 36.1.9.0 (build_id 13823996) (CL:N/A)
    • Platform android-36, build-tools 36.0.0
    • ANDROID_HOME = /Users/ilias/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 21.0.7+-13880790-b1038.58)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 16.4) [1,739ms]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 16F6
    • CocoaPods version 1.16.2

[✓] Chrome - develop for the web [73ms]
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2025.1) [72ms]
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 21.0.7+-13880790-b1038.58)

[✓] IntelliJ IDEA Community Edition (version 2025.1.4.1) [72ms]
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin version 86.0.2
    • Dart plugin version 251.27812.12

[✓] Connected device (5 available) [6.9s]
    • Pixel 9 (mobile)            • 192.168.1.58:5555         • android-arm64  • Android 16 (API 36)
    • sdk gphone64 arm64 (mobile) • emulator-5554             • android-arm64  • Android 16 (API 36) (emulator)
    • Ilias (wireless) (mobile)   • 00008110-000E04CE3E2B801E • ios            • iOS 18.6.2 22G100
    • macOS (desktop)             • macos                     • darwin-arm64   • macOS 15.6.1 24G90 darwin-arm64
    • Chrome (web)                • chrome                    • web-javascript • Google Chrome 140.0.7339.80

[✓] Network resources [764ms]
    • All expected network resources are available.

• No issues found!

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listfound in release: 3.35Found to occur in 3.35found in release: 3.36Found to occur in 3.36has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: go_routerThe go_router packagepackageflutter/packages repository. See also p: labels.team-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions