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 the latest stable version of Flutter. Page last updated on 2024-12-04. View source or report an issue.