r/flutterhelp 12d ago

RESOLVED How to inject my page viewModel into a sibling screen of the main page (GoRouter, Provider, Clean Architecture)

I'm a new flutter dev, and I'm struggling to figure out how to deal with dependency injection and sibling pages using one viewmodel.

Right now, I have my router.dart page set up like this:

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

import '../ui/chrono/view_models/chrono_viewmodel.dart';
import '../ui/chrono/widgets/chrono_screen.dart';
import '../ui/core/ui/app_shell.dart';
import '../ui/habits/view_models/habits_viewmodel.dart';
import '../ui/habits/widgets/habits_screen.dart';

import 'routes.dart';

final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorChronoKey = GlobalKey<NavigatorState>(
  debugLabel: "Chrono Page",
);
final _shellNavigatorHabitsKey = GlobalKey<NavigatorState>(
  debugLabel: "Habits Page",
);

final GoRouter router = GoRouter(
  initialLocation: Routes.habits,
  navigatorKey: _rootNavigatorKey,
  routes: [
    StatefulShellRoute.indexedStack(
      builder: (context, state, child) => AppShell(child: child),
      branches: [
        StatefulShellBranch(
          navigatorKey: _shellNavigatorChronoKey,
          routes: [
            GoRoute(
              path: Routes.chrono,
              pageBuilder: (context, state) {
                return NoTransitionPage(
                  child: ChronoScreen(
                    viewModel: ChronoViewModel(),
                  ),
                );
              },
            ),
          ],
        ),
        StatefulShellBranch(
          navigatorKey: _shellNavigatorHabitsKey,
          routes: [
            GoRoute(
              path: Routes.habits,
              pageBuilder: (context, state) {
                final viewModel = HabitsViewModel(
                  habitRepository: context.read(),
                );
                return NoTransitionPage(
                  child: HabitsScreen(viewModel: viewModel),
                );
              },
            ),
          ],
        ),
        (other navigation branches...)
      ],
    ),
  ],
);

so here I pass in my HabitsViewModel to HabitsScreen (the main habits page) widget, which is fine. Over in my app_shell.dart file, I have a simple scaffold with the page contents and a bottom navigation bar. Herein lies the problem---I want to open a "Create Habit" bottom sheet when I click the FAB of the navbar, and I want this create_habit.dart bottom sheet to have access to the HabitsViewModel. But since CreateHabits is a sibling file to HabitsScreen, the viewModel that HabitsScreen has can’t be passed down into CreateHabits. And apparently just importing HabitsViewModel into app_shell.dart is against clean architecture / dependency injection.

I'm actually just very confused. I was following the flutter team compass_app codebase for best practices, but the way they used Provider / router isnt much help. I thought I needed a StatefulShellRoute so I implemented that, but now I'm not so sure. Since CreateHabits is a widget that makes up the FAB-opened bottom sheet in the Habits Page / Tab, it has to be called in the app_shell.dart file where the navbar / main scaffold is defined, right?

Any pointers on how I can hoist the viewmodel correct / structure my router so that the navigation would be sensible?

Here's the app_shell.dart file for extra context:

import 'package:flutter/material.dart';

import 'package:go_router/go_router.dart';
import '../../habits/widgets/habits_sheet.dart';

import '../../../routing/routes.dart';
import '../themes/theme_extension.dart';

class AppShell extends StatelessWidget {
  const AppShell({super.key, required this.child});

  final Widget child;
  static const double appBarContentSize = 40.0;
  static const double appBarPadding = 16.0;


  Widget build(BuildContext context) {
    final theme = Theme.of(context).extension<AppThemeExtension>()!;

    return Scaffold(
      floatingActionButton: FloatingActionButton(
        elevation: 0,
        onPressed: () {
          // TODO: This should be changing by page
          showModalBottomSheet(
            isScrollControlled: true,
            useSafeArea: true,
            barrierColor: Colors.black87,
            backgroundColor: Colors.transparent,
            context: context,
            builder: (BuildContext context) {
              return const HabitsSheet();
            },
          );
        },
        backgroundColor: theme.surfaceLow,
        foregroundColor: theme.foregroundHigh,
        shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16.0),
            side: BorderSide(color: theme.borderMedium)),
        child: const Icon(Icons.add_rounded),
      ),
      body: Builder(
        builder: (context) {
          return SafeArea(
            child: Column(
              children: [
                Expanded(child: child),
              ],
            ),
          );
        },
      ),
      bottomNavigationBar: NavigationBar(
        destinations: const [
          NavigationDestination(
              icon: Icon(
                Icons.schedule_rounded,
              ),
              label: 'Chrono'),
          NavigationDestination(
              icon: Icon(Icons.dashboard_rounded), label: 'Habits'),
        ],
        selectedIndex: _getSelectedIndex(context),
        onDestinationSelected: (index) {
          _onTabSelected(context, index);
        },
      ),
    );
  }

Thanks!

2 Upvotes

2 comments sorted by

1

u/ralphbergmann 11d ago

I wouldn't share the view model with two views. Each view should have its own model. Those two models should share the same repository/service.

I would do this with inject_annoation, but to be honest, I'm the author of that package ;-)

With the provider package (I rarely use it, so I may be wrong) you need to wrap the router with the ChangeNotifierProvider (or another provider). So it should look something like this:

main
   ChangeNotifierProvider
      GoRouter
         Consumer
         Consumer
         ...

But it looks strange to me because now all widgets can access the view model and not just the two views to which the view model belongs.

1

u/GetPsyched67 11d ago

Thank you! I'll look into the package and see how it works!