Flutter (100): Global variables and shared state

Time: Column:Mobile & Frontend views:251

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();
    }
  }
}