Font's theme doesn't rebuilt properly as user changed the app's theme (GetX state management)

Viewed 1039

I've created an app that "almost done", but there is some issue regarding to switching theme. I'm using getx and get_storage for it's state management.

I also using a lot of static type for themes because I think it should never get rebuilt all over the time. It works fine for the other components but not for texts. It acts like this which is kinda weird..

problem visualization

I have no idea what causing this, should I avoid using static classes for themes? Please have a look at my script and feel free to tell me if I did something wrong.

main.dart

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

class MyApp extends StatelessWidget {
  final box = GetStorage();

  @override
  Widget build(BuildContext context) {
    return SimpleBuilder(
      builder: (_) {
        bool isDark = box.read('darkMode');
        return GetMaterialApp(
          ...
          theme: CustomTheme.light,
          darkTheme: CustomTheme.dark,
          themeMode: isDark == null
              ? ThemeMode.system
              : isDark ? ThemeMode.dark : ThemeMode.light,
          ...
        );
      },
    );
  }
}

custom_theme.dart

class CustomTheme {
  static ThemeData get light {
    return ThemeData.light().copyWith(
      ...
      cardTheme: CustomCardTheme.shared,
      textTheme: CustomTextTheme.light,
      ...
    );
  }

  static ThemeData get dark {
    return ThemeData.dark().copyWith(
      ...
      cardTheme: CustomCardTheme.shared,
      textTheme: CustomTextTheme.dark,
      ...
    );
  }
}

card_theme.dart

class CustomCardTheme {
  static CardTheme get shared {
    return CardTheme(
      elevation: 8.0,
      margin: const EdgeInsets.only(bottom: 8.0),
      shadowColor: Colors.black26,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10.0),
      ),
    );
  }
}

text_theme.dart

const String _fontFamily = 'Nunito Sans';

const TextStyle _lightStyle = TextStyle(
  color: Colors.black,
  fontFamily: _fontFamily,
);

const TextStyle _darkStyle = TextStyle(
  color: Colors.white,
  fontFamily: _fontFamily,
);

class CustomTextTheme {
  static TextTheme get light {
    return ThemeData.light().textTheme.copyWith(
          headline1: _lightStyle,
          headline2: _lightStyle,
          headline3: _lightStyle,
          headline4: _lightStyle,
          headline5: _lightStyle,
          headline6: _lightStyle,
          subtitle1: _lightStyle,
          subtitle2: _lightStyle,
          bodyText1: _lightStyle,
          bodyText2: _lightStyle,
          // caption: _secondaryStyle,
        );
  }

  static TextTheme get dark {
    return ThemeData.dark().textTheme.copyWith(
          headline1: _darkStyle,
          headline2: _darkStyle,
          headline3: _darkStyle,
          headline4: _darkStyle,
          headline5: _darkStyle,
          headline6: _darkStyle,
          subtitle1: _darkStyle,
          subtitle2: _darkStyle,
          bodyText1: _darkStyle,
          bodyText2: _darkStyle,
          // caption: _secondaryStyle,
        );
  }
}

profile_menu_card.dart

class ProfileMenuCard extends GetView<ProfileController> {
  const ProfileMenuCard({
    Key key,
    this.iconBackgroundColor,
    this.iconHeight = 18.0,
    this.iconWidth = 18.0,
    this.trailing,
    @required this.label,
    @required this.icon,
    @required this.onTap,
  }) : super(key: key);

  final Color iconBackgroundColor;
  final String label;
  final String icon;
  final double iconHeight;
  final double iconWidth;
  final VoidCallback onTap;
  final Widget trailing;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16.0),
        child: ListTile(
          onTap: onTap,
          contentPadding: EdgeInsets.zero,
          leading: Container(
            height: 32,
            width: 32,
            decoration: BoxDecoration(
              color: iconBackgroundColor ?? Get.theme.primaryColor,
              borderRadius: BorderRadius.circular(10.0),
            ),
            child: Center(
              child: SvgPicture.asset(
                icon,
                height: iconHeight,
                width: iconWidth,
              ),
            ),
          ),
          title: Text(
            StringUtils.capitalize(label, allWords: true),
            style: Get.textTheme.bodyText1.copyWith(
              fontWeight: FontWeight.w600,
            ),
          ),
          trailing: trailing ?? Icon(Icons.chevron_right),
        ),
      ),
    );
  }
}

dark_mode_switch.dart

class DarkModeSwitch extends StatelessWidget {
  final box = GetStorage();

  @override
  Widget build(BuildContext context) {
    bool isDark = box.read('darkMode');

    return Transform.scale(
      scale: 0.8,
      child: CupertinoSwitch(
        value: isDark,
        onChanged: (bool val) => box.write('darkMode', val),
      ),
    );
  }
}

profile_page.dart

const EdgeInsets _titlePadding = EdgeInsets.only(bottom: 10.0, left: 8.0);

class ProfilePage extends GetView<ProfileController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: SingleChildScrollView(
        padding: const EdgeInsets.symmetric(horizontal: 20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            ListViewGroup(
              title: Padding(
                padding: _titlePadding,
                child: Text('account.info'.tr, style: Get.textTheme.caption),
              ),
              items: [
                ProfileMenuCard(
                  icon: 'assets/icons/icon-user-circle.svg',
                  label: 'Personal',
                  onTap: () => Get.toNamed(Routes.ACCOUNT_INFO_PERSONAL),
                ),
                ProfileMenuCard(
                  iconBackgroundColor: Color(0xFFF3548D),
                  icon: 'assets/icons/icon-id-badge.svg',
                  label: 'Employment',
                  onTap: () {
                    // Get.toNamed(Routes.EMPLOYMENT_INFO);
                  },
                ),
                ProfileMenuCard(
                  iconBackgroundColor: Color(0xFF9A5BFF),
                  icon: 'assets/icons/icon-contacts.svg',
                  label: 'Contacts',
                  onTap: () => Get.toNamed(Routes.CONTACT_INFO),
                ),
                ProfileMenuCard(
                  iconBackgroundColor: Color(0xFF00D166),
                  icon: 'assets/icons/icon-book-reader.svg',
                  label: 'Educations',
                  onTap: () => Get.toNamed(Routes.EDUCATION_INFO),
                ),
                ProfileMenuCard(
                  iconBackgroundColor: Color(0xFF57109F),
                  icon: 'assets/icons/icon-briefcase.svg',
                  iconHeight: 15.75,
                  label: 'Working Experiences',
                  onTap: () => Get.toNamed(Routes.EXPERIENCE_INFO),
                ),
                ProfileMenuCard(
                  iconBackgroundColor: Color(0xFF28CEE8),
                  icon: 'assets/icons/icon-credit-card.svg',
                  iconWidth: 18,
                  iconHeight: 14,
                  label: 'Payroll',
                  onTap: () {},
                ),
              ],
            ),
            ListViewGroup(
              title: Padding(
                padding: _titlePadding,
                child: Text('settings'.tr, style: Get.textTheme.caption),
              ),
              items: [
                ProfileMenuCard(
                  iconBackgroundColor: Color(0xFF2DB9B0),
                  icon: 'assets/icons/icon-bell.svg',
                  label: 'Notifications',
                  onTap: () {},
                ),
                ProfileMenuCard(
                  iconBackgroundColor: Color(0xFF555D5C),
                  icon: 'assets/icons/icon-moon.svg',
                  label: 'Dark Mode',
                  trailing: DarkModeSwitch(),
                  onTap: null,
                ),
                ProfileMenuCard(
                  iconBackgroundColor: Color(0xFFFF894B),
                  icon: 'assets/icons/icon-paint.svg',
                  label: 'Color Scheme',
                  onTap: () {},
                ),
              ],
            ),
            ListViewGroup(
              title: Padding(
                padding: _titlePadding,
                child: Text('settings.others'.tr, style: Get.textTheme.caption),
              ),
              items: [
                ProfileMenuCard(
                  icon: 'assets/icons/icon-book.svg',
                  label: 'Official Documentation',
                  onTap: () {},
                ),
                ProfileMenuCard(
                  iconBackgroundColor: Color(0xFF9D99B9),
                  icon: 'assets/icons/icon-info.svg',
                  label: 'About this app',
                  onTap: () {},
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
1 Answers

Themes in Flutter are managed by InheritedWidget ( Theme.of(context) ) to rebuild the Widget that's consuming it's values, much like MediaQuery or Localization... same situation applies, u need to "invalidate" the current context.

You can use static values for fonts, colors, images, whatever... I would use them, as it's more clear to me to build the styles by hand that trying to make sense of all the properties in ThemeData. Although, when u do it that way, you lose the transitions capabilities that Themes has by default to interpolate the values. Anyway, if you are using GetX, just add at the beginning of each Widget you wanna rebuild on Theme change.

build(context){
   context.theme; /// this
   return ...
}

Here's a gist and the demo I made a while ago.

Related