When I started #30DaysOfFlutter on my Twitter (sorry for the shameless plugin xD), I never thought I would go more than 4 days. But it's Day 8 today and I feel the progress is really good.
While I regularly share my Flutter learnings on Twitter, today's topic is too good not to have its own dedicated post. So I am actually building a side project on Flutter that has different user roles - Admin, Manager and User. These roles demand distinct UIs, functionalities, and data access! So I decided not to drown in if-else statements within every widget and instead craft a robust system to seamlessly control access. And this is what we are gonna learn today - How to implement Role Based Access Control (RBAC) in Flutter.
Starting Point
We're starting simple! To explore role management, we'll build a basic app with just two files, app.dart
and main.dart
. Think of it as a blank canvas. Forget fancy UIs for now, we'll add complexity as we go. This way, we can focus on the core concepts of access control, step-by-step.
app.dart
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'RBAC Demo',
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('User model app'),
),
body: Container(),
);
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:userrole/app.dart';
void main() {
runApp(const MyApp());
}
Now letβs introduce some functionality. First, we begin by introducing a simple button that basically creates a snack bar with a message:
import 'package:flutter/material.dart';
class MessageButton extends StatefulWidget {
const MessageButton({Key? key}) : super(key: key);
@override
State<MessageButton> createState() => _MessageButtonState();
}
class _MessageButtonState extends State<MessageButton> {
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: _showMessage,
child: const Text('Show me'),
);
}
_showMessage() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('I am role based'),
),
);
}
}
The best practice when handling roles is not managing them from UI, but every feature call should be checked for roles and permission on the server side. UI role management is great for user experience.
To mimic real-world authentication, imagine we get user data (including roles) after a successful login. While we could use secure JWT tokens for this, let's simplify things and define the plain JSON format we'll use for storing this data in local storage.
{
"username": "Sparsh",
"email": "sparsh@gmail.com",
"role": {
"name": "admin",
"level": 3
}
}
We need to create a proper user data container in model.dart
.
import 'package:flutter/foundation.dart';
@immutable
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']),
);
}
}
@immutable
class UserRole {
final String name;
final int level;
const UserRole({
required this.name,
required this.level,
});
factory UserRole.fromJson(Map<String, dynamic> json) {
return UserRole(
name: json['name'],
level: json['level'] as int,
);
}
}
To make accessing user data a breeze, we'll have a dedicated file where it lives, readily available from anywhere in the app.
core.dart
import 'dart:convert' show jsonDecode;
import 'package:userrole/mdoel.dart';
import 'package:flutter/services.dart' show rootBundle;
class Core {
static UserData? _user;
UserData? get user => _user;
Future<void> setUserData() async {
// loads user data from shared preferences and sets it to
// [user]
}
}
main.dart
will be changed into the following:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Core core = Core();
await core.setUserData();
runApp(const MyApp());
}
To avoid hardcoded role checks in every widget, we'll define a stateful WidgetWithRole
class in role_handlers.dart
. This class interacts with the Core class for data and centralizes role verification, streamlining our code.
import 'package:flutter/material.dart';
import 'package:userrole/core.dart';
class WidgetWithRole extends StatefulWidget {
const WidgetWithRole({Key? key, required this.child}) : super(key: key);
final Widget child;
@override
State<WidgetWithRole> createState() => _WidgetWithRoleState();
}
class _WidgetWithRoleState extends State<WidgetWithRole> {
late Core core;
@override
void initState() {
core = Core();
super.initState();
}
bool get isAdmin => core.user?.role.name == "admin";
@override
Widget build(BuildContext context) {
if (isAdmin) {
return widget.child;
}
return Container();
}
}
Now, app.dart
can leverage this functionality by wrapping MessageButton with WidgetWithRole
, seamlessly implementing access control.
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('User model app'),
),
body: const Center(
child: WidgetWithRole(
child: MessageButton(),
),
),
);
}
We've got a fantastic role-checking wrapper, but the "admin" hardcode feels limiting. Let's make it even better with an allowedRole
parameter! Pass in any role as a string, but wouldn't enums be cleaner? We'll dedicate an enums.dart
file for them, convert UserRole
to an enum, and update WidgetWithRole
calls accordingly.
model.dart
import 'package:flutter/foundation.dart';
import 'package:userrole/enums.dart';
@immutable
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.admin,
);
}
}
enums.dart
enum UserRole {
admin('admin', 3);
const UserRole(this.name, this.level);
final String name;
final int level;
@override
String toString() => name;
}
Now in MyHomePage
widget we will pass allowedRole
argument to WidgetWIthRole
.
app.dart
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('User model app'),
),
body: const Center(
child: WidgetWithRole(
allowedRole: UserRole.admin,
child: MessageButton(),
),
),
);
}
Now we will modify the check in role_handlers.dart
file:
role_handlers.dart
class WidgetWithRole extends StatefulWidget {
const WidgetWithRole({
Key? key,
required this.child,
required this.allowedRole,
}) : super(key: key);
final Widget child;
final UserRole allowedRole;
@override
State<WidgetWithRole> createState() => _WidgetWithRoleState();
}
class _WidgetWithRoleState extends State<WidgetWithRole> {
late Core core;
@override
void initState() {
core = Core();
super.initState();
}
bool get isAllowed => core.user?.role == widget.allowedRole;
@override
Widget build(BuildContext context) {
if (isAllowed) {
return widget.child;
}
return Container();
}
}
Now, we have refactored and refined the version of our role handler, and I think it is time to introduce multiple roles and everything that comes to it.
First of all, letβs update model.dart
and enum.dart
to support multiple roles and their deserialization,
enum.dart
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;
}
}
}
role_handlers.dart
final List<UserRole> allowedRoles;
// ...
bool get isAllowed => widget.allowedRoles.contains(core.user?.role);
Now in app.dart
, we will modify the code to give access to multiple roles :
child: WidgetWithRole(
allowedRoles: [
UserRole.admin,
UserRole.manager,
UserRole.user,
],
child: MessageButton(),
),
Conclusion
This way you can introduce RBAC in your flutter app. There are some more approaches too - like adding a hierarchy in roles and only specifying lowest role so that all the roles above it will have the access to the widget. You can use your own approach whichever is best suited for your needs and complexity.
Top comments (0)