csells / go_router

The purpose of the go_router for Flutter is to use declarative routes to reduce complexity, regardless of the platform you're targeting (mobile, web, desktop), handling deep linking from Android, iOS and the web while still allowing an easy-to-use developer experience.
https://gorouter.dev
441 stars 97 forks source link

context.Push forgets parameters of parent route #351

Closed bw-flagship closed 2 years ago

bw-flagship commented 2 years ago

Scenario: We have two nested routes (families + persons) and a third, separate route (home). When I am on home and then use context.go to reach a specific person, it works. But when I use context.push with the same argument, it does not work because the first parameter (family id) is not available. A workaround is to push the families screen first and the persons-screen afterwards.

To provide a reproducible sample, I copied the "GoRouter Example: Nested Navigation" and modified it accordingly. When you run the code below, you find three buttons to demonstrate the issue.

If I can assist with any further information, do not hesitate to ask.

Thanks!

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

import 'shared/data.dart';

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

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

  static const title = 'GoRouter Example: Nested Navigation';

  @override
  Widget build(BuildContext context) => MaterialApp.router(
        routeInformationParser: _router.routeInformationParser,
        routerDelegate: _router.routerDelegate,
        title: title,
      );

  late final _router = GoRouter(
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => const HomeScreen(),
      ),
      GoRoute(
        path: '/family/:fid',
        builder: (context, state) => FamilyTabsScreen(
          key: state.pageKey,
          selectedFamily: Families.family(state.params['fid']!),
        ),
        routes: [
          GoRoute(
            path: 'person/:pid',
            builder: (context, state) {
              final family = Families.family(state.params['fid']!);
              final person = family.person(state.params['pid']!);

              return PersonScreen(family: family, person: person);
            },
          ),
        ],
      ),
    ],

    // show the current router location as the user navigates page to page; note
    // that this is not required for nested navigation but it is useful to show
    // the location as it changes
    navigatorBuilder: (context, state, child) => Material(
      child: Column(
        children: [
          Expanded(child: child),
          Padding(
            padding: const EdgeInsets.all(8),
            child: Text(state.location),
          ),
        ],
      ),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                context.go('/family/f2/person/p1');
              },
              child: const Text("Go (works)"),
            ),
            ElevatedButton(
              onPressed: () {
                context.push('/family/f2');
                context.push('/family/f2/person/p1');
              },
              child: const Text("Push seperately (works)"),
            ),
            ElevatedButton(
              onPressed: () {
                context.push('/family/f2/person/p1');
              },
              child: const Text("Push (does not work)"),
            ),
          ],
        ),
      ),
    );
  }
}

class FamilyTabsScreen extends StatefulWidget {
  FamilyTabsScreen({required Family selectedFamily, Key? key})
      : index = Families.data.indexWhere((f) => f.id == selectedFamily.id),
        super(key: key) {
    assert(index != -1);
  }

  final int index;

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

class _FamilyTabsScreenState extends State<FamilyTabsScreen>
    with TickerProviderStateMixin {
  late final TabController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TabController(
      length: Families.data.length,
      vsync: this,
      initialIndex: widget.index,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  void didUpdateWidget(FamilyTabsScreen oldWidget) {
    super.didUpdateWidget(oldWidget);
    _controller.index = widget.index;
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: const Text(App.title),
          bottom: TabBar(
            controller: _controller,
            tabs: [for (final f in Families.data) Tab(text: f.name)],
            onTap: (index) => _tap(context, index),
          ),
        ),
        body: TabBarView(
          controller: _controller,
          children: [for (final f in Families.data) FamilyView(family: f)],
        ),
      );

  void _tap(BuildContext context, int index) =>
      context.go('/family/${Families.data[index].id}');
}

class FamilyView extends StatefulWidget {
  const FamilyView({required this.family, Key? key}) : super(key: key);
  final Family family;

  @override
  State<FamilyView> createState() => _FamilyViewState();
}

class _FamilyViewState extends State<FamilyView>
    with AutomaticKeepAliveClientMixin {
  // Override `wantKeepAlive` when using `AutomaticKeepAliveClientMixin`.
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    // Call `super.build` when using `AutomaticKeepAliveClientMixin`.
    super.build(context);
    return Column(
      children: [
        Expanded(
          child: ListView(
            children: [
              for (final p in widget.family.people)
                ListTile(
                  title: Text(p.name),
                  onTap: () =>
                      context.go('/family/${widget.family.id}/person/${p.id}'),
                ),
            ],
          ),
        ),
      ],
    );
  }
}

class PersonScreen extends StatelessWidget {
  const PersonScreen({required this.family, required this.person, Key? key})
      : super(key: key);

  final Family family;
  final Person person;

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(person.name)),
        body: Text('${person.name} ${family.name} is ${person.age} years old'),
      );
}
csells commented 2 years ago

@bw-flagship this sounds like a bug. is this still an issue for you?

bw-flagship commented 2 years ago

@csells Yes it is. it is reproducable it with the provided sample and go_router 3.0.1

MrLightful commented 2 years ago

Yep, encountered the same issue. Trying to push through multiple levels at once, but doesn't work. And yes, indeed, looks like, after the 1st route, the state loses params for some reason (although they are in the uri).

NeroThroN commented 2 years ago

I have encounter the same issue in my project. Furthermore I have created a minimal code to reproduce this behavior :

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

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

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

  final _router = GoRouter(
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => Screen(title: "Home - ${state.location}"),
        routes: [
          GoRoute(
            path: 'family/:fid',
            builder: (context, state) => Screen(title: "Family ${state.params['fid']} - ${state.location}"),
            routes: [
              GoRoute(
                path: 'person/:pid',
                builder: (context, state) => Screen(title: "Family ${state.params['fid']} - Person ${state.params['pid']} - ${state.location}"),
              ),
            ],
          ),
        ],
      ),
    ],
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'GoRouter',
      routeInformationParser: _router.routeInformationParser,
      routerDelegate: _router.routerDelegate,
    );
  }
}

class Screen extends StatelessWidget {
  final String title;
  const Screen({Key? key, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            button(context, "Family 3", '/family/3'),
            button(context, "Family 3 person 2", '/family/3/person/2'),
          ],
        ),
      ),
    );
  }

  Widget button(BuildContext context, String label, String path) {
    void Function() onPressed = () => context.push(path); // Here the [:fid] is null when push nested route
    // void Function() onPressed = () => context.go(path); // Here the [:fid] is retrive correctly

    return ElevatedButton(onPressed: onPressed, child: Text(label));
  }
}

WORKAROUND: We can easily parse the state.location to retrieve the :fid inside the builder function of the GoRoute


// To search number ID
String? getIDInsideURI(String url, String label) => RegExp(r'\/' + label + r'\/\d*').stringMatch(url)?.split("/").last;
// To search uuid ID
String? getUUIDInsideURI(String url, String label) => RegExp(r'\/' + label + r'\/[\d\w-]*').stringMatch(url)?.split("/").last;

// -----------------

GoRoute(
  path: 'person/:pid',
  builder: (context, state) {
    String fid = getIDInsideURI(state.location, "family")!;
    return Screen(title: "Family $fid - Person ${state.params['pid']} - ${state.location}");
  }
),