flutter
/

GetX Theming: Dynamic Theme Switching in Flutter

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX Theming: Dynamic Theme Switching in Flutter

Introduction to Theming with GetX

GetX provides built‑in support for dynamic theming, allowing you to change your app's theme at runtime without needing to restart or rebuild the entire widget tree. With methods like Get.changeTheme and Get.changeThemeMode, you can instantly switch between light, dark, or system themes, and combine it with GetX's reactive state to build a seamless user experience. This guide covers everything from basic theme switching to persisting the user's preference using GetStorage.

Setting Up Themes

First, define your light and dark themes as ThemeData objects. You can place them in a separate file or directly in your main widget.

DARTRead-only
1
class AppThemes {
  static final lightTheme = ThemeData(
    brightness: Brightness.light,
    primarySwatch: Colors.blue,
    scaffoldBackgroundColor: Colors.white,
    appBarTheme: AppBarTheme(
      backgroundColor: Colors.blue,
      foregroundColor: Colors.white,
    ),
  );

  static final darkTheme = ThemeData(
    brightness: Brightness.dark,
    primarySwatch: Colors.blue,
    scaffoldBackgroundColor: Colors.black,
    appBarTheme: AppBarTheme(
      backgroundColor: Colors.grey[900],
      foregroundColor: Colors.white,
    ),
  );
}

Basic Theme Switching

In your GetMaterialApp, set the theme and darkTheme properties. GetX will automatically handle the rest when you call Get.changeTheme.

DARTRead-only
1
GetMaterialApp(
  title: 'My App',
  theme: AppThemes.lightTheme,
  darkTheme: AppThemes.darkTheme,
  themeMode: ThemeMode.system, // or light/dark
  home: HomePage(),
);

To switch themes, simply call:

DARTRead-only
1
// Switch to light theme
Get.changeTheme(ThemeData.light());

// Switch to dark theme
Get.changeTheme(ThemeData.dark());

// Switch theme mode (light/dark/system)
Get.changeThemeMode(ThemeMode.dark);

Reactive Theme Controller

For more control, create a controller that holds the current theme mode and persists it. You can then listen to it in your UI and use it to update other parts of the app.

DARTRead-only
1
class ThemeController extends GetxController {
  var themeMode = ThemeMode.system.obs;

  void setTheme(ThemeMode mode) {
    themeMode.value = mode;
    Get.changeThemeMode(mode);
  }

  void toggleDarkMode() {
    if (themeMode.value == ThemeMode.light) {
      setTheme(ThemeMode.dark);
    } else if (themeMode.value == ThemeMode.dark) {
      setTheme(ThemeMode.light);
    } else {
      // system mode: toggle based on current brightness
      final brightness = Get.mediaQuery?.platformBrightness ?? Brightness.light;
      setTheme(brightness == Brightness.light ? ThemeMode.dark : ThemeMode.light);
    }
  }
}

In your UI, you can use Obx to reflect the current mode and provide a toggle button.

DARTRead-only
1
class SettingsPage extends GetView<ThemeController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Obx(() => SwitchListTile(
          title: Text('Dark Mode'),
          value: controller.themeMode.value == ThemeMode.dark,
          onChanged: (_) => controller.toggleDarkMode(),
        )),
      ),
    );
  }
}

Persisting Theme with GetStorage

To remember the user's theme preference across app restarts, combine GetX with GetStorage. Save the theme mode when changed and load it on app start.

DARTRead-only
1
class ThemeController extends GetxController {
  final storage = GetStorage();
  var themeMode = ThemeMode.system.obs;

  @override
  void onInit() {
    super.onInit();
    // Load saved theme
    String? saved = storage.read('themeMode');
    if (saved != null) {
      themeMode.value = _stringToThemeMode(saved);
      Get.changeThemeMode(themeMode.value);
    }
  }

  void setTheme(ThemeMode mode) {
    themeMode.value = mode;
    storage.write('themeMode', _themeModeToString(mode));
    Get.changeThemeMode(mode);
  }

  String _themeModeToString(ThemeMode mode) {
    return mode.name;
  }

  ThemeMode _stringToThemeMode(String str) {
    return ThemeMode.values.firstWhere((e) => e.name == str, orElse: () => ThemeMode.system);
  }
}

Using GetX with Custom Theme Extensions

For custom colors beyond the standard theme, you can use ThemeExtension. Create a custom extension class and access it via Theme.of(context).extension. This allows you to have app‑specific colors that also react to theme changes.

DARTRead-only
1
class MyColors extends ThemeExtension<MyColors> {
  final Color brandColor;
  final Color accentColor;

  MyColors({required this.brandColor, required this.accentColor});

  @override
  MyColors copyWith({Color? brandColor, Color? accentColor}) {
    return MyColors(
      brandColor: brandColor ?? this.brandColor,
      accentColor: accentColor ?? this.accentColor,
    );
  }

  @override
  MyColors lerp(ThemeExtension<MyColors>? other, double t) {
    if (other is! MyColors) return this;
    return MyColors(
      brandColor: Color.lerp(brandColor, other.brandColor, t)!,
      accentColor: Color.lerp(accentColor, other.accentColor, t)!,
    );
  }
}

// In theme definitions
static final lightTheme = ThemeData(
  // ...
  extensions: [MyColors(brandColor: Colors.blue, accentColor: Colors.orange)],
);

static final darkTheme = ThemeData(
  // ...
  extensions: [MyColors(brandColor: Colors.lightBlue, accentColor: Colors.deepOrange)],
);

// Usage in widgets
final colors = Theme.of(context).extension<MyColors>()!;
Text('Hello', style: TextStyle(color: colors.brandColor));

Best Practices

  • Define all theme data centrally – Keep your light/dark themes in a separate class or file for maintainability.
  • Use Get.changeThemeMode for system‑aware switching – It respects the device's light/dark mode setting.
  • Persist user preference – Store the chosen theme in GetStorage or SharedPreferences.
  • Use a theme controller – Encapsulate the logic for switching and persistence in a single controller.
  • Test theme changes – Ensure UI updates correctly by using Obx or GetBuilder where needed.

Common Mistakes

  • ❌ Calling Get.changeTheme with the same theme – Unnecessary rebuilds. ✅ Only change when the theme actually differs.
  • ❌ Forgetting to set darkTheme in GetMaterialApp – Dark mode may not work properly. ✅ Always define both light and dark themes.
  • ❌ Not handling system theme changes – The app may not respond when the device theme changes. ✅ Use ThemeMode.system and listen to platform brightness.
  • ❌ Hardcoding colors instead of using theme – Makes theme switching ineffective. ✅ Use Theme.of(context) or theme extensions.

FAQ

  • Q: What's the difference between Get.changeTheme and Get.changeThemeMode?
    A: changeTheme directly sets a ThemeData object, while changeThemeMode switches between ThemeMode.light, ThemeMode.dark, or ThemeMode.system, using the predefined light/dark themes from GetMaterialApp.
  • Q: How do I listen to theme changes in a controller?
    A: You can use workers on a reactive variable that holds the theme mode, or simply rely on Obx in the UI.
  • Q: Can I use GetX theming with Cupertino?
    A: Yes, use GetCupertinoApp and define theme and darkTheme as CupertinoThemeData.
  • Q: How do I apply a custom font or text theme with theming?
    A: Include textTheme in your ThemeData for light and dark variations.
  • Q: Does GetX theming work with Flutter's Theme inheritance?
    A: Yes, it's fully compatible. All widgets that read the theme will update when the theme changes.

Conclusion

GetX makes dynamic theming simple and reactive. With just a few lines of code, you can add light/dark mode switching, persist user preferences, and even extend your theme with custom colors. Combined with GetX state management, you can build apps that adapt beautifully to any user preference.

Try it yourself

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';

void main() async {
  await GetStorage.init();
  runApp(MyApp());
}

class ThemeController extends GetxController {
  final storage = GetStorage();
  var themeMode = ThemeMode.system.obs;

  @override
  void onInit() {
    super.onInit();
    String? saved = storage.read('themeMode');
    if (saved != null) {
      themeMode.value = ThemeMode.values.firstWhere(
        (e) => e.name == saved,
        orElse: () => ThemeMode.system,
      );
      Get.changeThemeMode(themeMode.value);
    }
  }

  void setTheme(ThemeMode mode) {
    themeMode.value = mode;
    storage.write('themeMode', mode.name);
    Get.changeThemeMode(mode);
  }

  void toggleDarkMode() {
    if (themeMode.value == ThemeMode.light) {
      setTheme(ThemeMode.dark);
    } else if (themeMode.value == ThemeMode.dark) {
      setTheme(ThemeMode.light);
    } else {
      final brightness = Get.mediaQuery?.platformBrightness ?? Brightness.light;
      setTheme(brightness == Brightness.light ? ThemeMode.dark : ThemeMode.light);
    }
  }
}

class AppThemes {
  static final light = ThemeData(
    brightness: Brightness.light,
    primarySwatch: Colors.blue,
    scaffoldBackgroundColor: Colors.white,
  );
  static final dark = ThemeData(
    brightness: Brightness.dark,
    primarySwatch: Colors.blue,
    scaffoldBackgroundColor: Colors.black,
  );
}

class MyApp extends StatelessWidget {
  final controller = Get.put(ThemeController());

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'GetX Theming',
      theme: AppThemes.light,
      darkTheme: AppThemes.dark,
      themeMode: controller.themeMode.value,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  final controller = Get.find<ThemeController>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Theming Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Obx(() => Text('Current mode: ${controller.themeMode.value.name}')),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => controller.setTheme(ThemeMode.light),
                  child: Text('Light'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () => controller.setTheme(ThemeMode.dark),
                  child: Text('Dark'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () => controller.setTheme(ThemeMode.system),
                  child: Text('System'),
                ),
              ],
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: controller.toggleDarkMode,
              child: Text('Toggle Dark Mode'),
            ),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which GetX method switches between light, dark, or system theme?

A
Get.setTheme
B
Get.changeTheme
C
Get.changeThemeMode
D
Get.updateTheme
Q2
of 3

What is the recommended way to persist the user's theme preference?

A
SharedPreferences
B
GetStorage
C
LocalStorage
D
Both A and B
Q3
of 3

What must be set in GetMaterialApp to support dark mode switching?

A
theme and darkTheme
B
darkMode: true
C
themeMode and darkTheme
D
only theme

Previous

getx testing

Next

getx navigation transitions

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.