Communicating between layers
Along with defining clear responsibilities for each component of the architecture, it's important to consider how the components communicate. This refers to both the rules that dictate communication, and the technical implementation of how components communicate. An app's architecture should answer the following questions:
- Which components are allowed to communicate with which other components (including components of the same type)?
 - What do these components expose as output to each other?
 - How is any given layer 'wired up' to another layer?
 
                  
                
Using this diagram as a guide, the rules of engagement are as follows:
| Component | Rules of engagement | 
|---|---|
| View | 
                          
  | 
                      
| ViewModel | 
                          
  | 
                      
| Repository | 
                          
  | 
                      
| Service | 
                          
  | 
                      
Dependency injection
#
                  This guide has shown how these different components communicate
                  with each other by using inputs and outputs.
                  In every case, communication between two layers is facilitated by passing
                  a component into the constructor methods (of the components that
                  consume its data), such as a Service into a Repository.
                
class MyRepository {
  MyRepository({required MyService myService})
          : _myService = myService;
  late final MyService _myService;
}
                    
                    
                    
                  
                  One thing that's missing, however, is object creation. Where,
                  in an application, is the MyService instance created so that it can be
                  passed into MyRepository?
                  This answer to this question involves a
                  pattern known as dependency injection.
                
                  In the Compass app, dependency injection is handled using
                  package:provider. Based on their experience building Flutter apps,
                  teams at Google recommend using package:provider to implement
                  dependency injection.
                
                  Services and repositories are exposed to the top level of the widget tree of
                  the Flutter application as Provider objects.
                
runApp(
  MultiProvider(
    providers: [
      Provider(create: (context) => AuthApiClient()),
      Provider(create: (context) => ApiClient()),
      Provider(create: (context) => SharedPreferencesService()),
      ChangeNotifierProvider(
        create: (context) => AuthRepositoryRemote(
          authApiClient: context.read(),
          apiClient: context.read(),
          sharedPreferencesService: context.read(),
        ) as AuthRepository,
      ),
      Provider(create: (context) =>
        DestinationRepositoryRemote(
          apiClient: context.read(),
        ) as DestinationRepository,
      ),
      Provider(create: (context) =>
        ContinentRepositoryRemote(
          apiClient: context.read(),
        ) as ContinentRepository,
      ),
      // In the Compass app, additional service and repository providers live here.
    ],
    child: const MainApp(),
  ),
);
                    
                    
                    
                  
                  Services are exposed only so they can immediately be
                  injected into repositories via the BuildContext.read method from provider,
                  as shown in the preceding snippet.
                  Repositories are then exposed so that they can be
                  injected into view models as needed.
                
                  Slightly lower in the widget tree, view models that correspond to
                  a full screen are created in the package:go_router
                   configuration,
                  where provider is again used to inject the necessary repositories.
                
// This code was modified for demo purposes.
GoRouter router(
  AuthRepository authRepository,
) =>
    GoRouter(
      initialLocation: Routes.home,
      debugLogDiagnostics: true,
      redirect: _redirect,
      refreshListenable: authRepository,
      routes: [
        GoRoute(
          path: Routes.login,
          builder: (context, state) {
            return LoginScreen(
              viewModel: LoginViewModel(
                authRepository: context.read(),
              ),
            );
          },
        ),
        GoRoute(
          path: Routes.home,
          builder: (context, state) {
            final viewModel = HomeViewModel(
              bookingRepository: context.read(),
            );
            return HomeScreen(viewModel: viewModel);
          },
          routes: [
            // ...
          ],
        ),
      ],
    );
                    
                    
                    
                  
                  Within the view model or repository, the injected component should be private.
                  For example, the HomeViewModel class looks like this:
                
class HomeViewModel extends ChangeNotifier {
  HomeViewModel({
    required BookingRepository bookingRepository,
    required UserRepository userRepository,
  })  : _bookingRepository = bookingRepository,
        _userRepository = userRepository;
  final BookingRepository _bookingRepository;
  final UserRepository _userRepository;
  // ...
}
                    
                    
                    
                  Private methods prevent the view, which has access to the view model, from calling methods on the repository directly.
This concludes the code walkthrough of the Compass app. This page only walked through the architecture-related code, but it doesn't tell the whole story. Most utility code, widget code, and UI styling was ignored. Browse the code in the Compass app repository for a complete example of a robust Flutter application built following these principles.
Feedback
#As this section of the website is evolving, we welcome your feedback!
Unless stated otherwise, the documentation on this site reflects Flutter 3.35.5. Page last updated on 2025-9-5. View source or report an issue.