【Flutter】新手向,多主题与国际化/本地化的配置与使用

850 阅读18分钟

多主题与国际化的配置与使用

前言

在如今的移动应用开发中,用户体验的优劣直接影响着应用的受欢迎程度和用户留存率。而主题和国际化是提升用户体验的重要方面。多主题支持使得用户能够根据自己的喜好选择合适的界面风格,而国际化则确保了应用能够满足不同语言用户的需求。这些都是我们实际开发应用中的“刚需”。

Flutter 作为一个现代化的跨平台开发框架,提供了强大的主题和国际化功能,使得开发者能够轻松实现这些需求。

而 GetX 作为一个轻量级的状态管理和路由管理库,也为主题和国际化的实现提供了简化的解决方案。

在本文中,我们将深入探讨如何在 Flutter 中使用多主题与国际化功能,分别从 Flutter 原生的实现方式以及使用 GetX 的方式进行详细讲解。

接下来,我们将从多主题样式的使用开始,介绍如何在Flutter应用中实现黑暗模式、亮色模式等多种主题样式,并进一步讲解如何在GetX中实现类似的功能。随后,我们将转向国际化的配置与使用,探讨如何支持中英文等多语言环境的切换,以及在GetX中如何简化国际化的操作。希望通过本篇文章,能够帮助开发者们更好地理解并运用这些功能,从而提升应用的用户体验。

一、黑暗模式,亮色模式等多主题样式的使用

在现代应用开发中,支持多种主题样式,特别是黑暗模式和亮色模式,已经成为了一项基本需求。这不仅提升了用户体验,也满足了用户个性化的需求。接下来,我们将分别探讨如何在Flutter中实现多主题样式,以及如何使用GetX框架来简化这一过程。

1.1 Flutter的用法

在Flutter中实现多主题样式相对简单。我们可以使用ThemeData类来定义不同的主题样式。以下是一个简单的示例,演示如何在Flutter应用中实现黑暗模式和亮色模式的切换。

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool isDarkMode = false;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: isDarkMode ? ThemeData.dark() : ThemeData.light(),
      home: Scaffold(
        appBar: AppBar(
          title: Text('多主题示例'),
          actions: [
            IconButton(
              icon: Icon(isDarkMode ? Icons.wb_sunny : Icons.nights_stay),
              onPressed: () {
                setState(() {
                  isDarkMode = !isDarkMode;
                });
              },
            ),
          ],
        ),
        body: Center(
          child: Card(
            color: Theme.of(context).cardColor, // 使用主题的cardColor
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Text(
                '当前主题: ${isDarkMode ? "黑暗模式" : "亮色模式"}',
                style: TextStyle(fontSize: 24, color: Theme.of(context).primaryColor), // 使用主题的primaryColor
              ),
            ),
          ),
        ),
      ),
    );
  }
}

在这个示例中我们通过 isDarkMode 变量来控制我们的应用是显示暗色模式还是亮色模式,从而使用 ThemeData.dark() 还是 ThemeData.light() 。

那么在下面的控件中 Card 组件的背景颜色使用了Theme.of(context).cardColor,而文本的颜色使用了Theme.of(context).primaryColor。这样,无论是黑暗模式还是亮色模式,组件中的颜色都会自动适应当前主题。

因为 cardColor 和 primaryColor 都是 ThemeData 中默认的属性,我们直接用变量即可实现主题变色。

那么我的第一个疑问来了?虽然 ThemeData 可以默认有对应的颜色值但是和我们UI设计师给的颜色值不同啊,怎么办?

所以我们就可以创建自己的 ThemeData 亮色和暗色的模式,示例如下:

static ThemeData createTheme({
    required Brightness brightness,
    required Color background,
    required Color primaryText,
    Color? secondaryText,
    required Color accentColor,
    Color? divider,
    Color? buttonBackground,
    required Color buttonText,
    Color? cardBackground,
    Color? disabled,
    required Color error,
  }) {

    return ThemeData(
      brightness: brightness,
      canvasColor: background,
      cardColor: background,
      dividerColor: divider,
      primaryColor: accentColor,
      unselectedWidgetColor: hexToColor('#DADCDD'),
      dividerTheme: DividerThemeData(
        color: divider,
        space: 1,
        thickness: 1,
      ),
      cardTheme: CardTheme(
        color: cardBackground,
        margin: EdgeInsets.zero,
        clipBehavior: Clip.antiAliasWithSaveLayer,
      ),
      textSelectionTheme: TextSelectionThemeData(
        selectionColor: accentColor,
        selectionHandleColor: accentColor,
        cursorColor: accentColor,
      ),
      appBarTheme: AppBarTheme(
        color: cardBackground,
        titleTextStyle: TextStyle(
          color: secondaryText,
          fontSize: 18,
        ),
        iconTheme: IconThemeData(
          color: secondaryText,
        ),
      ),
      iconTheme: IconThemeData(
        color: secondaryText,
        size: 16.0,
      ),
      buttonTheme: ButtonThemeData(
        textTheme: ButtonTextTheme.primary,
        colorScheme: ColorScheme(
          brightness: brightness,
          primary: accentColor,
          secondary: accentColor,
          surface: background,
          background: background,
          error: error,
          onPrimary: buttonText,
          onSecondary: buttonText,
          onSurface: buttonText,
          onBackground: buttonText,
          onError: buttonText,
        ),
        padding: const EdgeInsets.all(16.0),
      ),
      cupertinoOverrideTheme: CupertinoThemeData(
        brightness: brightness,
        primaryColor: accentColor,
      ),
      inputDecorationTheme: InputDecorationTheme(
        errorStyle: TextStyle(color: error),
        labelStyle: TextStyle(
          fontFamily: 'Rubik',
          fontWeight: FontWeight.w600,
          fontSize: 16.0,
          color: primaryText.withOpacity(0.5),
        ),
        hintStyle: TextStyle(
          color: secondaryText,
          fontSize: 13.0,
          fontWeight: FontWeight.w300,
        ),
      ),

    );
  }

  static ThemeData get lightTheme => createTheme(
        brightness: Brightness.light,
        background: ColorConstants.lightScaffoldBackgroundColor,
        cardBackground: ColorConstants.secondaryAppColor,
        primaryText: Colors.black,
        secondaryText: Colors.black,
        accentColor: ColorConstants.secondaryAppColor,
        divider: ColorConstants.secondaryAppColor,
        buttonBackground: Colors.black38,
        buttonText: ColorConstants.secondaryAppColor,
        disabled: ColorConstants.secondaryAppColor,
        error: Colors.red,
      );

  static ThemeData get darkTheme => createTheme(
        brightness: Brightness.dark,
        background: ColorConstants.darkScaffoldBackgroundColor,
        cardBackground: ColorConstants.secondaryDarkAppColor,
        primaryText: Colors.white,
        secondaryText: Colors.white,
        accentColor: ColorConstants.secondaryDarkAppColor,
        divider: Colors.black45,
        buttonBackground: Colors.white,
        buttonText: ColorConstants.secondaryDarkAppColor,
        disabled: ColorConstants.secondaryDarkAppColor,
        error: Colors.red,
      );

第二个疑问又来了,我不想用它的自带属性,又少不说又不符合我的页面场景,我想要自定义颜色属性,然后分别在亮色暗色模式设置不同的值,这能不能实现啊。

当然也是可以实现的,我们可以通过 ThemeExtension,你可以方便地扩展 Flutter 的主题系统,以满足应用的个性化需求。这种方式使得自定义属性的管理更加清晰,并能够根据不同的主题模式返回不同的样式

import 'package:flutter/material.dart';

//定义扩展方便获取到颜色值
extension ThemeDataExtension on ThemeData {
  AppColorsTheme get appColors => extension<AppColorsTheme>()!;
}

extension AppColorsThemeExtensions on BuildContext {
  AppColorsTheme get appColors => Theme.of(this).extension<AppColorsTheme>()!;
}

class AppColorsTheme extends ThemeExtension<AppColorsTheme> {
  //亮色主题的自定义颜色值
  static const color666666 = Color(0xFF666666);
  static const _colorPrimary = Color(0xFF4161D0);
  static const _colorFF5E75 = Color(0xFFFF5E75);
  static const _colorFCFCFC = Color(0xFFFCFCFC);
  static const _colorD7DBE7 = Color(0xffD7DBE7);
  static const _colorBDBDBD = Color(0xFFBDBDBD);
  static const _colorF2F2F2 = Color(0xFFF2F2F2);
  static const _colorFE6C00 = Color(0xFFFE6C00);
  static const _colorD3D3D3 = Color(0xFFD3D3D3);
  static const _color333333 = Color(0xFF333333);
  static const _colorF3F3F3 = Color(0xFFF3F3F3);
  static const _color999999 = Color(0xFF999999);
  static const _colorF2F3F6 = Color(0xFFF2F3F6);
  static const _colorDCDCDC = Color(0xFFDCDCDC);
  static const _colorEFF3FF = Color(0xFFEFF3FF);
  static const _colorDFF0FF = Color(0xFFDFF0FF);
  static const _color1B61CA = Color(0X4D1B61CA);
  static const _color8B96BA = Color(0xFF8B96BA);
  static const _colorB4C5FF = Color(0xFFB4C5FF);
  static const _colorFE4066 = Color(0xFFFE4066);
  static const _colorE9E9E9 = Color(0xFFE9E9E9);
  static const _color99A8CA = Color(0xFF99A8CA);
  static const _color0DBE1E= Color(0xFF0DBE1E);

  //暗色主题的一些自定义颜色值
  static const _darkBlackBg = Color(0xFF0F0F0F);
  static const _darkBlackItem = Color(0xFF1D1D1E);
  static const _darkBlackItemLight = Color(0xFF3B3933);
  static const _darkBlackItemLightShadow = Color(0x4D3B3933);
  static const _darkBlackItemLightMost = Color(0xFF5C5850);
  static const _darkBlackItemDivider = Color(0xFF3B3B3F);

  // 页面中真正使用到的颜色名称
  final Color backgroundDefault; //页面背景颜色(偏白)
  final Color backgroundDark; //页面背景颜色(蓝灰蓝)
  final Color btnBgDefault; //按钮背景颜色
  final Color searchFiledBorder; //搜索框的边框颜色
  final Color authFiledHint; //输入框默认的提示文本颜色
  final Color authFiledText; //输入框默认的文本颜色
  final Color authFiledBG; //输入框默认的背景颜色
  final Color textPrimary; //主题色文本
  final Color textBlack; //黑色文本
  final Color textWhite; // 白色文本
  final Color textDarkGray; //深灰色 666 文本
  final Color orangeBG; //按钮的橙色背景
  final Color tabBgSelectedPrimary; //Tab的选中主题色背景
  final Color tabBgUnSelectedPrimary; //Tab的未选中主题色背景
  final Color tabTextSelectedPrimary; //Tab的未选中主题色文本
  final Color tabTextUnSelectedPrimary; //Tab的未选中主题色文本
  final Color imgGrayBg; //灰色的图片底色背景
  final Color textDarkGray999; //文本深灰色
  final Color whiteBG; //按钮的白色色背景
  final Color whiteSecondBG; //按钮的白色色背景
  final Color redDefault; //App标准红色
  final Color dividerDefault; //分割线默认颜色
  final Color lightPurpleBg; //淡紫色背景
  final Color lightBlueBg; //淡蓝色背景
  final Color tabTextSelectedDefault; //Tab文本,选中主题蓝,黑暗模式为白色
  final Color tabTextUnSelectedDefault; //Tab文本,未选中 亮色为黑色,黑暗模式为灰色
  final Color tabLightBlueShadow; //Tab的淡蓝色阴影
  final Color textLightPurple; //文本淡紫色
  final Color avatarBg; //头像框的淡蓝色
  final Color deleteRed; //删除的红色
  final Color grayBgE9; //e9灰色背景
  final Color disEnableGray; //禁用的灰色
  final Color authFiledHintDark; //文本Hint的深蓝色
  final Color greenBG; //按钮的绿色背景

  // 私有的构造函数
  const AppColorsTheme._internal({
    required this.backgroundDefault,
    required this.backgroundDark,
    required this.btnBgDefault,
    required this.searchFiledBorder,
    required this.authFiledHint,
    required this.authFiledText,
    required this.authFiledBG,
    required this.textPrimary,
    required this.textBlack,
    required this.textWhite,
    required this.textDarkGray,
    required this.orangeBG,
    required this.tabBgSelectedPrimary,
    required this.tabBgUnSelectedPrimary,
    required this.tabTextSelectedPrimary,
    required this.tabTextUnSelectedPrimary,
    required this.imgGrayBg,
    required this.textDarkGray999,
    required this.whiteBG,
    required this.whiteSecondBG,
    required this.redDefault,
    required this.dividerDefault,
    required this.lightPurpleBg,
    required this.lightBlueBg,
    required this.tabLightBlueShadow,
    required this.tabTextSelectedDefault,
    required this.tabTextUnSelectedDefault,
    required this.textLightPurple,
    required this.avatarBg,
    required this.deleteRed,
    required this.grayBgE9,
    required this.disEnableGray,
    required this.authFiledHintDark,
    required this.greenBG,
  });

  // 浅色主题工厂方法
  factory AppColorsTheme.light() {
    return const AppColorsTheme._internal(
      backgroundDefault: _colorFCFCFC,
      backgroundDark: _colorF2F3F6,
      btnBgDefault: _colorPrimary,
      searchFiledBorder: _colorD7DBE7,
      authFiledHint: _colorBDBDBD,
      authFiledText: Colors.black,
      authFiledBG: _colorF2F2F2,
      textPrimary: _colorPrimary,
      textBlack: Colors.black,
      textWhite: Colors.white,
      textDarkGray: color666666,
      orangeBG: _colorFE6C00,
      tabBgSelectedPrimary: _colorPrimary,
      tabBgUnSelectedPrimary: _colorD3D3D3,
      tabTextSelectedPrimary: Colors.white,
      tabTextUnSelectedPrimary: _color333333,
      imgGrayBg: _colorF3F3F3,
      textDarkGray999: _color999999,
      whiteBG: Colors.white,
      whiteSecondBG: Colors.white,
      redDefault: _colorFF5E75,
      dividerDefault: _colorDCDCDC,
      lightPurpleBg: _colorEFF3FF,
      lightBlueBg: _colorDFF0FF,
      tabLightBlueShadow: _color1B61CA,
      tabTextSelectedDefault: _colorPrimary,
      tabTextUnSelectedDefault: Colors.black,
      textLightPurple: _color8B96BA,
      avatarBg: _colorB4C5FF,
      deleteRed: _colorFE4066,
      grayBgE9: _colorE9E9E9,
      disEnableGray: _colorBDBDBD,
      authFiledHintDark: _color99A8CA,
      greenBG: _color0DBE1E,
    );
  }

  // 暗色主题工厂方法
  factory AppColorsTheme.dark() {
    return const AppColorsTheme._internal(
      backgroundDefault: _darkBlackBg,
      backgroundDark: _darkBlackBg,
      btnBgDefault: _darkBlackItem,
      searchFiledBorder: _darkBlackItem,
      authFiledHint: _colorD7DBE7,
      authFiledText: Colors.white,
      authFiledBG: _darkBlackItem,
      textPrimary: Colors.white,
      textBlack: Colors.white,
      textWhite: Colors.black,
      textDarkGray: Colors.white,
      orangeBG: _darkBlackItem,
      tabBgSelectedPrimary: Colors.white,
      tabBgUnSelectedPrimary: _darkBlackItem,
      tabTextSelectedPrimary: Colors.black,
      tabTextUnSelectedPrimary: Colors.white,
      imgGrayBg: _darkBlackItem,
      textDarkGray999: Colors.white,
      whiteBG: _darkBlackBg,
      whiteSecondBG: _darkBlackItemLight,
      redDefault: _darkBlackItem,
      dividerDefault: _darkBlackItemDivider,
      lightPurpleBg: _darkBlackItemLight,
      lightBlueBg: _darkBlackItem,
      tabLightBlueShadow: _darkBlackItemLightShadow,
      tabTextSelectedDefault: Colors.white,
      tabTextUnSelectedDefault: _darkBlackItemLightMost,
      textLightPurple: Colors.white,
      avatarBg: _darkBlackItemLight,
      deleteRed: Colors.white,
      grayBgE9: _darkBlackItemLightMost,
      disEnableGray: _darkBlackItemLight,
      authFiledHintDark: _color99A8CA,
      greenBG: _darkBlackItemLight,
    );
  }

  @override
  ThemeExtension<AppColorsTheme> copyWith({bool? lightMode}) {
    if (lightMode == null || lightMode == true) {
      return AppColorsTheme.light();
    }
    return AppColorsTheme.dark();
  }

  @override
  AppColorsTheme lerp(covariant ThemeExtension<AppColorsTheme>? other, double t) {
    if (other is! AppColorsTheme) return this;
    return AppColorsTheme._internal(
      backgroundDefault: Color.lerp(backgroundDefault, other.backgroundDefault, t)!,
      backgroundDark: Color.lerp(backgroundDark, other.backgroundDark, t)!,
      btnBgDefault: Color.lerp(btnBgDefault, other.btnBgDefault, t)!,
      searchFiledBorder: Color.lerp(searchFiledBorder, other.searchFiledBorder, t)!,
      authFiledHint: Color.lerp(authFiledHint, other.authFiledHint, t)!,
      authFiledText: Color.lerp(authFiledText, other.authFiledText, t)!,
      authFiledBG: Color.lerp(authFiledBG, other.authFiledBG, t)!,
      textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!,
      textBlack: Color.lerp(textBlack, other.textBlack, t)!,
      textWhite: Color.lerp(textWhite, other.textWhite, t)!,
      textDarkGray: Color.lerp(textDarkGray, other.textDarkGray, t)!,
      orangeBG: Color.lerp(orangeBG, other.orangeBG, t)!,
      tabBgSelectedPrimary: Color.lerp(tabBgSelectedPrimary, other.tabBgSelectedPrimary, t)!,
      tabBgUnSelectedPrimary: Color.lerp(tabBgUnSelectedPrimary, other.tabBgUnSelectedPrimary, t)!,
      tabTextSelectedPrimary: Color.lerp(tabTextSelectedPrimary, other.tabTextSelectedPrimary, t)!,
      tabTextUnSelectedPrimary: Color.lerp(tabTextUnSelectedPrimary, other.tabTextUnSelectedPrimary, t)!,
      imgGrayBg: Color.lerp(imgGrayBg, other.imgGrayBg, t)!,
      textDarkGray999: Color.lerp(textDarkGray999, other.textDarkGray999, t)!,
      whiteBG: Color.lerp(whiteBG, other.whiteBG, t)!,
      whiteSecondBG: Color.lerp(whiteSecondBG, other.whiteSecondBG, t)!,
      redDefault: Color.lerp(redDefault, other.redDefault, t)!,
      dividerDefault: Color.lerp(dividerDefault, other.dividerDefault, t)!,
      lightPurpleBg: Color.lerp(lightPurpleBg, other.lightPurpleBg, t)!,
      lightBlueBg: Color.lerp(lightBlueBg, other.lightBlueBg, t)!,
      tabTextSelectedDefault: Color.lerp(tabTextSelectedDefault, other.tabTextSelectedDefault, t)!,
      tabLightBlueShadow: Color.lerp(tabLightBlueShadow, other.tabLightBlueShadow, t)!,
      tabTextUnSelectedDefault: Color.lerp(tabTextUnSelectedDefault, other.tabTextUnSelectedDefault, t)!,
      textLightPurple: Color.lerp(textLightPurple, other.textLightPurple, t)!,
      avatarBg: Color.lerp(avatarBg, other.avatarBg, t)!,
      deleteRed: Color.lerp(deleteRed, other.deleteRed, t)!,
      grayBgE9: Color.lerp(grayBgE9, other.grayBgE9, t)!,
      disEnableGray: Color.lerp(disEnableGray, other.disEnableGray, t)!,
      authFiledHintDark: Color.lerp(authFiledHintDark, other.authFiledHintDark, t)!,
      greenBG: Color.lerp(greenBG, other.greenBG, t)!,
    );
  }
}

在入口我们配置的时候:

      //主题配置
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4161D0)),
            useMaterial3: false,
          ).copyWith(extensions: [
            AppColorsTheme.light(),
          ]),
          darkTheme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4161D0), brightness: Brightness.dark),
            useMaterial3: false,
          ).copyWith(extensions: [
            AppColorsTheme.dark(),
          ]),
          themeMode:  isDarkMode ? ThemeMode.dark : ThemeMode.light,

这里我们顺便使用扩展方法的方式方便使用自定义的 ThemeData 示例:

  Scaffold(
      appBar: MyAppBar.appBar(
        context,
        S.current.facility,
        backgroundColor: context.appColors.whiteBG,
      ),
      backgroundColor: context.appColors.backgroundDark,
  )

此时我们就可以很方便的实现我们自定义的样式颜色属性了,是不是很方便和灵活呢?

第三个疑问又来了,你这亮色模式和暗色模式固定不变的,我如何才能跟随系统和指定模式呢?

通常来说默认情况下我们是需要跟随系统的亮色暗色模式的,一般来说很多App都可以指定当前为亮色或暗色,设计如图:

image.png

我们又可以跟随系统又可以指定模式,此时我们就需要用到状态管理的工具,Getx Riverpod Bloc都可以,这里我以 Riverpod 为例:

在切换模式的页面我们把跟随系统,指定亮色,指定暗色这三种状态存起来,这里以 SP 的存储为例:

在 main.dart 的入口中我们取出对应的值:

    //根据主题配置对应的状态栏
    int? darkModel = SPUtil.getInt(AppConstant.storageDarkModel, defValue: 0);
    late SystemUiOverlayStyle systemUiOverlayStyle;

    if (DeviceUtils.isAndroid) {
      //根据SP存入的暗色模式,指定全局页面的状态栏,导航栏等设置
      switch (darkModel) {
        case 1:
          Log.d("main.dart - 指定亮色模式");
          systemUiOverlayStyle = ThemeConfig.systemUiOverlayStyleLightThemeBlack;
          break;
        case 2:
          Log.d("main.dart - 指定暗色模式");
          systemUiOverlayStyle = ThemeConfig.systemUiOverlayStyleDarkTheme;
          break;
        default:
          systemUiOverlayStyle = ThemeConfig.getSystemUiOverlayStyleByTheme(context);
          break;
      }
    }

在 MaterialApp 中配置:

    MaterialApp.router(
          title: 'Demo',
          debugShowCheckedModeBanner: true,

          //主题配置
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4161D0)),
            useMaterial3: false,
          ).copyWith(extensions: [
            AppColorsTheme.light(),
          ]),
          darkTheme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4161D0), brightness: Brightness.dark),
            useMaterial3: false,
          ).copyWith(extensions: [
            AppColorsTheme.dark(),
          ]),
          themeMode: themeMode == ThemeMode.system ? ThemeMode.system : themeMode,
    )

这里的 themeMode 是在跟随系统的时候监听到改变设置给 themeMode 变量:

final themeProvider = StateNotifierProvider<ThemeNotifier, ThemeMode>((ref) {
  return ThemeNotifier();
});

class ThemeNotifier extends StateNotifier<ThemeMode> {
  //这里应该根据用户的SP配置来设置不同的主题,先偷个懒这里我先写死跟随系统的主题
  ThemeNotifier() : super(ThemeMode.system);  //默认 system 主题

  //手动切换主题
  void toggleTheme() {
    if (state == ThemeMode.light) {
      state = ThemeMode.dark;
    } else if (state == ThemeMode.dark) {
      state = ThemeMode.light;
    } else {
      state = ThemeMode.light;
    }
  }
  //跟随系统主题
  void followSystemTheme() {
    state = ThemeMode.system;
  }
}

效果:

Screen_recording_20241127_160755.gif

那么到此我们的“正经”操作就完成了,基本上是可以满足我们的一般应用所需了。

但是我还有第四个疑问 ,那我如果想要玩骚操作我要自定义主题怎么办?产品和设计说要五颜六色的主题啊,让用户自定义主题颜色,给十几种颜色给用户选择,个性化的主题应用如何处理呢?

说实话这操作确实骚,但是也不是不能做,既然在上文中我们已经通过 ThemeExtension 实现自定义颜色值属性了,我们同时也能定义更多的样式,这里以橙色,蓝色,绿色三个颜色为示例。

我们在 AppColorsTheme 添加一些代码:

class AppColorsTheme extends ThemeExtension<AppColorsTheme> {
  // ... 其他已定义的颜色

  // 添加新的主题工厂方法
  static AppColorsTheme orange() {
    return const AppColorsTheme(
      // 这里填入橙色主题的颜色
      backgroundDefault: Color(0xFFFFF3E0), // 橙色背景
      // 其他颜色属性...
    );
  }

  static AppColorsTheme blue() {
    return const AppColorsTheme(
      // 这里填入蓝色主题的颜色
      backgroundDefault: Color(0xFFE3F2FD), // 蓝色背景
      // 其他颜色属性...
    );
  }

  static AppColorsTheme green() {
    return const AppColorsTheme(
      // 这里填入绿色主题的颜色
      backgroundDefault: Color(0xFFE8F5E9), // 绿色背景
      // 其他颜色属性...
    );
  }

  // ... 其他已有的代码
}

修改我们的 Provider 提供的样式:

class ThemeNotifier extends StateNotifier<ThemeMode> {
  ThemeNotifier() : super(ThemeMode.system); // 默认系统主题

  // 新增字段来存储用户选择的颜色主题
  String selectedColorTheme = 'default';

  void toggleColorTheme(String colorTheme) {
    selectedColorTheme = colorTheme;
    notifyListeners(); // 触发重建
  }

  void toggleTheme() {
    if (state == ThemeMode.light) {
      state = ThemeMode.dark;
    } else if (state == ThemeMode.dark) {
      state = ThemeMode.light;
    } else {
      state = ThemeMode.light; // 默认为亮色模式
    }
  }
}

在 MaterialApp 中配置主题

    // 在 MaterialApp 中设置主题
    MaterialApp(
      // ... 其他属性
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4161D0)),
        useMaterial3: false,
      ).copyWith(extensions: [
        selectedColorTheme == 'orange'
            ? AppColorsTheme.orange()
            : selectedColorTheme == 'blue'
                ? AppColorsTheme.blue()
                : selectedColorTheme == 'green'
                    ? AppColorsTheme.green()
                    : AppColorsTheme.light(),
      ]),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4161D0), brightness: Brightness.dark),
        useMaterial3: false,
      ).copyWith(extensions: [
        AppColorsTheme.dark(),
      ]),
      themeMode: themeMode == ThemeMode.system ? ThemeMode.system : themeMode,
    )

在切换的时候我们给不同的值就能切换到不同的颜色主题下了:

themeNotifier.toggleColorTheme('orange');

虽然是可以实现,但是我们要注意的是你的应用颜色主题越多那么对应的颜色就要写的越多,比如我们的应用,一个页面对应一个颜色又不统一,灰色都有五六种,头都大了,我还不敢大声说话,此时我们再对应多颜色主题的话那真是惨(还好我们不用做)

1.2 Getx的用法

至于Getx的话我们可以用类似上面的方法也可以使用判断法都可以,由于 Getx 已经很简单了我也就就简单的过一下。


    int? darkModel = SpUtil.getInt(AppConstant.storagedarkmodel, defValue: 0);

    GetMaterialApp(
          //顶部是否展示Debug图标
          debugShowCheckedModeBanner: true,
          //网页Title显示
          title: 'Demo',
          //样式相关
          theme: ThemeConfig.lightTheme,
          darkTheme: ThemeConfig.darkTheme,
          themeMode: darkModel == 1
              ? ThemeMode.light
              : darkModel == 2
                  ? ThemeMode.dark
                  : ThemeMode.system,
    )

我们可以不定义具体的自定义样式,直接切换亮色和暗色的主题。

    Scaffold(
        backgroundColor: DarkThemeUtil.multiColors(ColorConstants.pageBg, darkColor: ColorConstants.darkBlackBg),
    )

我们可以直接用工具类的方式切换,直接指定亮色和暗色模式的颜色值。

  static Color? multiColors(Color? lightColor, {Color? darkColor}) {
    Color? color;
    if (Get.isDarkMode) {
      color = darkColor ?? ColorConstants.darkScaffoldBackgroundColor;
    } else {
      color = lightColor;
    }
    return color;
  }

我们当然也可以使用原生Flutter的那种方案自定义 ThemeData 或者 ThemeExtension 的方式实现,都蛮方便的。

二、中英文等国际化的配置与使用

在现代应用开发中,支持多语言的国际化(i18n)已经成为一项基本需求。这不仅可以提升用户体验,还能让产品更具全球化的竞争力。接下来,我们将探讨如何在 Flutter 和 GetX 中实现国际化。

2.1 Flutter的用法

在 Flutter 中实现国际化相对简单,主要依赖于 flutter_localizations 包和 intl 的插件,以下是配置步骤:

依赖项:在 pubspec.yaml 文件中添加 flutter_localizations

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter

如果是 AS 编辑器,那么是默认自带 Flutter Intl 插件的,如果你没有可以自行安装

image.png

然后我们初始化项目:

image.png

就会看到已经生成了对应的文件和文件夹:

image.png

以后每次保存内容都会自动的生成对应的国际化资源。

随后我们在 MaterialApp 中配置我们的国际化资源与配置:

    MaterialApp.router(
          title: 'Demo',
          debugShowCheckedModeBanner: true,

          //主题配置
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4161D0)),
            useMaterial3: false,
          ).copyWith(extensions: [
            AppColorsTheme.light(),
          ]),
          darkTheme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4161D0), brightness: Brightness.dark),
            useMaterial3: false,
          ).copyWith(extensions: [
            AppColorsTheme.dark(),
          ]),
          themeMode: themeMode == ThemeMode.system ? ThemeMode.system : themeMode,

          //国际化配置
          localizationsDelegates: const [
            S.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
          ],
          //国际化英语为首选项
          supportedLocales: [const Locale('en', ''), ...S.delegate.supportedLocales],
          localeResolutionCallback: (locale, supportLocales) {
            if (locale?.languageCode == 'zh') {
              if (locale?.scriptCode == 'Hant') {
                return const Locale('zh', 'HK'); //繁体
              } else {
                return const Locale('zh', 'CN'); //简体
              }
            }
            return null;
          },
    )

当然如果你要支持iOS设备需要在xcode中添加Localizations

image.png

定义:

 "not_approved": "Not Approved",

使用:

S.of(context).not_approved
S.current.not_approved

占位符:

  "approved_on_sometime": "Approved on {time}",

使用:

S.of(context).send_on_sometime("04 Feb 2024 at 04:00 PM")
S.current.send_on_sometime("04 Feb 2024 at 04:00 PM")

还可以多个占位符:

  "approved_by": "Approved by {name} on {time}",

使用:

S.of(context).approved_by("Wu Lei" , s"04 Feb 2024 at 04:00 PM")
S.current.approved_by("Wu Lei" , "04 Feb 2024 at 04:00 PM")

效果:

Screen_recording_20241127_170101.gif

看起来都卡卡的,原谅我的低端手机。

2.2 Getx的用法

GetX 提供了更简单、直观的方式来实现国际化,特别适合快速开发。以下是实现步骤:

我们先实现对应的国际化文档:

image.png

其中格式如下:

const Map<String, String> zh_CN = {
  'Name': '姓名',

};


const Map<String, String> en_US = {
  'Name': 'Name',

};

translation_service.dart 中我们配置对应的Local 对应哪一个格式文件:

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'en_US.dart';
import 'zh_CN.dart';

class TranslationService extends Translations {
  static Locale? get locale => Get.deviceLocale;
  static const fallbackLocale = Locale('zh', 'CN');

  @override
  Map<String, Map<String, String>> get keys => {
        'zh_CN': zh_CN,
        'en_US': en_US,
      };
}

然后直接配置到 GetMaterialApp 中:

GetMaterialApp(
    //本地化相关
    locale: TranslationService.locale,
    fallbackLocale: TranslationService.fallbackLocale,
        localizationsDelegates: const [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        ],
    supportedLocales: const [
        Locale('zh', 'CH'),
        Locale('en', 'US'),
        ],
    translations: TranslationService(),
)

使用起来就是大家都熟悉的 .tr 的方式了:

 _buildItem('Name'.tr, '', () {
        Get.toNamed(RouterPath.Name);
    }),

效果:

Screen_recording_20241127_170550.gif

GetX 和 intl 库在国际化的实现原理上有一些根本性的区别,在 GetX 中,.tr 方法用于从翻译文件中获取对应的翻译字符串。当调用 .tr 方法时,GetX 会查找当前的语言环境并返回相应的翻译文本,通过响应式的概念通知指定的UI更新。

intl 库的国际化功能是通过使用 .arb 文件来管理翻译字符串,翻译内容在构建时已经确定,翻译字符串在运行时是以静态方式存在的,不会直接在 UI 组件中进行动态查找,相对性能高一些。

但是 Getx 的优势是不需要额外的代码生成步骤,通过 Get.updateLocale() 方法可以轻松实现语言切换。GetX 提供了方便的 API,开发者可以在应用中快速响应用户的语言选择,并即时更新 UI。GetX 还结合了状态管理、路由管理等功能,提供了一个更全面的框架,使得国际化在整个应用中的使用更加一致和高效。

而intl 库的优势是...等等跑题了。我只管分享如何他们使用的怎么写着写着搞对比起来了。不说了,都好用都好用。

后记

为什么我要把原生 Flutter 框架的多主题样式与国际化配置与 Getx 分开区别出来,大家应该也已经能看出来了,他们有相似也有不同。

使用 Getx 框架的开发者你只需要面向 Getx 开发即可,并不需要了解 Flutter 的原生配置就可以很方便的开发出对应的效果,其实也挺好开发也快速,而使用其他状态管理框架如 Riverpod Bloc 之类的框架我们就需要实现 Flutter 自己的框架。

就我本人来说,经历的 Flutter 项目也不少了,这两种方式都用过,2023年一直是基于 Getx 开发的,2024年之后是基于 Riverpod 开发。就我个人的体感而言不管是刷新速度性能效率来说 Flutter 原生的效果会比 Getx 框架的效果要好上一些的,但是Getx开发速度快!

当然也并不是说使用了 Getx 就一定要使用 Getx 自带的方案,完全也可以混用使用 Flutter 的效果。不过嘛这...就感觉怪怪的,这不是失去了 Getx 的灵魂了吗。

那么今天的记录/分享就到这里啦,当然如果你有其他的更多的更好的实现方式推荐,也希望大家能评论区交流一起学习进步。

如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,理解不正确的地方,同学们都可以指出修正。

本文的代码比较简单,相对比较基础,并且全部的代码也已经在文中展出,这里就不放出项目链接了。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

Ok,这一期就此完结。