samples icon indicating copy to clipboard operation
samples copied to clipboard

[compass_app] Unnecessary screen rebuild on navigation

Open tsantos84 opened this issue 11 months ago • 7 comments

I've cloned the compass_app project to investigate a behavior I'm also encountering in my own application. Surprisingly, compass_app exhibits the same issue: when navigating between screens, the previous screen is rebuilt, triggering its data fetching logic and resulting in an unnecessary backend call.

Here's how to reproduce the behavior:

  1. Start the application in development mode.
  2. Clear the app logs for easier analysis.
  3. From the HomeScreen, tap the first activity. This will navigate to the ActivitiesScreen.
  4. Examine the logs. You'll see the HomeViewModel logs reappear, indicating that the _load method has been called again.

tsantos84 avatar Feb 04 '25 02:02 tsantos84

It appears that this issue might be related to the go_router package. There are several open issues in the Flutter repository that seem to be related:

#158934 #147639 #141281

While I’m not entirely sure if these issues are directly related to this specific problem, they might provide some useful context or overlap. It would be great to investigate further to determine if this is a known issue or a new edge case.

tsantos84 avatar Feb 05 '25 18:02 tsantos84

do we have any workaround/suggestion to prevent re-fetch data when page is loading if possible? thks

lancelot-koh avatar Mar 21 '25 15:03 lancelot-koh

do we have any workaround/suggestion to prevent re-fetch data when page is loading if possible? thks

I guess using DI for the viewModel will solve the issue. I tried the following configuration on my routes:

GoRoute(
      path: Routes.cart, // '/cart'
      builder: (context, state) => CartScreen(viewModel: context.read()), // here the DI
      routes: [
        GoRoute(
          path: ':id', // '/cart/:id'
          builder: (context, state) {
            final viewModel = CartItemViewModel(cart: context.read());
            final id = state.pathParameters['id']!;
            viewModel.load.execute(id);
            return CartItemScreen(viewModel: viewModel);
          },
        ),
      ],
    ),

And instead of loading the data when the viewmodel's created,

class CartViewModel extends ChangeNotifier {
  CartViewModel({required CartRepository cart}) : _cartRepository = cart {
    load = Command0(_load)..execute(); // move this execution
  }
  ...
}

I moved it to the screen state:

class _CartScreenState extends State<CartScreen> {
  ...
  @override
  void initState() {
    super.initState();
    widget.viewModel.load.execute(); // moved here
  }
  ...
}

With this configuration, when user navigate from /cart to /cart/:id I don't experience the "parent-rebuilt" again. When user navigate back to home and navigate again to /cart, the CartScreen will be built as expected .

hanprat avatar Jun 07 '25 11:06 hanprat

Use ⁠ChangeNotifierProvider to prevent the view model from rebuilding. It works for me.

GoRouter router() => GoRouter(
      initialLocation: Routes.settings,
      debugLogDiagnostics: true,
      routes: [
        ShellRoute(
            builder: (context, state, child) {
              return MainLayout(child: child);
            },
            routes: [
              GoRoute(
                  path: Routes.settings,
                  builder: (context, state) {
                    return ChangeNotifierProvider(
                      create: (context) => SettingsViewModel(context.read()), // here 
                      child: Consumer<SettingsViewModel>(
                        builder: (context, viewModel, child) {
                          return SettingsPage(viewModel: viewModel);
                        },
                      ),
                    );
                  },
                  routes: [
                    GoRoute(
                        path: Routes.aboutRelative,
                        builder: (context, state) {
                          final viewModel = AboutViewModel();
                          return AboutPage(viewModel: viewModel);
                        }),
                  ]),
            ])
      ],
    );

shadowfish07 avatar Jun 08 '25 11:06 shadowfish07

@shadowfish07 I tried your solution but what I observe is since the builder runs again the SettingsPage is recreated with a new view model again. Am I missing something?

alperenekin avatar Jun 13 '25 22:06 alperenekin

@shadowfish07 I tried your solution but what I observe is since the builder runs again the SettingsPage is recreated with a new view model again. Am I missing something?

Did you wrap the child in Consumer? I recall this prevents creating a new SettingsPage view model when opening the AboutPage. I'm currently traveling – perhaps in a few days I can double-check this.

shadowfish07 avatar Jun 13 '25 23:06 shadowfish07

Yes Consumer solution did not help me. Maybe it was shell route in your case but that did not help me either šŸ˜•

https://github.com/flutter/flutter/issues/141281#issuecomment-2970010792

alperenekin avatar Jun 14 '25 02:06 alperenekin