flutter 权限管理与主题切换

623 阅读9分钟

本文主要介绍Flutter两个功能的实现:

1.使用函数包装器对角色等级权限进行最小单位处理,包括用户信息的定义、权限检查和基于角色的UI定制。

2.使用ThemeData自定义主题与主题切换(包括暗黑模式)来实现换肤功能,让APP更加美观和多样性 。

一、 实现用户角色和权限管理实践

用户信息以及权限类定义

案例定义了三种角色 usermanageradmin,他们的优先级分别对应1,2,3,可以根据需求自行添加,在实际开发中与服务器定义相同,并且从服务器获取相关信息。

class UserData {
  final String username;
  final String email;
  final UserRole role;

  const UserData({
    required this.username,
    required this.email,
    required this.role,
  });

  factory UserData.fromJson(Map<String, dynamic> json) {
    return UserData(
      username: json['username']!,
      email: json['email'],
      role: UserRole.fromJson(json['role']["name"]),
    );
  }
}

enum UserRole {
  admin('admin', 3),
  manager('manager', 2),
  user('user', 1);
    
  const UserRole(this.name, this.level);
  final String name;
  final int level;

  @override
  String toString() => name;
    
  factory UserRole.fromJson(String? role) {
    switch (role) {
      case "admin":
        return UserRole.admin;
      case "manager":
        return UserRole.manager;
      default:
        return UserRole.user;
    }
  }
}

UserData类添加isAllowed方法,该方法通过检查特定的允许角色列表允许的最低角色级别来判断用户是否被允许执行某个操作,具体逻辑如下:

  1. 优先检查特定的允许角色列表。
  2. 如果没有提供特定的允许角色列表,则检查允许的最低角色级别。
  3. 如果既没有提供特定的允许角色列表,也没有提供允许的最低角色级别,则默认用户不被允许。

代码如下:

bool isAllowed({
    List<UserRole>? specificAllowedRoles,
    UserRole? allowedRoleLevel,
  }) {
    if (specificAllowedRoles != null) {
      return specificAllowedRoles.contains(role);
    }

    if (allowedRoleLevel == null) return false;
    return role.level >= allowedRoleLevel.level;
  }

specificAllowedRoles:一个可选的 List<UserRole> 类型的参数,表示特定的允许角色列表。如果提供了这个参数,函数将检查用户的角色是否在这个列表中。

allowedRoleLevel:一个可选的 UserRole 类型的参数,表示允许的最低角色级别。如果提供了这个参数,函数将检查用户的角色级别是否大于或等于这个最低级别,从而返回结果。

用户信息存储以及权限管理

案例定义了一个Core类来存储当前用户的登录信息,并且定义抽象类来共享行为和属性,实际开发中可以使用状态管理来进行行为和属性的共享,比如:Bloc、 provider、getx等

class Core {
 static UserData? _user;

 UserData? get user => _user;

 Future<void> setUserData({UserData? userData}) async {
   if(userData != null){
     _user =  userData;
   }else{
     // 从共享偏好设置中或其他存储方式中加载用户数据
   }
 }
}

通过抽象类来定义一些共享的行为或属性

abstract class UserRoleStateful extends StatefulWidget {
  UserRoleStateful({Key? key}) : super(key: key);
  final Core core = Core();
}

abstract class UserRoleState<B extends UserRoleStateful> extends State<B> {
  final Core core = Core();
}

abstract class UserRoleStateless extends StatelessWidget {
  final Core core = Core();
}

HomePage页面可以随意进行权限设定,拿到权限后就可以进行区分,通过Core类可以在任何地方进行权限设定。

class HomePage extends UserRoleStateful {
  HomePage({super.key});
  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends UserRoleState<HomePage> {
    
 @override
void initState() {
  super.initState();
    
/// admin权限    
bool isAllowed =
    core.user!.isAllowed(allowedRoleLevel: UserRole.fromJson("admin"));
    
/// manager和user权限
bool isAllowed = core.user!.isAllowed(specificAllowedRoles: [
  UserRole.fromJson("manager"),
  UserRole.fromJson("user")
]);
    
 @override
 Widget build(BuildContext context) {
  ....
});
}

使用函数包装器对不同权限进行不同处理。

允许角色等级的函数包装器,将一个可用的小部件作为子项并决定是否显示它, 比如级别1,2,3,如果是 2以下的级别将返回accepted否则返回rejected

T callWithRole<T>({
 required T accepted,
 required T rejected,
 required UserRole roleLevel,
}) {
 Core core = Core();
 // 允许角色等级
 bool isAllowed = core.user!.isAllowed(allowedRoleLevel: roleLevel);
 if (isAllowed) {
   return accepted;
 }
 return rejected;
}

callWithRole 函数是一个泛型函数,用于根据用户的角色级别来决定返回值,这个返回值可以是一个颜色,一个方法或者是一个任意widget,在APP任何地方都可以使用它进行权限管理从而返回任意所需要的值。

使用代码如下:

 // 不同权限实现不方法
TextButton(
  onPressed: callWithRole<void Function()>(
    accepted: () {print("admin以上权限实现方法")},
    rejected: () {print("admin以下权限实现方法")},
    roleLevel: UserRole.fromJson("admin"),
  ),
  child:
      Text('进入到不同页面', style: TextStyle(color: mfTheme.textColor)),
)
 // 不同权限返回不同 Widget  
callWithRole<Widget>(
    accepted: Container(
      width: context.width - 40,
      height: 160,
      decoration: mfTheme.cardBoxDecoration,
      child: Text("manager 与admin的 管理区",
          style: TextStyle(color: mfTheme.textColor)),
    ),
    rejected: Container(
      width: context.width - 40,
      height: 100,
      decoration: mfTheme.cardBoxDecoration,
      child: Text("user 工作区",
          style: TextStyle(color: mfTheme.textColor)),
    ),
    roleLevel: UserRole.fromJson("manager")),

callWithRole 函数包装器只是一种方法,还可以随意搭配权限管理与返回的内容 比如:

T? callWithLevel<T>({
  required T leve1,
  required T leve2,
  required T leve3,
}) {
  Core core = Core();
  switch(core.user!.role.level){
    case 1:
      return leve1;
    case 2:
      return leve2;
    case 3:
      return leve3;
  }
  return null;
}

callWithLevel用于根据用户的角色级别返回相应的值。它接受三个必需参数:leve1leve2 和 leve3。函数的作用是根据用户的角色级别来返回相应的值。

实现:

 Text(
  core.user?.username ?? "",
  style: callWithLevel<TextStyle>(
    leve1: const TextStyle(fontSize: 20, color: Colors.black),
    leve2: const TextStyle(fontSize: 22, color: Colors.blue),
    leve3: const TextStyle(fontSize: 24, color: Colors.red),
  ),
)

注意:案例可以通过状态管理工具,如Bloc、Provider或GetX,以优化用户信息和权限管理,本地持久化主题功能存储用户登录状态也需要使用shared_preferences或其他本地存储库来进行操作,案例未实现相关代码。

二、主题切换

效果展示

此案例自定义了三种亮色主题两种暗黑主题,并且可以一键更换当前主题样式和暗黑模式(其中包含切换字体,播放的动画主题颜色等)。

1111 -small-original.gif

依赖与素材

案例中使用了 getXrive插件 和 fonts 文件。

  • getX: 进行状态管理从而控制主题更换功能。
  • rive动画 与 fonts 字体文件主要体现该案例实现的不仅仅是换肤,更多的元素都可以随之变化。
pubspec.yaml 文件配置

    ...
    get: ^4.6.6
    rive: ^0.13.12
    ...
    fonts:
      - family: ModakRegular
        fonts:
          - asset: assets/Modak-Regular.ttf
      - family: LINESeedSans_A_XBd
        fonts:
          - asset: assets/LINESeedSans_XBd.ttf
    assets:
        - assets/dragon_solos.riv
        - assets/balloon_popper.riv
        - assets/dark_1.riv
        - assets/dark_2.riv

设置默认主题

设置 MaterialApp 中的themedarkThemethemeMode三个属性可实现主题切换与自定义主题设置。

  • theme: 亮色主题模式。
  • darkTheme: 暗黑主题模式。
  • themeMode :决定当前启用亮色模式或暗黑模式还是由系统决定。
MaterialApp(
     title: 'Flutter Demo',
     theme: ThemeData.light(),
     darkTheme: ThemeData.dark(),
     themeMode: ThemeMode.dark,
     ...
 ));

上面代码设置亮色主题暗黑主题,由于设置了themeMode属性为 ThemeMode.dark,所以APP会默认加载暗黑模式主题

自定义主题

  1. ThemeData自带的属性,直接进行修改从而改变主题。
/// 修改亮色主题
ThemeData.light().copyWith(
  primaryColor: const Color(0xFFF3ECFE),
  appBarTheme: const AppBarTheme(
    backgroundColor: Color(0xFFF3ECFE),
  ), 
  .... \\\ 其他系统属性
  );
  
/// 修改暗黑主题
ThemeData.dark().copyWith(
  primaryColor: const Color(0xFF707070),
  appBarTheme: const AppBarTheme(
    backgroundColor: Color.fromRGBO(0, 0, 0, 1.0),
  )
  .... \\\ 其他系统属性
  );

示例ThemeData自带属性我只重新定义了两个,其他ThemeData自带属性如下:

-   `brightness` - 应用程序的整体明暗模式(亮色或暗色)。
-   `primaryColor` - 应用程序的主要颜色,影响AppBar、Button、Switch等部件。
-   `primaryColorBrightness` - primaryColor的明暗模式。
-   `primaryColorLight` - primaryColor的较轻色调,影响FlatButton、OutlinedButton等部件。
-   `primaryColorDark` - primaryColor的较暗色调,影响RaisedButton、Switch等部件。
-   `accentColor` - 应用程序的强调颜色,影响进度条、选择器等部件。
-   `accentColorBrightness` - accentColor的明暗模式。
-   `canvasColor` - Material的默认颜色,影响Card、Dialog等部件的背景颜色。
-   `scaffoldBackgroundColor` - Scaffold的背景颜色。
-   `bottomAppBarColor` - 底部AppBar的颜色。
-   `cardColor` - Card的背景颜色。
-   `dividerColor` - Divider的颜色。
-   `focusColor` - Focus的颜色。
-   `hoverColor` - 鼠标悬停的颜色。
-   `splashColor` - 部件被轻触时的颜色。
-   `selectedRowColor` - 选择行时的颜色。
-   `unselectedWidgetColor` - 未选中的Checkbox、Radio、Switch等部件的颜色。
-   `disabledColor` - 部件不可用时的颜色。
.... 其他属性

  1. ThemeData提供属性无法满足时,可以通过 extensions<ThemeExtension> 扩展属性,此案例中定义扩展类MFTheme,增加字体、加载的动画、自定义BoxDecoration 的相关代码:
static final lightTheme1 = ThemeData.light().copyWith(
      appBarTheme: const AppBarTheme(
        backgroundColor: Color.fromRGBO(251, 235, 216, 1.0),
      ),
  scaffoldBackgroundColor: const Color.fromRGBO(251, 235, 216, 1.0),
  .... \\\ 其他系统属性
  extensions: <ThemeExtension<dynamic>>[
    MFTheme(
      textColor: const Color(0xFFE6E9ED),
      fontFamily: "yahei",
      firstRiv:"assets/monster_lottietorive.riv",
      cardBoxDecoration: BoxDecoration(
        borderRadius: BorderRadius.circular(12.0),
        gradient: const LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            Color.fromRGBO(99, 171, 125, 1.0),
            Color.fromRGBO(153, 240, 214, 1.0),
          ],
        ),
      ),
     .... \\\ 其他自定义属性
    )
  ],
);

下面是我对MFTheme的定义,这个定义可以随意添加属性:

class MFTheme extends ThemeExtension<MFTheme> {
  final String themeKey;
  final Color mainColor;
  final BoxDecoration cardBoxDecoration;
  final Color textColor;
  final Color containerColor;
  final String firstImage;
  final String? fontFamily;
  final String? firstRiv;
  final BoxDecoration? bcDecoration;



  MFTheme({
    required this.themeKey,
    required this.mainColor,
    required this.cardBoxDecoration,
    required this.textColor,
    required this.containerColor,
    required this.firstImage,
    this.fontFamily,
    this.firstRiv,
    this.bcDecoration,
  });

  @override
  ThemeExtension<MFTheme> copyWith({
    String? themeKey,
    Color? mainColor,
    BoxDecoration? cardBoxDecoration,
    Color? textColor,
    Color? containerColor,
    String? firstImage,
    String? fontFamily,
    String? firstRiv,
    BoxDecoration?  bcDecoration,
  }) {
    return MFTheme(
      themeKey: themeKey ?? this.themeKey,
      mainColor: mainColor ?? this.mainColor,
      cardBoxDecoration: cardBoxDecoration ?? this.cardBoxDecoration,
      bcDecoration: bcDecoration ?? this.bcDecoration,
      textColor: textColor ?? this.textColor,
      containerColor: containerColor ?? this.containerColor,
      firstImage: firstImage ?? this.firstImage,
      fontFamily: fontFamily ?? this.fontFamily,
      firstRiv : firstRiv ?? this.firstRiv,
    );
  }

  @override
  ThemeExtension<MFTheme> lerp(
      covariant ThemeExtension<MFTheme>? other, double t) {
    if (other is! MFTheme) {
      return this;
    }

    return MFTheme(
      themeKey: themeKey,
      mainColor: Color.lerp(mainColor, other.mainColor, t)!,
      cardBoxDecoration: cardBoxDecoration,
      bcDecoration:bcDecoration,
      textColor: Color.lerp(textColor, other.textColor, t)!,
      containerColor: Color.lerp(containerColor, other.containerColor, t)!,
      firstImage: firstImage,
      fontFamily: fontFamily,
      firstRiv:firstRiv,
    );
  }
}

3.设置多个主题风格,每个主题的详细设置根据UI决定或者直接使用ThemeData自带的两种主题(明亮主题与暗黑主题)。

static final lightTheme1 = ThemeData.light().copyWith(
  ...,
  extensions: <ThemeExtension<dynamic>>[...]
  ]);

static final darkTheme1 = ThemeData.dark().copyWith(
  ...,
  extensions: <ThemeExtension<dynamic>>[...]
  ]);

... 其他风格
    

控制主题变化

  1. 通过状态管理控制全局主题变化, 状态管理插件有很多例如: Blocproviderget等,案例中使用的是GetX
class ThemeService extends GetxService {
    
  // themeMode:定义当前使用的主题模式(亮色、暗色或跟随系统)变更
  final themeMode = ThemeMode.light.obs;
  void changeThemeMode(ThemeMode style) {
    themeMode.value = style;
  }

  // theme:定义亮色主题变更
  final defaultLightTheme = lightTheme1.obs;
  void changeLightTheme(ThemeData themeData) {
    defaultLightTheme.value = themeData;
  }

  // darkTheme:定义暗色主题变更
  final defaultDarkTheme = darkTheme1.obs;
  void changeDarkTheme(ThemeData themeData) {
    defaultDarkTheme.value = themeData;
  }
  
  .... \\\其他代码

2. 绑定MyApp中的themedarkThemethemeMode 三个属性实现主题切换与自定义主题。

class MyApp extends StatelessWidget {
  MyApp({super.key});

  final themeService = Get.find<ThemeService>();

  @override
  Widget build(BuildContext context) {
    return Obx(() => MaterialApp(
          title: 'Flutter Demo',
          navigatorKey: navigatorKey,
          theme: themeService.defaultLightTheme.value,
          darkTheme: themeService.defaultDarkTheme.value,
          themeMode: themeService.themeMode.value,
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        ));
  }
}

3. 最后在合适的地方添加对应的切换主题控制代码:

// 切换亮色主题
GestureDetector(
  onTap: () {
    Get.find<ThemeService>()
        .changeLightTheme(item["style"]);
  }
...

// 切换暗黑主题
GestureDetector(
  onTap: () {
    Get.find<ThemeService>()
        .changeDarkTheme(item["style"]);
  }
...

// 切换当前主题模式
GestureDetector(
  onTap: () {
    Get.find<ThemeService>()
        .changeThemeMode(item["mde"]);
  }

注意:关键代码块都在上面,此示例并不是全部代码,本地持久化主题功能需要使用shared_preferences 或其他本地存储库来保存用户选择的主题,本示例相关代码也没有实现。

总结

本文介绍了如何在 Flutter 中实现主题切换功能,包括自定义主题、状态管理、这些内容能帮助在项目中实现美观且多样化的主题切换效果。以及在应用中实现用户角色和权限管理,包括用户信息的定义、权限检查和基于角色的UI定制,通过这些方法,可以实现应用的安全性和功能性。