Harshit Singh
Harshit Singh

Reputation: 11

Flutter GoRouter context.push() unexpected behaviour

In the past few days, I'm working on a project and got stuck with gorouter's behaviour. Below, I'm going to explain what it is -

class ABCScreen extends StatelessWidget {
  final String id;
  const ABCScreen({super.key, required this.id});

  @override
  Widget build(BuildContext context) {
    return Consumer<ABCState>(builder: (context, future, child) {
      return FutureBuilder(
        future: future.call(id: id),
        builder: ((context, snapshot) {
          if (snapshot.data != null &&
              snapshot.connectionState == ConnectionState.done) {
            return XYZScreen(model: snapshot.data!);
          } else {
            return const ShimmerScreen();
          }
        }),
      );
    });
  }

}

So ABCScreen basically responsible for fetching data from a REST API Endpoint using a provider and passes the data to XYZ Screen for build. Also it uses FutureBuilder. This is how the State looks -

class ABCState extends ChangeNotifierProvider {

  Future<Model?> call({required String id}) async {
    Model? data = await ABCServices().fetchData(id: id); // REST API call
    if (data != null) {
      return data;
    }
    return null;
  }

}

Now, Here I'm aware that I'm not storing the API result into some variable in the provider and then use these variable to build the Screen, because I'm getting my desired results with future builder and I don't like to change things.

So, the major problem with GoRouter is arised form the XYZScreen (child screen of the ABCScreen ), this screen contains a navigation route to another QWEScreen(having defined path in gorouter route configuration, let's say /path),

  1. when navigating to this screen using context.push('/path'), it builds the entire parent widget again i.e. ABCScreen (which results in single extra calls to APIs).
  2. while using Navigator.push() and context.go(), everything works perfectly fine, i.e. no extra calls to API i.e. no rebuild/re-render of parent widget.

Also, since I've used goRouter as my app's primary route method, I want to stick to it and not want to use Navigator.push(). Seems like I've some error while implementing the code. Please have a look, any help will be appreciated. Thank you in advance :)

class XYZScreen extends StatelessWidget {
  const XYZScreen({
    super.key,
    required this.model,
  });

  final Model model ;

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {
        if (context.mounted) {
          context.push('/path');
        }
      },
      child: SomeContainer(model: model),
    );
  }
}

The exepectation is just to use goRouter in navigation while ensuring that no extra calls/ rebuild of the widget takes place.

Upvotes: 0

Views: 191

Answers (1)

ininmm
ininmm

Reputation: 552

I've ask the Flutter members, and according to the issue, if the view widget is not a const class, the GoRouter would rebuild the view as a expect behavior.

You have two solutions to solved this question:

  1. (Suggestion) Change your widget to a const class, i.e., make ABCScreen to be a const class, and if your still need to pass a parameter into widget, consider using:
    • Provider for global state management in your case.
    • Pass parameters through the Provider (less suitable for complex data).
  2. Another way is conditional widget building with GoRouter:
    • Benefit:
      • Useful when you cannot make ABCScreen a const class (e.g., requires dynamic initialization).
    • Implementation:
      • This approach checks if the current location (screen) matches the ABCScreen route.
      • If they don't match (navigation is happening), a SizedBox.shrink() is returned to avoid rebuilding ABCScreen.
      • If they match (returning to ABCScreen), the widget is built with the provided parameter.
  GoRoute(
    path: ABCScreen.routePath,
    pageBuilder: (BuildContext context, GoRouterState state) {
      final String parameter = state.extra as String;
        return MaterialPage<void>(
          child: Builder(builder: (BuildContext context) {
            // FIXME:https://stackoverflow.com/a/56666801/5643911
            // FIXME:https://github.com/flutter/flutter/issues/152906
            if (state.matchedLocation != GoRouter.of(context).location) {
              return const SizedBox.shrink();
            }
          return ABCScreen(id: parameter);
        }),
      );
    },
  ),

------------
extension GoRouterExtensions on GoRouter {
  String get location {
    final RouteMatch lastMatch = routerDelegate.currentConfiguration.last;
    final RouteMatchList matchList =
        lastMatch is ImperativeRouteMatch ? lastMatch.matches : routerDelegate.currentConfiguration;
    return matchList.uri.toString();
  }
}

Hope the answer providers clear solution for your Flutter project!

Upvotes: 0

Related Questions