How ChatMCP Handles Theme Switching Between Light, Dark, and System Modes
ChatMCP implements theme switching through a Provider-based architecture that persists user preferences to SharedPreferences, exposes a dropdown selector in the General Settings UI, and applies the selected mode via Flutter's MaterialApp themeMode property.
ChatMCP is an open-source Flutter client for Model Context Protocol (MCP) servers that provides a customizable chat interface. Theme switching in ChatMCP allows users to toggle between light, dark, and system-default appearances, with preferences persisted across app restarts. The implementation relies on Provider state management and Flutter's built-in theming capabilities to deliver a reactive user experience.
Persistent Storage with SharedPreferences
Theme preferences in ChatMCP are stored as part of the GeneralSetting model and persisted using SharedPreferences. The default theme is set to light when no prior selection exists.
In lib/provider/settings_provider.dart, the theme field is defined within the GeneralSetting class (lines 88‑96). The provider exposes the updateGeneralSettingsPartially method (lines 14‑28) to modify this value asynchronously. When called, this method updates the in-memory model, encodes it to JSON, and writes it to SharedPreferences under the key generalSettings.
Future<void> updateGeneralSettingsPartially({String? theme, …}) async {
final prefs = await SharedPreferences.getInstance();
_generalSetting = GeneralSetting(
theme: theme ?? _generalSetting.theme,
// other fields unchanged …
);
await prefs.setString('generalSettings', jsonEncode(_generalSetting.toJson()));
notifyListeners();
}
The notifyListeners() call triggers a rebuild in any widget consuming this provider, ensuring the UI reflects the new preference immediately.
Theme Selection UI
The settings interface allows users to select their preferred theme through a dropdown in the General Settings screen. Located in lib/page/setting/general_setting.dart (lines 24‑64), the UI uses a DropdownButtonFormField bound to the current value from SettingsProvider.
When a user selects a new option, the widget invokes settings.updateGeneralSettingsPartially(theme: value) to persist the change:
DropdownButtonFormField<String>(
value: settings.generalSetting.theme,
items: const [
DropdownMenuItem(value: 'light', child: CText(text: l10n.lightTheme)),
DropdownMenuItem(value: 'dark', child: CText(text: l10n.darkTheme)),
DropdownMenuItem(value: 'system', child: CText(text: l10n.followSystem)),
],
onChanged: (value) {
if (value != null) {
settings.updateGeneralSettingsPartially(theme: value);
}
},
);
This pattern decouples the UI from storage logic, delegating persistence concerns to the provider while the widget focuses purely on presentation and user input handling.
Applying Themes at the Application Root
The root MyApp widget in lib/main.dart (lines 77‑90) consumes SettingsProvider via a Consumer widget and applies the theme using Flutter's MaterialApp properties. The implementation provides distinct ThemeData configurations for light and dark modes, while the themeMode property determines which theme is active.
A private helper method _getThemeMode (lines 5‑14) maps the stored string values to Flutter's ThemeMode enum:
return Consumer<SettingsProvider>(
builder: (context, settings, child) {
return MaterialApp(
theme: ThemeData(useMaterial3: true, brightness: Brightness.light),
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark),
themeMode: _getThemeMode(settings.generalSetting.theme),
// …
);
},
);
ThemeMode _getThemeMode(String theme) {
switch (theme) {
case 'light': return ThemeMode.light;
case 'dark': return ThemeMode.dark;
case 'system':
default: return ThemeMode.system;
}
}
When the user selects System, ThemeMode.system instructs Flutter to automatically follow the platform's brightness setting without requiring additional platform channel code.
Programmatic Theme Changes
For testing or automated configuration, themes can be changed programmatically by invoking the provider method directly:
await SettingsProvider().updateGeneralSettingsPartially(theme: 'dark');
This bypasses the UI layer while maintaining the same persistence and notification guarantees.
Summary
- Storage Layer:
lib/provider/settings_provider.dartmanages theGeneralSetting.themefield, persists values to SharedPreferences, and notifies listeners of changes. - UI Layer:
lib/page/setting/general_setting.dartprovides aDropdownButtonFormFieldthat callsupdateGeneralSettingsPartially()when selections change. - Application Layer:
lib/main.dartconsumes the provider, maps string values toThemeModevia_getThemeMode(), and suppliesThemeDatatoMaterialApp. - System Integration: Selecting
systemdelegates brightness detection to Flutter's built-in platform brightness handling.
Frequently Asked Questions
Where does ChatMCP store the selected theme preference?
ChatMCP stores the theme preference in SharedPreferences as a JSON-encoded string under the key generalSettings. The SettingsProvider class in lib/provider/settings_provider.dart handles serialization and deserialization of the GeneralSetting object, ensuring the theme persists across app restarts.
What happens when a user selects "System" theme mode?
When the user selects system, the _getThemeMode helper in lib/main.dart returns ThemeMode.system. Flutter then automatically matches the app's brightness to the operating system's current setting (light or dark) without requiring manual platform checks or additional code in ChatMCP.
How does the UI update immediately after changing the theme?
The SettingsProvider class extends Flutter's ChangeNotifier. When updateGeneralSettingsPartially() is called, it invokes notifyListeners() after persisting the new value. The root MyApp widget wraps MaterialApp in a Consumer<SettingsProvider>, which rebuilds the entire app widget tree when the provider notifies listeners, applying the new ThemeMode instantly.
Can the theme be changed from outside the settings screen?
Yes. Any component with access to the SettingsProvider instance can change the theme programmatically by calling updateGeneralSettingsPartially(theme: 'dark') (or light/system). This updates the persisted preference and triggers a global UI rebuild, though the primary interface for this action is the dropdown in the General Settings page.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →