slovnicki / beamer

A routing package built on top of Router and Navigator's pages API, supporting arbitrary nested navigation, guards and more.
MIT License
582 stars 128 forks source link

BeamerLocation rebuilds on Hot Reload with Nested Beamer #656

Closed Namli1 closed 5 months ago

Namli1 commented 5 months ago

Describe the bug I have a nested Beamer where I navigate from one Location to another Location. Then upon reaching the second location, if I do a Hot Reload, the second location will rebuilt (i.e. buildInit() is called again). But this only happens once, on the second Hot Reload, the location does not rebuilt, everything keeps its state. See the example below for more details.

Beamer version: 2.0.0-dev.0 (but same problem occurs with 1.6.0)

To Reproduce Below is a minimal reproducible example, the setup is as follow:

  1. In the MyApp() widget, the top-level router delegate is configured (but in this example it just redirects to the MainScreen. The problem only occurs when using a nested beamer.)
  2. In the MainScreen() widget, the nested Beamer is set up, consisting of two locations.
  3. When starting the app, the initial location is FirstLocation. A widget is displayed that allows to navigate to the SecondLocation
  4. If you click on the button and navigate to the SecondLocation, the buildInit() method of the SecondLocation is called. This is expected.
  5. Now, if you perform a Hot Reload, the buildInit() method of SecondLocation is called again and there is a navigation animation. This is not the expected behaviour. No navigation was triggered, so the state should just be kept.
import 'package:beamer/beamer.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  //Top-level BeamerDelegate
  final BeamerDelegate routerDelegate = BeamerDelegate(
    locationBuilder: RoutesLocationBuilder(
      routes: {
        '/*': (context, state, data) => MainScreen(),
      },
    ),
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: routerDelegate,
      routeInformationParser: BeamerParser(),
      title: 'Beamer Bug Test',
    );
  }
}

class MainScreen extends StatelessWidget {
  MainScreen({super.key});

  //Nested BeamerDelegate
  final BeamerDelegate routerDelegate = BeamerDelegate(
    initialPath: '/first',
    locationBuilder: BeamerLocationBuilder(
      beamLocations: [
        SecondLocation(),
        FirstLocation(),
      ],
    ),
  );

  @override
  Widget build(BuildContext context) {
    return Beamer(routerDelegate: routerDelegate);
  }
}

class FirstLocation extends BeamLocation<BeamState> {
  @override
  List<Pattern> get pathPatterns => [
        RegExp(("/first")),
      ];

  @override
  void buildInit(BuildContext context) {
    print("FirstLocation build init");
    super.buildInit(context);
  }

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) => [
        BeamPage(
            key: const ValueKey('first'),
            child: Scaffold(
              body: Center(
                child: ElevatedButton(
                  //Navigate to SecondLocation
                  onPressed: () =>
                      Beamer.of(context).beamToNamed('/first/second'),
                  child: const Text("Navigate"),
                ),
              ),
            )),
      ];
}

class SecondLocation extends BeamLocation<BeamState> {
  @override
  List<Pattern> get pathPatterns => [
        RegExp(("/first/second")),
      ];

  @override
  void buildInit(BuildContext context) {
    print("SecondLocation build init");
    super.buildInit(context);
  }

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) => [
        const BeamPage(
          key: ValueKey('second'),
          child: Scaffold(body: Placeholder()),
        ),
      ];
}

Expected behavior After navigating to SecondLocation, the state should be kept and no navigation should occur when performing a Hot Reload.

I am not sure if this is a problem with the setup of the nested navigation or a bug in the package itself. Any help is appreciated!

Namli1 commented 5 months ago

Fixed the problem by myself, it is indeed a problem with the setup of the nested Beamer. When doing a Hot Reload, the Beamer widget gets rebuilt and my guess is that it then starts again at the initial path and navigates to the current path from there, thus rebuilding the SecondLocation and causing an unwanted navigation. The fix is easy, just replace the Beamer widget with a Material.router, that way the path seems to be kept across rebuilds.

Working example:

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  //Top-level BeamerDelegate
  final BeamerDelegate routerDelegate = BeamerDelegate(
    locationBuilder: RoutesLocationBuilder(
      routes: {
        '/*': (context, state, data) => const MainScreen(),
      },
    ),
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: routerDelegate,
      routeInformationParser: BeamerParser(),
      title: 'Beamer Bug Test',
    );
  }
}

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

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  //Nested BeamerDelegate
  late final BeamerDelegate routerDelegate = BeamerDelegate(
    initialPath: '/first',
    locationBuilder: BeamerLocationBuilder(
      beamLocations: [
        SecondLocation(),
        FirstLocation(),
      ],
    ),
  );

  @override
  Widget build(BuildContext context) {
    //Use MaterialApp.router instead of Beamer here
    return MaterialApp.router(
      routerDelegate: routerDelegate,
      routeInformationParser: BeamerParser(),
    );
  }
}

class FirstLocation extends BeamLocation<BeamState> {
  @override
  List<Pattern> get pathPatterns => [
        RegExp("/first"),
      ];

  @override
  void buildInit(BuildContext context) {
    print("FirstLocation build init");
    super.buildInit(context);
  }

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) => [
        BeamPage(
            key: const ValueKey('first'),
            child: Scaffold(
              body: Center(
                child: ElevatedButton(
                  //Navigate to SecondLocation
                  onPressed: () =>
                      Beamer.of(context).beamToNamed('/first/second'),
                  child: const Text("Navigate"),
                ),
              ),
            )),
      ];
}

class SecondLocation extends BeamLocation<BeamState> {
  @override
  List<Pattern> get pathPatterns => [
        RegExp("/first/second"),
      ];

  @override
  void buildInit(BuildContext context) {
    print("SecondLocation build init");
    super.buildInit(context);
  }

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) => [
        const BeamPage(
          key: ValueKey('second'),
          child: Scaffold(body: Placeholder()),
        ),
      ];
}