slovnicki / beamer

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

Strange behavior when pressing the Android back button on child Beamer #631

Open pishguy opened 1 year ago

pishguy commented 1 year ago

Beamer version: 1.5.6

In my application, at the beginning, I have 3 pages with the following routes:

routerDelegate = BeamerDelegate(
  initialPath: initialPath,
  locationBuilder: RoutesLocationBuilder(
    routes: {
      '/signup': (_, __, ___) => const SignupScreenWidget(),
      '/verifyCode': (_, __, ___) => const VerifyCodeRootScreenWidget(),
      '/home': (_, __, ___) => const HomeScreen(),
    },
  ),
);

Inside the Home page, I defined the following BeamLocations, and since I have another BeamLocation inside the ChefRootWidget that has its own specific links, when I go to the following path:

/home/chefMainWidget/chefStoreHomePage/chefStoreMenu

There is no problem returning to the previous page by clicking on the back icon from chefStoreMenu to chefStoreHomePage. However, after touching the back button on the phone, the return occurs again, i.e., instead of returning to chefStoreHomePage, I am directly taken to the home page, which is wrong, and the page should be displayed. Therefore, both the home and chefStoreHomePage pages have their own specific BeamLocations.

The BeamLocation of the Home page is:

class HomeScreenTab extends BeamLocation<BeamState> {
  HomeScreenTab(super.routeInformation);

  @override
  List<String> get pathPatterns => [
        '/home/chefMainWidget/:storeId',
      ];

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) {
    List<BeamPage> pages = [];
    pages.add(
      const BeamPage(
        key: ValueKey('/home'),
        type: BeamPageType.noTransition,
        child: HomeRootScreenWidget(),
      ),
    );

    if (state.uri.pathSegments.length > 1) {
      String key = '';
      Widget? screen;
      switch (state.uri.pathSegments[1]) {
        case 'chefMainWidget':
          final storeId = state.pathParameters['storeId'];
          key = '/home/chefMainWidget-$storeId-${DateTime.now()}';
          screen = storeId == null ? null : ChefRootWidget(storeId: int.tryParse(storeId));
          break;
      }
      if (screen != null) {
        pages.add(BeamPage(
          key: ValueKey(key),
          type: BeamPageType.slideRightTransition,
          child: screen,
        ));
      }
    }

    return pages;
  }
}

and ChefRootWidget class:

class ChefRootWidget extends StatefulWidget {
  final int? storeId;

  const ChefRootWidget({super.key, required this.storeId});

  @override
  State<StatefulWidget> createState() => _ChefRootWidget();
}

class _ChefRootWidget extends State<ChefRootWidget> {
  int get storeId => widget.storeId!;
  late List<BeamerDelegate> _routerDelegates;

  @override
  void initState() {
    _routerDelegates = [
      BeamerDelegate(
        initialPath: '/home/chefMainWidget/chefStoreHomePage',
        locationBuilder: (routeInformation, _) {
          if (routeInformation.uri.toString().contains('/home/chefMainWidget/chefStoreHomePage')) {
            return ChefStoreBeamer(routeInformation, storeId);
          }
          return NotFound(path: routeInformation.uri.toString());
        },
      ),
    ];
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Beamer(
        routerDelegate: _routerDelegates[0],
      ),
    );
  }
}

The BeamLocation of the chefStoreHomePage page is:

class ChefStoreBeamer extends BeamLocation<BeamState> {
  final int storeId;

  ChefStoreBeamer(super.routeInformation, this.storeId);

  @override
  List<String> get pathPatterns => [
        '/home/chefMainWidget/chefStoreHomePage/chefStoreMenu/:menuId',
      ];

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) {
    List<BeamPage> pages = [];
    pages.add(
      BeamPage(
        key: ValueKey('/home/chefMainWidget/chefStoreHomePage-$storeId-${DateTime.now()}'),
        type: BeamPageType.slideRightTransition,
        child: ChefStoreHomePageWidget(storeId: storeId),
      ),
    );
    if (state.uri.pathSegments.contains('chefStoreMenu')) {
      final menuId = state.pathParameters['menuId'];
      if (menuId != null) {
        pages.add(
          BeamPage(
            key: ValueKey('/home/chefMainWidget/chefStoreHomePage/chefStoreMenu-$menuId-${DateTime.now()}'),
            type: BeamPageType.slideRightTransition,
            child: ChefMenuWidget(menuId: int.tryParse(menuId)),
          ),
        );
      }
    }
    return pages;
  }
}

What I do is use Beamer.of(context).beamToNamed to move between pages, for example:

Beamer.of(context).beamToNamed('/home/chefMainWidget/chefStoreHomePage/chefStoreMenu/$menuId');

its a full sample code:

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

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

class ALocation extends BeamLocation<BeamState> {
  ALocation(super.routeInformation);

  @override
  List<String> get pathPatterns => ['/a/child-root'];

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) {
    List<BeamPage> pages = [];
    pages.add(
      const BeamPage(
        key: ValueKey('/a'),
        type: BeamPageType.noTransition,
        child: RootScreen(),
      ),
    );

    if (state.pathPatternSegments.contains('child-root')) {
      pages.add(
        const BeamPage(
          key: ValueKey('/a/child-root'),
          type: BeamPageType.noTransition,
          child: ChildScreen(),
        ),
      );
    }

    return pages;
  }
}

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

  @override
  State<RootBeamerScreen> createState() => _RootBeamerScreenState();
}

class _RootBeamerScreenState extends State<RootBeamerScreen> {
  final _routerDelegates = [
    BeamerDelegate(
      initialPath: '/a',
      locationBuilder: (routeInformation, _) {
        if (routeInformation.uri.toString().contains('/a')) {
          return ALocation(routeInformation);
        }
        return NotFound(path: routeInformation.uri.toString());
      },
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Beamer(
        routerDelegate: _routerDelegates[0],
      ),
    );
  }
}

class RootScreen extends StatelessWidget {
  const RootScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('root'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            const Padding(padding: EdgeInsets.all(4)),
            TextButton(
              onPressed: () => Beamer.of(context).beamToNamed('/a/child-root'),
              child: const Text('View child-root'),
            ),
          ],
        ),
      ),
    );
  }
}

class ChildLocation extends BeamLocation<BeamState> {
  ChildLocation(super.routeInformation);

  @override
  List<String> get pathPatterns => ['/child-root/detail'];

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) {
    List<BeamPage> pages = [];
    pages.add(
      const BeamPage(
        key: ValueKey('/child-root'),
        type: BeamPageType.noTransition,
        child: ChildDetailWidget(),
      ),
    );

    if (state.pathPatternSegments.contains('detail')) {
      pages.add(
        const BeamPage(
          key: ValueKey('/child-root/detail'),
          type: BeamPageType.noTransition,
          child: DetailScreen(),
        ),
      );
    }

    return pages;
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('child screen')),
      body: const SizedBox(
        width: double.infinity,
        height: double.infinity,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text('Yooohooooooooo'),
          ],
        ),
      ),
    );
  }
}

class ChildScreen extends StatefulWidget {
  const ChildScreen({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _ChildScreen();
}

class _ChildScreen extends State<ChildScreen> {
  final _routerDelegates = [
    BeamerDelegate(
      initialPath: '/child-root',
      locationBuilder: (routeInformation, _) {
        if (routeInformation.uri.toString().contains('/child-root')) {
          return ChildLocation(routeInformation);
        }
        return NotFound(path: routeInformation.uri.toString());
      },
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Beamer(
        routerDelegate: _routerDelegates[0],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('child screen')),
      body: SizedBox(
        width: double.infinity,
        height: double.infinity,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            TextButton(
              onPressed: () =>
                  Beamer.of(context).beamToNamed('/child-root/detail'),
              child: const Text('Click here'),
            ),
          ],
        ),
      ),
    );
  }
}

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

  final routerDelegate = BeamerDelegate(
    initialPath: '/a',
    locationBuilder: RoutesLocationBuilder(
      routes: {
        '*': (context, state, data) => const RootBeamerScreen(),
      },
    ),
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(primarySwatch: Colors.indigo),
      routerDelegate: routerDelegate,
      routeInformationParser: BeamerParser(),
      backButtonDispatcher: BeamerBackButtonDispatcher(
        delegate: routerDelegate,
      ),
    );
  }
}