Global Variables and Shared State in Flutter Applications
Applications typically contain variables that are relevant throughout their lifecycle, such as current user information and local settings. In Flutter, we categorize information that needs to be globally shared into two types: global variables and shared state. Global variables refer to those that persist throughout the app's lifecycle, used simply to store information or encapsulate global tools and methods. Shared state, on the other hand, refers to information that needs to be shared across components or routes. This information is usually also a global variable, but the key difference is that when shared state changes, it must notify all components that use it, while global variables do not require this. Therefore, we manage global variables and shared states separately.
1 Global Variables - Global Class
We create a Global
class in the lib/common
directory, which mainly manages the app's global variables, defined as follows:
// Provide five optional theme colors const _themes = <MaterialColor>[ Colors.blue, Colors.cyan, Colors.teal, Colors.green, Colors.red, ]; class Global { static late SharedPreferences _prefs; static Profile profile = Profile(); // Network cache object static NetCache netCache = NetCache(); // Optional theme list static List<MaterialColor> get themes => _themes; // Is it a release version? static bool get isRelease => bool.fromEnvironment("dart.vm.product"); // Initialize global information, executed at app startup static Future init() async { WidgetsFlutterBinding.ensureInitialized(); _prefs = await SharedPreferences.getInstance(); var _profile = _prefs.getString("profile"); if (_profile != null) { try { profile = Profile.fromJson(jsonDecode(_profile)); } catch (e) { print(e); } } else { // Default theme index is 0, representing blue profile = Profile()..theme = 0; } // If there's no cache strategy, set the default cache strategy profile.cache = profile.cache ?? CacheConfig() ..enable = true ..maxAge = 3600 ..maxCount = 100; // Initialize network request-related configurations Git.init(); } // Persist Profile information static saveProfile() => _prefs.setString("profile", jsonEncode(profile.toJson())); }
The meanings of the fields in the Global
class are documented in comments, so we won’t elaborate here. It’s important to note that init()
needs to be executed at app startup, so the main method of the application is as follows:
void main() => Global.init().then((e) => runApp(MyApp()));
Here, it's crucial to ensure that the Global.init()
method does not throw exceptions; otherwise, runApp(MyApp())
will not be executed.
2 Shared State
With global variables in place, we also need to consider how to share state across components. While we could replace all shared states with global variables, this is not a good practice in Flutter development. Component states are tied to the UI, and when the state changes, we expect the UI components that depend on that state to update automatically. Using global variables would require manually handling state change notifications, receiving mechanisms, and variable-component dependencies. Therefore, in this example, we use the Provider
package introduced earlier to achieve cross-component state sharing, requiring us to define related providers. The states to be shared include user login information, app theme information, and app language information. Since changes to this information must immediately notify other dependent widgets to update, we should use ChangeNotifierProvider
. Additionally, these changes require updating the Profile
information and persisting it.
We can define a base class called ProfileChangeNotifier
, which models that need to be shared can inherit from:
class ProfileChangeNotifier extends ChangeNotifier { Profile get _profile => Global.profile; @override void notifyListeners() { Global.saveProfile(); // Save Profile changes super.notifyListeners(); // Notify dependent widgets to update } }
2-1. User State
The user state updates and notifies its dependencies when the login state changes, defined as follows:
class UserModel extends ProfileChangeNotifier { User get user => _profile.user; // Is the app logged in? (If there is user information, it indicates logged in) bool get isLogin => user != null; // When user information changes, update it and notify dependent widgets to update set user(User user) { if (user?.login != _profile.user?.login) { _profile.lastLogin = _profile.user?.login; _profile.user = user; notifyListeners(); } } }
2-2. App Theme State
The theme state updates and notifies its dependencies when the user changes the app theme, defined as follows:
class ThemeModel extends ProfileChangeNotifier { // Get the current theme; if no theme is set, default to blue ColorSwatch get theme => Global.themes .firstWhere((e) => e.value == _profile.theme, orElse: () => Colors.blue); // When the theme changes, notify dependencies; the new theme takes effect immediately set theme(ColorSwatch color) { if (color != theme) { _profile.theme = color[500].value; notifyListeners(); } } }
2-3. App Language State
When the app language is set to follow the system (Auto), it updates when the system language changes. When the user selects a specific language (like American English or Simplified Chinese), the app will continue to use the user-selected language and will no longer change with the system language. The language state class is defined as follows:
class LocaleModel extends ProfileChangeNotifier { // Get the current user's app language configuration as Locale; if null, follow system language Locale getLocale() { if (_profile.locale == null) return null; var t = _profile.locale.split("_"); return Locale(t[0], t[1]); } // Get the string representation of the current locale String get locale => _profile.locale; // When the user changes the app language, notify dependencies to update; the new language takes effect immediately set locale(String locale) { if (locale != _profile.locale) { _profile.locale = locale; notifyListeners(); } } }