So I am at the dependency injection part of the responsive scaffold and canonical layout adaptive pattern. It comes to mind that I have not covered the computer science reasons for the tools we use to implement state management in flutter apps.
So let me cover that now and at the end of the article show the core dependency injection pattern one has to know how to implement.
Why State Management
If you can explain the-why, then you can pick which is the correct solutions. Why do we have state management? Some parts of state management are there due to how the language operates.
Let's start with Dart, which is a mixed imperative and declarative computer language of both classes and functions. Everything is fine until we hit the GUI, then we have state sync problems and of course dependency cyclic problems.
Now, let me answer things in a set of problems
Separation Of Concerns
Classes in computer programming are the original application of separation of concerns. As classes act as modules within the program. But, while we can use the OOP tools such as inheritance to set up business logic it does not work the same for the GUI.
In the GUI we have to avoid using OOP and instead use first class functions to compose the user interface. And that presents a state even problem and dependency problem. Basically, we need to form two object graphs that have acrylic directed graph-node edges that do not cycle back in both the dependency area and the state event area.
That involves implementing the next concept in two different variations, that is Inversion Of Control.
Inversion Of Control
On the dependency side the inversion of control refers to setting up some container in which the container's API controls how dependencies are recognized and used in the application lifecycle. And because the graph only exists during runtime it is often referred to as the runtime binding.
Non Widget Dependency Injection
We seek the light start up container which is usually like the package get_it in that it only registers the dependencies by type into the container.
Widget Dependency Injection
Widget dependency injection came about as the original need to include the ThemeData model. Google came up with inherited widget and model at that time and the community eventually came together and expanded that into provider and riverpod packages. Its main advantage is that both packages have widget tools that control the UI rebuild. Its main disadvantages is that they cannot do dependency injection outside of the widget tree.
The second variation of IoC is the state event one in which we want the event state flow to be by an API rather than any class OOP method. While the Flutter SDK has training wheel based notifiers as limited observers; those tools lack the sophistication to be able to handle complex primitive immutable types found in most mature big apps.
Thus, most of us have to go outside of the Flutter SDK to find an observer based system that can handle the more complex primitive immutable types found in models and of course be more composition and reactive based. Composition on passing the state events through the state management but still using the observer pattern to interface with the class.
Several of us in the Flutter community have come up with keeping certain things about state management as separate areas of concerns, namely:
-Dependency Injection, runtime binding of dependencies only
-Portion of UI re-building, do not combine it with state solutions
-Composable communication of state events and observing with
decent observer support to interface with the widget classes in the UI.
So now let me highlight the common dependency injection case we have to implement; the dreaded chained dependencies via the get_it package.
Chained Dependency Injection Pattern
So in apps we have either:
MV-Controller-Service-Something
MV-ViewModel-Service-Something
So we need a way to declare a chained dependency. In the get_it package we can do that by registering the first part of the chain and then register the other dependency calling it via:
// ambient variable to access the service locator | |
import 'package:get_it/get_it.dart'; | |
import 'package:state_part_get_it/src/settings/settings_controller.dart'; | |
import 'package:state_part_get_it/src/settings/settings_service.dart'; | |
GetIt sl = GetIt.instance; | |
void setUp() { | |
// In General register the dependent serivce | |
// before the other entity using it. | |
sl.registerSingleton<SettingsService>(SettingsService()); | |
sl.registerFactory<SettingsController>(() => SettingsController(sl < SettingsService>())); | |
} |
Then we will still do the local call in the same format but make it equal to calling the dependency via the get_it reference:
import 'package:flutter/material.dart'; | |
import 'package:state_part_get_it/service_locator.dart'; | |
import 'settings_controller.dart'; | |
/// Displays the various settings that can be customized by the user. | |
/// | |
/// When a user changes a setting, the SettingsController is updated and | |
/// Widgets that listen to the SettingsController are rebuilt. | |
class SettingsView extends StatelessWidget { | |
SettingsView({super.key}); | |
static const routeName = '/settings'; | |
final SettingsController controller = sl<SettingsController>(); | |
//final SettingsController controller; | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('Settings'), | |
), | |
body: Padding( | |
padding: const EdgeInsets.all(16), | |
// Glue the SettingsController to the theme selection DropdownButton. | |
// | |
// When a user selects a theme from the dropdown list, the | |
// SettingsController is updated, which rebuilds the MaterialApp. | |
child: DropdownButton<ThemeMode>( | |
// Read the selected themeMode from the controller | |
value: controller.themeMode, | |
// Call the updateThemeMode method any time the user selects a theme. | |
onChanged: controller.updateThemeMode, | |
items: const [ | |
DropdownMenuItem( | |
value: ThemeMode.system, | |
child: Text('System Theme'), | |
), | |
DropdownMenuItem( | |
value: ThemeMode.light, | |
child: Text('Light Theme'), | |
), | |
DropdownMenuItem( | |
value: ThemeMode.dark, | |
child: Text('Dark Theme'), | |
) | |
], | |
), | |
), | |
); | |
} | |
} |
Notice we keep the UI rebuilding logic outside of how we are managing state as:
import 'package:flutter/material.dart'; | |
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | |
import 'package:flutter_localizations/flutter_localizations.dart'; | |
import 'package:state_part_get_it/service_locator.dart'; | |
import 'package:state_part_get_it/src/custom_scroll_behavior.dart'; | |
import 'package:state_part_get_it/src/global_context.dart'; | |
import 'package:state_part_get_it/src/layout_utils.dart'; | |
import 'sample_feature/sample_item_details_view.dart'; | |
import 'sample_feature/sample_item_list_view.dart'; | |
import 'settings/settings_controller.dart'; | |
import 'settings/settings_view.dart'; | |
/// The Widget that configures your application. | |
class MyApp extends StatelessWidget { | |
MyApp({ | |
super.key, | |
}); | |
final SettingsController settingsController = sl<SettingsController>(); | |
@override | |
Widget build(BuildContext context) { | |
// Glue the SettingsController to the MaterialApp. | |
// | |
// The ListenableBuilder Widget listens to the SettingsController for changes. | |
// Whenever the user updates their settings, the MaterialApp is rebuilt. | |
return ListenableBuilder( | |
listenable: settingsController, | |
builder: (BuildContext context, Widget? child) { | |
return MaterialApp( | |
// turn off the debug banner | |
debugShowCheckedModeBanner: false, | |
// so we have scroll for web and all devices | |
scrollBehavior: CustomScrollBehavior(), | |
// used both here and for Nav 2.0 in MaterialApp.router so that we can use a global context | |
// for localization outside of UI in case | |
// of implementing models | |
scaffoldMessengerKey: scaffoldMessengerKey, | |
// Providing a restorationScopeId allows the Navigator built by the | |
// MaterialApp to restore the navigation stack when a user leaves and | |
// returns to the app after it has been killed while running in the | |
// background. | |
restorationScopeId: 'app', | |
// Provide the generated AppLocalizations to the MaterialApp. This | |
// allows descendant Widgets to display the correct translations | |
// depending on the user's locale. | |
localizationsDelegates: const [ | |
AppLocalizations.delegate, | |
GlobalMaterialLocalizations.delegate, | |
GlobalWidgetsLocalizations.delegate, | |
GlobalCupertinoLocalizations.delegate, | |
], | |
supportedLocales: const [ | |
Locale('en', ''), // English, no country code | |
], | |
// Use AppLocalizations to configure the correct application title | |
// depending on the user's locale. | |
// | |
// The appTitle is defined in .arb files found in the localization | |
// directory. | |
onGenerateTitle: (BuildContext context) => | |
AppLocalizations.of(context)!.appTitle, | |
// Define a light and dark color theme. Then, read the user's | |
// preferred ThemeMode (light, dark, or system default) from the | |
// SettingsController to display the correct theme. | |
theme: ThemeData( | |
brightness: Brightness.light, | |
visualDensity: LayoutUtils.getVisualDensity(context),), | |
darkTheme: ThemeData( | |
brightness: Brightness.dark, | |
visualDensity: LayoutUtils.getVisualDensity(context),), | |
themeMode: settingsController.themeMode, | |
// Define a function to handle named routes in order to support | |
// Flutter web url navigation and deep linking. | |
onGenerateRoute: (RouteSettings routeSettings) { | |
return MaterialPageRoute<void>( | |
settings: routeSettings, | |
builder: (BuildContext context) { | |
switch (routeSettings.name) { | |
case SettingsView.routeName: | |
return SettingsView(); | |
case SampleItemDetailsView.routeName: | |
return const SampleItemDetailsView(); | |
case SampleItemListView.routeName: | |
default: | |
return const SampleItemListView(); | |
} | |
}, | |
); | |
}, | |
); | |
}, | |
); | |
} | |
} |
Conclusion
Its counter to what Google says, but your dependency injection needs to separate from the UI re-building not combined into it. Provider and riverpod have devtool extensions due to the caching problems using both those solutions. Not to mention the chaining of BLoC is a nightmare no matter whether you use provider or riverpod.
Next up, is the preparation for changing from navigator 1.0 to navigator 2.0 using router through the GoRouter package. The source code can be found here:
https://github.com/fredgrott/master_flutter_adaptive
The general idea is that CodeLabs is lacking in worked out solutions to some common use-cases and so I am creating some repositories that offer those solutions.