Flutter Tutorial: Login, Role, and Permissions

by Didin J. on Nov 11, 2021 Flutter Tutorial: Login, Role, and Permissions

The comprehensive step by step Flutter tutorial on building Android and iOS apps that can Login with role-based and permissions from REST API

In this Flutter tutorial, we will show you how to build Android and iOS apps that can Login with role-based and permissions by accessing REST API. So, there will be a different screen or view that is accessible for each different role. Each role has a different permissions array. Each permission is represented by a screen, function, or REST API endpoint access.

This tutorial is divided into several steps:

The REST API will consume our existing Node-Express-MySQL auth-role-permissions from the previous tutorial. So, we will focus on Flutter Android and iOS application building.

The flow of this Flutter app is very simple. The app will start with a Login screen. If the user has not registered, there's a register screen to register as a new user. A new user will have an Admin role automatically (manage by REST API backend). After successful login, the user will show a Home Screen with a drawer menu that contains modules for Users, Roles, and Products management (CRUD). The access to each menu and module item depends on permissions owned by the logged-in user. Otherwise, it will throw a 403 (Forbidden) message.

The following tools, frameworks, and libraries are required for this tutorial:

  1. Flutter SDK
  2. Node-Express Auth-Role-Permissions REST API
  3. Android SDK
  4. XCode
  5. Terminal (on Mac/Linux) or CMD (on Windows)
  6. IDE (Android Studio/IntelliJ/Visual Studio Code)

You can watch the video tutorial on our YouTube channel here.

Let's get started on the main steps!


Step #1: Preparation

Install Flutter SDK

To install flutter SDK, first, we have to download flutter_macos_2.5.3-stable.zip then extract the file to your desired location.

mkdir ~/development
cd ~/development
unzip ~/Downloads/flutter_macos_2.5.3-stable.zip

Next, add flutter tools to the path.

export PATH="$PATH:~/development/flutter/bin"

Next, to make iOS and Android binaries that can be downloaded ahead of time, type this Flutter command.

flutter precache

Check the Required Dependencies

To check the environment and displays a report to the terminal window to find dependencies that are required to install, type this command.

flutter doctor

We have this summary in the Terminal.

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.2.3, on macOS 11.6 20G165 darwin-x64, locale
    en-ID)
[!] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    ! Some Android licenses not accepted.  To resolve this, run: flutter doctor
      --android-licenses
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio (version 4.1)
[✓] IntelliJ IDEA Community Edition (version 2021.2.2)
[✓] VS Code (version 1.61.2)
[✓] Connected device (1 available)

! Doctor found issues in 2 categories.

To fixes, the issues like this, just connect the Android device and update the Android license by typing this command.

flutter doctor --android-licenses

Type `y` for every question displayed in the terminal. Next, check the connected Android device by typing this command.

adb devices
List of devices attached
FUZDON75YLOVVC5S    device

Setup IDE

We need to set up the IDE to make it work with the Flutter app easily and compatibly. Start the Android Studio (we will use this IDE) then open Android Studio Menu -> Preference.

Flutter Tutorial: Login, Role, and Permissions - 1

Choose plugins in the left pane.

Flutter Tutorial: Login, Role, and Permissions - 2

Type Flutter in the plugins marketplace then presses Enter. Next, click the Install button on the Flutter. If there's a prompt to install Dart, click Yes. Restart the IDE when prompt to ask for a restart.

Now, the Flutter is ready to develop Android and iOS mobile apps.


Step #2: Create a Flutter Application

After the IDE setup is complete, it will back to the Android starting dialog.

Flutter Tutorial: Login, Role, and Permissions - 4

Choose `Create New Flutter Project` then choose `Flutter Application`.

Flutter Tutorial: Login, Role, and Permissions - 5

Click the Next Button then fill in the required fields and choose the previously installed Flutter SDK path.

Flutter Tutorial: Login, Role, and Permissions - 6

Click the next button then fill the package name with your own domain and leave the "Include Kotlin support for Android code" and "Include Swift support for iOS code" blank.

Flutter Tutorial: Login, Role, and Permissions - 7

Click the Finish button and the Flutter application creation in progress. Next, run the Flutter application for the first time. In the Android Studio toolbar, choose the device and main.dart then click the play button.

Flutter Tutorial: Login, Role, and Permissions - 8

The initial Flutter apps look like this on the Android device.

Flutter Tutorial: Login, Role, and Permissions - 9


Step #3: Create Flutter Models

We need to add the models that represent all responses from the REST API. There will be User, Role, Permissions, and Product models. So, create a new package inside the lib folder by right-clicking the lib folder -> New -> Package then giving it the name `models`.

Next, create these files inside that model's package.

lib/models/users.dart
lib/models/roles.dart
lib/models/permissions.dart
lib/models/products.dart

Next, open and edit `lib/models/users.dart` then add these lines of Dart codes.

import 'roles.dart';

class Users {
  final int? id;
  final String? email;
  final String? password;
  final String? fullname;
  final String? phone;
  final String? createdAt;
  final String? updatedAt;
  final Roles? roles;

  Users({this.id, this.email, this.password, this.fullname, this.phone, this.createdAt, this.updatedAt, this.roles});

  factory Users.fromJson(Map<String, dynamic> json) {

    return Users(
        id: json['id'] as int,
        email: json['email'] as String,
        password: json['password'] as String,
        fullname: json['fullname'] as String,
        phone: json['phone'] as String,
        createdAt: json['createdAt'] as String,
        updatedAt: json['updatedAt'] as String,
        roles: Roles.fromJson(json['Role']));
  }

  @override
  String toString() {
    return 'Users{name: $fullname, email: $email}';
  }
}

As you can see, there's a model relationship with the Roles model because the REST API has a one-to-one relationship between User and Role models. Next, open and edit `lib/models/roles.dart` then add these lines of Dart codes. 

import 'permissions.dart';

class Roles {
  final int? id;
  final String? roleName;
  final String? roleDescription;
  final String? createdAt;
  final String? updatedAt;
  final List<Permissions>? permissions;

  Roles({this.id, this.roleName, this.roleDescription, this.createdAt, this.updatedAt, this.permissions});
  // Roles({this.id, this.roleName, this.roleDescription, this.createdAt, this.updatedAt});

  factory Roles.fromJson(Map<String, dynamic> json) {
    var list = json['permissions'] as List;
    List<Permissions> permissionList = list.map((i) => Permissions.fromJson(i)).toList();

    return Roles(
        id: json['id'] as int,
        roleName: json['role_name'] as String,
        roleDescription: json['role_description'] as String,
        createdAt: json['createdAt'] as String,
        updatedAt: json['updatedAt'] as String,
        permissions: permissionList);
  }

  @override
  String toString() {
    return 'Roles{role: $roleName}';
  }
}

As you can see, there's a model relationship with the Permissions model because the REST API has a one-to-many relationship between Role and Permission models. Next, open and edit `lib/models/permissions.dart` then add these lines of Dart codes.

class Permissions {
  final int? id;
  final String? permName;
  final String? permDescription;
  final String? createdAt;
  final String? updatedAt;

  Permissions({this.id, this.permName, this.permDescription, this.createdAt, this.updatedAt});

  factory Permissions.fromJson(Map<String, dynamic> json) {
    return Permissions(
        id: json['id'] as int,
        permName: json['perm_name'] as String,
        permDescription: json['perm_description'] as String,
        createdAt: json['createdAt'] as String,
        updatedAt: json['updatedAt'] as String);
  }

  @override
  String toString() {
    return '$permDescription';
  }
}

Next, open and edit `lib/models/products.dart` then add these lines of Dart codes.

class Products {
  final int? id;
  final String? prodName;
  final String? prodDescription;
  final String? prodImage;
  final int? prodPrice;
  final String? createdAt;
  final String? updatedAt;

  Products({this.id, this.prodName, this.prodDescription, this.prodImage, this.prodPrice, this.createdAt, this.updatedAt});

  factory Products.fromJson(Map<String, dynamic> json) {
    return Products(
        id: json['id'] as int,
        prodName: json['prod_name'] as String,
        prodDescription: json['prod_description'] as String,
        prodImage: json['prod_image'] as String,
        prodPrice: int.parse(json['prod_price'] as String),
        createdAt: json['createdAt'] as String,
        updatedAt: json['updatedAt'] as String);
  }

  @override
  String toString() {
    return 'Products{name: $prodName}';
  }
}

Those models are optional and will use for mapping the JSON response from the REST API.


Step #4: Create Flutter HTTP Service

As we mention in the first paragraph, we will use the HTTP library package to access the RESTful API from the Node-Express Auth-Role-Permissions REST API server. Also, we will need secure storage to save the JWT token from successful login and an HTTP interceptor to intercept every secure request to the REST API. For that, install this package by opening and editing pubspec.yaml then adds this dependency.

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  http: ^0.13.3
  flutter_secure_storage: ^4.2.0
  http_interceptor: ^1.0.1

Next, click the "Packages get" button in the Flutter commands at the top of pubspec.yml content. That command will install the registered dependencies.

Flutter Tutorial: Login, Role, and Permissions  - 10

Before creating the services, we need to create a package and a Dart file `lib/helpers/constant.dart` that contains all required constants variables. Fill this file with a class that contains the constructor and a variable of REST API URL.

class Constants {
  Constants._();
  static const BASE_URL = 'http://192.168.100.11:3000/api/v1';
}

That URL is a Node Express REST API that runs on this computer and uses a local network address. Of course, when we run Android or iOS apps should use the same local network.

Next, create a package and class or object `lib/services/api_service.dart` where we will put all CRUD (POST, GET, PUT, DELETE) methods to all endpoints of the REST API. In that file, add a class of API service.

class ApiService {
  
}

Add the imports before the class.

import 'dart:convert';

import 'package:flutter_auth_roleperm/helpers/constants.dart';
import 'package:flutter_auth_roleperm/services/apiinterceptor.dart';
import 'package:http/http.dart';
import 'package:http_interceptor/http/intercepted_client.dart';

As you can see in the imports, we will use a customized intercepted HTTP client instead of default Flutter HTTP. Next, declare a Client object that implementing the API interceptor inside the ApiService class.

  Client client = InterceptedClient.build(interceptors: [
    ApiInterceptor(),
  ]);

Next, add a request after the client object to get the currently logged-in user details.

  Future<Response> getMyProfile() async {
    var myProfileUri = Uri.parse('${Constants.BASE_URL}/auth/user/me');
    final res = await client.get(myProfileUri);
    return res;
  }

Add the CRUD REST API operation for the user endpoint.

  Future<Response> getUserList() async {
    var userUrl = Uri.parse('${Constants.BASE_URL}/users');
    final res = await client.get(userUrl);
    return res;
  }

  Future<Response> getUserById(String id) async {
    var userUrl = Uri.parse('${Constants.BASE_URL}/users/$id');
    final res = await client.get(userUrl);
    return res;
  }

  Future<Response> addUser(int roleId, String email, String password,
      String fullname, String phone) async {
    var userUrl = Uri.parse('${Constants.BASE_URL}/users');
    final res = await client.post(userUrl, body: {
      "role_id": roleId.toString(),
      "email": email,
      "password": password,
      "fullname": fullname,
      "phone": phone
    });
    return res;
  }

  Future<Response> updateUser(
      int? id, int roleId, String email, String fullname, String phone) async {
    var userUrl = Uri.parse('${Constants.BASE_URL}/users/$id');
    final res = await client.put(userUrl, body: {
      "role_id": roleId.toString(),
      "email": email,
      "fullname": fullname,
      "phone": phone
    });
    return res;
  }

  Future<Response> deleteUser(String id) async {
    var userUrl = Uri.parse('${Constants.BASE_URL}/users/$id');
    final res = await client.delete(userUrl);
    return res;
  }

Add the CRUD REST API operation for roles endpoint including endpoint to add permissions to a role.

  Future<Response> getRoleList() async {
    var rolerUrl = Uri.parse('${Constants.BASE_URL}/roles');
    final res = await client.get(rolerUrl);
    return res;
  }

  Future<Response> getRoleById(String id) async {
    var rolerUrl = Uri.parse('${Constants.BASE_URL}/roles/$id');
    final res = await client.get(rolerUrl);
    return res;
  }

  Future<Response> addRole(String roleName, String roleDescription) async {
    var rolerUrl = Uri.parse('${Constants.BASE_URL}/roles');
    final res = await client.post(rolerUrl,
        body: {"role_name": roleName, "role_description": roleDescription});
    return res;
  }

  Future<Response> updateRole(
      int? id, String roleName, String roleDescription) async {
    var rolerUrl = Uri.parse('${Constants.BASE_URL}/roles/$id');
    final res = await client.put(rolerUrl,
        body: {"role_name": roleName, "role_description": roleDescription});
    return res;
  }

  Future<Response> deleteRole(String id) async {
    var rolerUrl = Uri.parse('${Constants.BASE_URL}/roles/$id');
    final res = await client.delete(rolerUrl);
    return res;
  }

  Future<Response> addPermissiontoRole(int? id, List<int?> permissions) async {
    var permissionUrl =
        Uri.parse('${Constants.BASE_URL}/roles/permissions/$id');
    final res =
        await client.post(permissionUrl, body: {"permissions": jsonEncode(permissions)});
    return res;
  }

Add the CRUD REST API operation for the products endpoint.

  Future<Response> getProductList() async {
    var productUrl = Uri.parse('${Constants.BASE_URL}/products');
    final res = await client.get(productUrl);
    return res;
  }

  Future<Response> getProductById(String id) async {
    var productUrl = Uri.parse('${Constants.BASE_URL}/products/$id');
    final res = await client.get(productUrl);
    return res;
  }

  Future<Response> addProduct(String prodName, String prodDescription,
      String prodImage, String prodPrice) async {
    var productUrl = Uri.parse('${Constants.BASE_URL}/products');
    final res = await client.post(productUrl, body: {
      "prod_name": prodName,
      "prod_description": prodDescription,
      "prod_image": prodImage,
      "prod_price": prodPrice
    });
    return res;
  }

  Future<Response> updateProduct(int? id, String prodName,
      String prodDescription, String prodImage, String prodPrice) async {
    var productUrl = Uri.parse('${Constants.BASE_URL}/products/$id');
    final res = await client.put(productUrl, body: {
      "prod_name": prodName,
      "prod_description": prodDescription,
      "prod_image": prodImage,
      "prod_price": prodPrice
    });
    return res;
  }

  Future<Response> deleteProduct(String id) async {
    var productUrl = Uri.parse('${Constants.BASE_URL}/products/$id');
    final res = await client.delete(productUrl);
    return res;
  }

All REST API calls are secured and required Authorization headers using the JWT token. Authorization headers apply in ApiInterceptor class that will be created in the next step.


Step #5: Create Flutter Authentication Service and Interceptor

Before creating the services for the authentication (login, register, etc). We need to create API Interceptors that uses in the previous step. Inside the services folder add a new Dart file `lib/services/apiinterceptor.dart`. Add these lines of Dart codes inside that file.

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http_interceptor/http/interceptor_contract.dart';
import 'package:http_interceptor/models/request_data.dart';
import 'package:http_interceptor/models/response_data.dart';

class ApiInterceptor implements InterceptorContract {
  final storage = FlutterSecureStorage();

  Future<String> get tokenOrEmpty async {
    var token = await storage.read(key: "token");
    if (token == null) return "";
    return token;
  }

  @override
  Future<RequestData> interceptRequest({required RequestData data}) async {
    String token = await tokenOrEmpty;
    try {
      data.headers["Authorization"] = token;
    } catch (e) {
      print(e);
    }
    return data;
  }

  @override
  Future<ResponseData> interceptResponse({required ResponseData data}) async => data;
}

As you can see, the JWT token is stored in the Flutter Secure Storage. This interceptor class will check if the token exists or not then intercept the REST API request that was previously created by adding Authorization headers that contain the JWT token.

Next, create the authentication services inside the same package `lib/services/auth.dart` then fill that file with these Dart codes.

import 'package:flutter_auth_roleperm/helpers/constants.dart';
import 'package:http/http.dart';

class AuthService {
  var loginUri = Uri.parse('${Constants.BASE_URL}/auth/signin');
  var registerUri = Uri.parse('${Constants.BASE_URL}/auth/signup');

  Future<Response?> login(String username, String password) async {
    var res = await post(
        loginUri,
        body: {
          "username": username,
          "password": password
        }
    );
    return res;
  }

  Future<Response?> register(String username, String password, String fullname, String phone, String nik) async {
    var res = await post(
        registerUri,
        body: {
          "username": username,
          "password": password,
          "fullname": fullname,
          "phone": phone
        }
    );
    return res;
  }
} 

 That Auth Service just contains Login and Register only for this tutorial.


Step #6: Create Login and Register Screen

To build the views or screens for the mobile apps in the Flutter application, we need to prepare the required dependencies, libraries, or components. This tutorial required an email validator to validate an email text field, flutter easy loading to display a nice progress indicator, and custom fonts. Add dependencies first to `pubspec.yaml` file.

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  http: ^0.13.3
  flutter_secure_storage: ^4.2.0
  http_interceptor: ^1.0.1
  email_validator: ^2.0.1
  flutter_easyloading: ^3.0.0
  intl: ^0.17.0

Create a fonts folder at the root of the project folder. Download the custom fonts (ours: Roboto) then extract and copy them to the newly created fonts folder. Register those fonts to the `pubspec.yaml` file.

# The following section is specific to Flutter.
flutter:

  fonts:
    - family: Roboto
      fonts:
        - asset: fonts/Roboto-Black.ttf
        - asset: fonts/Roboto-BlackItalic.ttf
        - asset: fonts/Roboto-Bold.ttf
        - asset: fonts/Roboto-BoldItalic.ttf
        - asset: fonts/Roboto-Italic.ttf
        - asset: fonts/Roboto-Light.ttf
        - asset: fonts/Roboto-LightItalic.ttf
        - asset: fonts/Roboto-Medium.ttf
        - asset: fonts/Roboto-MediumItalic.ttf
        - asset: fonts/Roboto-Regular.ttf
        - asset: fonts/Roboto-Thin.ttf
        - asset: fonts/Roboto-ThinItalic.ttf

Next, we will configure the easy loading dependencies open end edit `lib/main.dart` then add this import.

import 'package:flutter_easyloading/flutter_easyloading.dart';

Add a method after the main method for easy loading configuration.

void configLoading() {
  EasyLoading.instance
    ..displayDuration = const Duration(milliseconds: 2000)
    ..indicatorType = EasyLoadingIndicatorType.wave
    ..loadingStyle = EasyLoadingStyle.custom
    ..indicatorSize = 45.0
    ..radius = 10.0
    ..progressColor = const Color.fromARGB(255, 255, 200, 0)
    ..backgroundColor = const Color.fromARGB(255, 0, 0, 0)
    ..indicatorColor = const Color.fromARGB(255, 255, 200, 0)
    ..textColor = const Color.fromARGB(255, 255, 200, 0)
    ..maskColor = Colors.blue.withOpacity(0.5)
    ..userInteractions = true
    ..dismissOnTap = false;
}

Add that method calls inside the main methods.

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

Next, create the package inside the lib folder `lib/screens` then add these files (`login.dart`, `register.dart`). Open and edit `lib/screens/login.dart` then add these Dart codes.

import 'dart:convert';

import 'package:email_validator/email_validator.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/screens/homescreen.dart';
import 'package:flutter_auth_roleperm/screens/registerscreen.dart';
import 'package:flutter_auth_roleperm/services/auth_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class LoginScreen extends StatelessWidget {
  const LoginScreen({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;
  static const String _title = 'Login';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulLoginWidget(errMsg: errMsg,),
    );
  }
}

class StatefulLoginWidget extends StatefulWidget {
  const StatefulLoginWidget({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;

  @override
  // ignore: no_logic_in_create_state
  State<StatefulLoginWidget> createState() => _StatefulLoginWidget(errMsg: errMsg);
}

class _StatefulLoginWidget extends State<StatefulLoginWidget> {
  _StatefulLoginWidget({required this.errMsg});
  final String errMsg;
  final AuthService authService = AuthService();
  final storage = const FlutterSecureStorage();
  final _loginFormKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool isLoading = false;

  void checkToken() async {
    var token = await storage.read(key: "token");
    if (token != null) {
      Navigator.pushReplacement(
          context, SlideRightRoute(page: const HomeScreen(errMsg: '',)));
    }
  }

  @override
  void initState() {
    super.initState();
    checkToken();

    if (errMsg.isNotEmpty) {
      Future.delayed(Duration.zero, () {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(errMsg),
        ));
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      body: SingleChildScrollView(
        child: Form(
          key: _loginFormKey,
          child: Column(
            children: [
              const Padding(
                padding: EdgeInsets.fromLTRB(15, 80, 15, 20),
                child: Text(
                  'Please login to enter the app!',
                  overflow: TextOverflow.visible,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    height: 1.171875,
                    fontSize: 18.0,
                    fontFamily: 'Roboto',
                    fontWeight: FontWeight.w500,
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _emailController,
                  validator: (value) {
                    if(value==null) {
                      return 'Please enter your email';
                    } else {
                      return EmailValidator.validate(value) ? null: 'Please fill with the valid email';
                    }
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Email',
                    hintText: 'Email',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.email,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _passwordController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your password';
                    } else {
                      return null;
                    }
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  obscureText: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Password',
                    hintText: 'Password',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.password,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 235, 235, 235), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
                child: SizedBox(
                  height: 60.0,
                  width: MediaQuery.of(context).size.width * 1.0,
                  child: ElevatedButton.icon(
                    icon: const Icon(
                      Icons.login,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24.0,
                    ),
                    style: ButtonStyle(
                      shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                          RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(5.0),
                            side: const BorderSide(color: Color.fromARGB(255, 128, 255, 0), width: 1.0),
                          )),
                      backgroundColor: MaterialStateProperty.all<Color>(
                          const Color.fromARGB(255, 255, 200, 0)),
                    ),
                    onPressed: () async {
                      if(_loginFormKey.currentState==null) {
                        ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                          content: Text("Wrong email and password!"),
                        ));
                      } else {
                        if (_loginFormKey.currentState!.validate()) {
                          _loginFormKey.currentState!.save();
                          EasyLoading.show();
                          var res = await authService.login(
                              _emailController.text, _passwordController.text);

                          switch (res!.statusCode) {
                            case 200:
                              EasyLoading.dismiss();
                              var data = jsonDecode(res.body);
                              storage.write(key: "token", value: data['token']);
                              Navigator.pushReplacement(
                                  context, SlideRightRoute(page: const HomeScreen(errMsg: '',)));
                              break;
                            case 401:
                              EasyLoading.dismiss();
                              ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                                content: Text("Wrong email or password!"),
                              ));
                              break;
                            default:
                              EasyLoading.dismiss();
                              ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                                content: Text("Wrong email or password!"),
                              ));
                              break;
                          }
                        }
                      }
                    },
                    label: const Text('LOGIN',
                        style: TextStyle(
                          height: 1.171875,
                          fontSize: 24.0,
                          fontFamily: 'Roboto',
                          fontWeight: FontWeight.w400,
                          color: Color.fromARGB(255, 0, 0, 0),
                        )),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(30),
                child: RichText(
                  text: TextSpan(
                    text: 'Not registered? ',
                    style: const TextStyle(
                      fontSize: 18.0,
                      fontFamily: 'Roboto',
                      fontWeight: FontWeight.w300,
                      color: Color.fromARGB(255, 0, 0, 0),
                    ),
                    children: <TextSpan>[
                      const TextSpan(
                          text: 'Register ',
                          style: TextStyle(
                            fontSize: 18.0,
                            fontFamily: 'Roboto',
                            fontWeight: FontWeight.w300,
                            color: Color.fromARGB(255, 0, 0, 0),
                          )),
                      TextSpan(
                          text: 'here ',
                          recognizer: TapGestureRecognizer()
                            ..onTap = () {
                              Navigator.push(context,
                                  SlideRightRoute(page: const RegisterScreen()));
                            },
                          style: const TextStyle(
                            fontSize: 18.0,
                            fontFamily: 'Roboto',
                            fontWeight: FontWeight.w700,
                            color: Color.fromARGB(255, 128, 255, 0),
                          )),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

As you can see, this screen uses StatelessWidget that include the StatelfulWidget as the MaterialApp home. The StatefulWidget build returns the Scaffold that has a body the SingleChildScrollView to make the screen scrollable when a soft keyboard shows up or manually slides to scroll the screen. The Form is used as the SingleChildScrollView child and it has one Column that contains Text, TextFormField, and Button which align stacked vertically.

The Button action (onPressed event) will validate the Form first then call the login method from the AuthService with the parameters email and password that get from the controllers. The response from the REST API might be 200 which means success to POST the login then redirecting to the HomeScreen that will be created in the next step. The other response codes will display the Snack Bar messages.

Next, open and edit `lib/screens/register.dart` then add these lines of Dart codes. 

import 'dart:convert';

import 'package:email_validator/email_validator.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/screens/loginscreen.dart';
import 'package:flutter_auth_roleperm/services/auth_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class RegisterScreen extends StatelessWidget {
  const RegisterScreen({Key? key}) : super(key: key);
  static const String _title = 'Register';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: StatefulRegisterWidget(),
    );
  }
}

class StatefulRegisterWidget extends StatefulWidget {
  const StatefulRegisterWidget({Key? key}) : super(key: key);

  @override
  State<StatefulRegisterWidget> createState() => _StatefulRegisterWidget();
}

class _StatefulRegisterWidget extends State<StatefulRegisterWidget> {
  final AuthService authService = AuthService();
  final storage = const FlutterSecureStorage();
  final _registerFormKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _nameController = TextEditingController();
  final _phoneController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      body: SingleChildScrollView(
        child: Form(
          key: _registerFormKey,
          child: Column(
            children: [
              const Padding(
                padding: EdgeInsets.fromLTRB(15, 80, 15, 20),
                child: Text(
                  'Please fill your email, password, name, and phone',
                  overflow: TextOverflow.visible,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    height: 1.171875,
                    fontSize: 18.0,
                    fontFamily: 'Roboto',
                    fontWeight: FontWeight.w500,
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _emailController,
                  validator: (value) {
                    if(value==null) {
                      return 'Please enter your email';
                    } else {
                      return EmailValidator.validate(value) ? null: 'Please fill with the valid email';
                    }
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    labelText: 'Email',
                    hintText: 'Email',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.email,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _passwordController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your password';
                    } else if(value.length < 6) {
                      return 'Minimum 6 characters';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  obscureText: true,
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    labelText: 'Kata Sandi',
                    hintText: 'Kata Sandi',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.password,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _nameController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your name';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Name',
                    hintText: 'Name',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.perm_identity,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _phoneController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your name';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Nomor HP',
                    hintText: 'Nomor HP',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.perm_identity,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
                child: SizedBox(
                  height: 60.0,
                  width: MediaQuery.of(context).size.width * 1.0,
                  child: ElevatedButton.icon(
                    icon: const Icon(
                      Icons.update,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24.0,
                    ),
                    style: ButtonStyle(
                      shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                          RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(5.0),
                            side: const BorderSide(color: Color.fromARGB(255, 128, 255, 0), width: 1.0),
                          )),
                      backgroundColor: MaterialStateProperty.all<Color>(
                          const Color.fromARGB(255, 255, 200, 0)),
                    ),
                    onPressed: () async {
                      if (_registerFormKey.currentState!.validate()) {
                        _registerFormKey.currentState!.save();
                        EasyLoading.show();
                        var res = await authService.register(
                            _emailController.text, _passwordController.text, _nameController.text, _phoneController.text);

                        switch (res!.statusCode) {
                          case 201:
                            EasyLoading.dismiss();
                            Navigator.push(
                                context, SlideRightRoute(page: const LoginScreen(errMsg: 'Registered Successfully',)));
                            break;
                          case 400:
                            EasyLoading.dismiss();
                            var data = jsonDecode(res.body);
                            if (data["msg"]) {
                              ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                                content: Text(data["msg"].toString()),
                              ));
                            }
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Registration Failed"),
                            ));
                            break;
                          default:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Registration Failed"),
                            ));
                            break;
                        }
                      }
                    },
                    label: const Text('REGISTER',
                        style: TextStyle(
                          height: 1.171875,
                          fontSize: 24.0,
                          fontFamily: 'Roboto',
                          fontWeight: FontWeight.w400,
                          color: Color.fromARGB(255, 0, 0, 0),
                        )),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(30),
                child: RichText(
                  text: TextSpan(
                    text: 'Already registered? ',
                    style: const TextStyle(
                      fontSize: 18.0,
                      fontFamily: 'Roboto',
                      fontWeight: FontWeight.w300,
                      color: Color.fromARGB(255, 0, 0, 0),
                    ),
                    children: <TextSpan>[
                      const TextSpan(
                          text: 'Login ',
                          style: TextStyle(
                            fontSize: 18.0,
                            fontFamily: 'Roboto',
                            fontWeight: FontWeight.w300,
                            color: Color.fromARGB(255, 235, 235, 235),
                          )),
                      TextSpan(
                          text: 'here ',
                          recognizer: TapGestureRecognizer()
                            ..onTap = () {
                              Navigator.push(context,
                                  SlideRightRoute(page: const LoginScreen(errMsg: '',)));
                            },
                          style: const TextStyle(
                            fontSize: 18.0,
                            fontFamily: 'Roboto',
                            fontWeight: FontWeight.w700,
                            color: Color.fromARGB(255, 128, 255, 0),
                          )),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

The structure of the Flutter widget same as the login screen. The Button action (onPressed event) will validate the Form first then call the register method from the AuthService with the parameters email, password, name, and phone that get from the controllers. The response from the REST API might be 201 which means success to POST the registered data then back to the Login screen. The other response codes will display the Snack Bar messages.


Step #7: Create Home Screen and Drawer

The users, roles, and products management (CRUD) will organized from the Drawer in the Home Screen. For that, create a Dart file inside screens package `lib/screens/home.dart. Open that file then add these lines of Dart codes.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/screens/loginscreen.dart';
import 'package:flutter_auth_roleperm/screens/productsscreen.dart';
import 'package:flutter_auth_roleperm/screens/rolesscreen.dart';
import 'package:flutter_auth_roleperm/screens/usersscreen.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;
  static const String _title = 'Home';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulHomeWidget(errMsg: errMsg,),
    );
  }
}

class StatefulHomeWidget extends StatefulWidget {
  const StatefulHomeWidget({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;

  @override
  // ignore: no_logic_in_create_state
  State<StatefulHomeWidget> createState() => _StatefulHomeWidget(errMsg: errMsg);
}

class _StatefulHomeWidget extends State<StatefulHomeWidget> {
  _StatefulHomeWidget({required this.errMsg});
  final String errMsg;
  final storage = const FlutterSecureStorage();

  @override
  void initState() {
    super.initState();

    if (errMsg.isNotEmpty) {
      Future.delayed(Duration.zero, () {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(errMsg),
        ));
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Flutter Auth Role',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
      ),
      drawer: Drawer(
        child: Container(
          color: const Color.fromARGB(255, 252, 142, 54),
          child: ListView(
            padding: EdgeInsets.zero,
            children: [
              const DrawerHeader(
                decoration: BoxDecoration(
                  color: Color.fromARGB(255, 0, 0, 0),
                ),
                child: Center(
                  child: Text(
                    'MENU',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      height: 1.171875,
                      fontSize: 18.0,
                      fontFamily: 'Roboto',
                      fontWeight: FontWeight.w500,
                      color: Color.fromARGB(255, 26, 255, 1),
                    ),
                  ),
                ),
              ),
              Container(
                decoration: const BoxDecoration(
                  border: Border(
                    left: BorderSide(width: 6.0, color: Color.fromARGB(255, 0, 0, 0)),
                    bottom: BorderSide(width: 1.0, color: Color.fromARGB(255, 0, 0, 0)),
                  ),
                ),
                child: ListTile(
                  title: const Text(
                    'Users',
                    style: TextStyle(
                      height: 1.171875,
                      fontSize: 18.0,
                      fontFamily: 'Roboto',
                      fontWeight: FontWeight.w500,
                      color: Color.fromARGB(255, 26, 255, 1),
                    ),
                  ),
                  onTap: () {
                    Navigator.push(context,
                        SlideRightRoute(page: const UsersScreen(errMsg: '',)));
                  },
                ),
              ),
              Container(
                decoration: const BoxDecoration(
                  border: Border(
                    left: BorderSide(width: 6.0, color: Color.fromARGB(255, 0, 0, 0)),
                    bottom: BorderSide(width: 1.0, color: Color.fromARGB(255, 0, 0, 0)),
                  ),
                ),
                child: ListTile(
                  title: const Text(
                    'Roles',
                    style: TextStyle(
                      height: 1.171875,
                      fontSize: 18.0,
                      fontFamily: 'Roboto',
                      fontWeight: FontWeight.w500,
                      color: Color.fromARGB(255, 26, 255, 1),
                    ),
                  ),
                  onTap: () {
                    Navigator.push(context,
                        SlideRightRoute(page: const RolesScreen(errMsg: '',)));
                  },
                ),
              ),
              Container(
                decoration: const BoxDecoration(
                  border: Border(
                    left: BorderSide(width: 6.0, color: Color.fromARGB(255, 0, 0, 0)),
                    bottom: BorderSide(width: 1.0, color: Color.fromARGB(255, 0, 0, 0)),
                  ),
                ),
                child: ListTile(
                  title: const Text(
                    'Products',
                    style: TextStyle(
                      height: 1.171875,
                      fontSize: 18.0,
                      fontFamily: 'Roboto',
                      fontWeight: FontWeight.w500,
                      color: Color.fromARGB(255, 26, 255, 1),
                    ),
                  ),
                  onTap: () {
                    Navigator.push(context,
                        SlideRightRoute(page: const ProductsScreen(errMsg: '',)));
                  },
                ),
              ),
              Container(
                decoration: const BoxDecoration(
                  border: Border(
                    left: BorderSide(width: 6.0, color: Color.fromARGB(255, 0, 0, 0)),
                    bottom: BorderSide(width: 1.0, color: Color.fromARGB(255, 0, 0, 0)),
                  ),
                ),
                child: ListTile(
                  title: const Text(
                    'Logout',
                    style: TextStyle(
                      height: 1.171875,
                      fontSize: 18.0,
                      fontFamily: 'Roboto',
                      fontWeight: FontWeight.w500,
                      color: Color.fromARGB(255, 26, 255, 1),
                    ),
                  ),
                  onTap: () async {
                    await storage.deleteAll();
                    Navigator.push(context,
                        SlideRightRoute(page: const LoginScreen(errMsg: 'User logged out',)));
                  },
                ),
              ),
            ],
          ),
        ),
      ),
      body: const Center(
        child: Text(
          'Welcome Home!\nThis is the Role Based Authentication\nwith Permissions Apps',
          textAlign: TextAlign.center,
          overflow: TextOverflow.visible,
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 0, 0, 0),
          ),
        ),
      ),
    );
  }
} 

As you can see, this screen uses StatelessWidget that include the StatelfulWidget as the MaterialApp home. The StatefulWidget build returns the Scaffold that has an AppBar, a drawer that contains the menu to access Users, Roles, and Products, a body that has a child of the Center that will display a single Text only.


Step #8: Create Users CRUD Screen

One of the action buttons in the drawer menu will navigate to the Users screen that contains a list of users that registered of added manually. Create the new files `lib/screens/usersscreen.dart`, `lib/screens/userdetailsscreen.dart`, `lib/screens/edituserscreen.dart`, `lib/screens/adduserscreen.dart` and a folder and file `lib/widgets/userlistwidget.dart`. We will use the separate list widget to display the list of users. Open and edit `lib/screens/usersscreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/models/users.dart';
import 'package:flutter_auth_roleperm/screens/adduserscreen.dart';
import 'package:flutter_auth_roleperm/screens/homescreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_auth_roleperm/widgets/userlistwidget.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart';

import 'loginscreen.dart';

class UsersScreen extends StatelessWidget {
  const UsersScreen({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;
  static const String _title = 'User List';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulUsersWidget(errMsg: errMsg,),
    );
  }
}

class StatefulUsersWidget extends StatefulWidget {
  const StatefulUsersWidget({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;

  @override
  // ignore: no_logic_in_create_state
  State<StatefulUsersWidget> createState() => _StatefulUsersWidget(errMsg: errMsg);
}

class _StatefulUsersWidget extends State<StatefulUsersWidget> {
  _StatefulUsersWidget({required this.errMsg});
  final String errMsg;
  final ApiService api = ApiService();
  late List<Users> users = [];

  @override
  void initState() {
    super.initState();

    if (errMsg.isNotEmpty) {
      Future.delayed(Duration.zero, () {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(errMsg),
        ));
      });
    }
  }

  Future<Response> get listUsers async {
    EasyLoading.show();
    return await api.getUserList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Users',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () => Navigator.pushReplacement(
              context, SlideRightRoute(page: const HomeScreen(errMsg: '',))),
        ),
      ),
      body: Center(
        child: FutureBuilder(
          future: listUsers,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              Response resp = snapshot.data as Response;
              if (resp.statusCode == 200) {
                EasyLoading.dismiss();
                final jsonMap = json.decode(resp.body);
                users = (jsonMap as List)
                    .map((userItem) => Users.fromJson(userItem))
                    .toList();
                return users.isNotEmpty
                    ? Padding(
                        padding: const EdgeInsets.symmetric(
                            vertical: 10.0, horizontal: 10.0),
                        child: UserListWidget(users: users),
                      )
                    : const Center(
                        child: Padding(
                          padding: EdgeInsets.symmetric(
                              vertical: 10.0, horizontal: 20.0),
                          child: Text(
                            'No users found',
                            overflow: TextOverflow.visible,
                            textAlign: TextAlign.center,
                            style: TextStyle(
                              height: 1.171875,
                              fontSize: 24.0,
                              fontFamily: 'Roboto',
                              fontWeight: FontWeight.w300,
                              color: Color.fromARGB(255, 255, 255, 255),
                            ),
                          ),
                        ),
                      );
              } else if (resp.statusCode == 401) {
                EasyLoading.dismiss();
                Future.delayed(Duration.zero, () {
                  Navigator.push(
                      context,
                      MaterialPageRoute(
                          builder: (context) => const LoginScreen(errMsg: 'Unauthenticated',)));
                });
              } else if (resp.statusCode == 403) {
                EasyLoading.dismiss();
                Future.delayed(Duration.zero, () {
                  Navigator.push(
                      context,
                      MaterialPageRoute(
                          builder: (context) => const HomeScreen(errMsg: 'Permission Denied',)));
                });
              }
            } else if (snapshot.hasError) {
              EasyLoading.dismiss();
              ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                content: Text('${snapshot.error}'),
              ));
            }
            return const Center(
              child: Text(''''''),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
              context,
              MaterialPageRoute(
                  builder: (context) => const AddUserScreen()));
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
      ),
    );
  }
}

As you can see, that screen is extended from StatelessWidget that has a constructor with a string variable. This StatelessWidget builds a MaterialApp with the home value is a StatefulWidget that also has a constructor with a string variable. The message from other screens, API service, and List of Users variable is included in the State<StatefulUsersWidget> class. The message is displayed in the initState method and the API call to get the list of users in another method before the build widget.

The build widget has an appBar and body that contains FutureBuilder that will use the list of users variable then arrange in the separate list-builder widget. Inside the body, there is also a floating action button that will use to navigate to add user screen.

Next, open and edit `lib/screens/usersscreen.dart` the add these lines of Dart codes.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/models/users.dart';
import 'package:flutter_auth_roleperm/screens/userdetailsscreen.dart';

class UserListWidget extends StatelessWidget {
  final List<Users> users;

  const UserListWidget({Key? key, required this.users}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: users.isEmpty ? 0 : users.length,
      itemBuilder: (BuildContext context, int index) {
        return Padding(
          padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0),
          child: Card(
            color: const Color.fromARGB(255, 252, 142, 54),
            shape: RoundedRectangleBorder(
              side: const BorderSide(
                  color: Color.fromARGB(255, 0, 0, 0), width: 1),
              borderRadius: BorderRadius.circular(10),
            ),
            child: InkWell(
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) =>
                          UserDetailsScreen(users: users[index])),
                );
              },
              child: ListTile(
                leading: const Icon(
                  Icons.person,
                  size: 48,
                  color: Color.fromARGB(255, 0, 0, 0),
                ),
                title: Text(
                  users[index].fullname.toString(),
                  style: const TextStyle(
                    fontFamily: 'Roboto',
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
                subtitle: Text(
                  users[index].email.toString(),
                  style: const TextStyle(
                    fontFamily: 'Roboto',
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

That ListView builder contains the Card that has the child of InkWell that use to navigate to the DetailWidget using MaterialPageRoute. The child of the Card is ListTile that contains an Icon (leading), Text (title), and Text(subtitle).

The InkWell widget has an onTap event with an action to Navigate to the details page. Container, Column, Image, and Text have their own properties to adjust the style or layout.

Keep in mind, every widget that uses the child only has one widget as its child. If you need to put more than one widget to the parent widget, use children: <Widget> property.

Next, open and edit `lib/screens/userdetailsscreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/models/users.dart';
import 'package:flutter_auth_roleperm/screens/edituserscreen.dart';
import 'package:flutter_auth_roleperm/screens/usersscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';

class UserDetailsScreen extends StatelessWidget {
  const UserDetailsScreen({Key? key, required this.users}) : super(key: key);
  final Users users;
  static const String _title = 'Users';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulUserDetailsWidget(users: users),
    );
  }
}

class StatefulUserDetailsWidget extends StatefulWidget {
  const StatefulUserDetailsWidget({Key? key, required this.users})
      : super(key: key);
  final Users users;

  @override
  // ignore: no_logic_in_create_state
  _UserDetailsWidgetState createState() => _UserDetailsWidgetState(users: users);
}

class _UserDetailsWidgetState extends State<StatefulUserDetailsWidget> {
  _UserDetailsWidgetState({required this.users});

  final Users users;
  final ApiService api = ApiService();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'User Details',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () => Navigator.pushReplacement(
              context, SlideRightRoute(page: const UsersScreen(errMsg: '',))),
        ),
      ),
      body: SingleChildScrollView(
        child: Container(
          padding: const EdgeInsets.all(20.0),
          child: Card(
              child: Container(
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(5.0),
                    color: const Color.fromARGB(255, 252, 142, 54),
                      border: Border.all(color: const Color.fromARGB(255, 0, 0, 0)),
                  ),
                  padding: const EdgeInsets.all(10.0),
                  width: 440,
                  child: Column(
                    children: <Widget>[
                      Container(
                        margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
                        child: Column(
                          children: <Widget>[
                            Text('Name:',
                                style: TextStyle(
                                    color: Colors.black.withOpacity(0.8))),
                            Text(widget.users.fullname.toString(),
                                style: Theme.of(context).textTheme.subtitle1)
                          ],
                        ),
                      ),
                      Container(
                        margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
                        child: Column(
                          children: <Widget>[
                            Text('Email:',
                                style: TextStyle(
                                    color: Colors.black.withOpacity(0.8))),
                            Text(widget.users.email.toString(),
                                style: Theme.of(context).textTheme.subtitle1)
                          ],
                        ),
                      ),
                      Container(
                        margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
                        child: Column(
                          children: <Widget>[
                            Text('Phone:',
                                style: TextStyle(
                                    color: Colors.black.withOpacity(0.8))),
                            Text(widget.users.phone.toString(),
                                style: Theme.of(context).textTheme.subtitle1)
                          ],
                        ),
                      ),
                      Container(
                        margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
                        child: Column(
                          children: <Widget>[
                            Text('Role:',
                                style: TextStyle(
                                    color: Colors.black.withOpacity(0.8))),
                            Text(widget.users.roles!.roleName.toString(),
                                style: Theme.of(context).textTheme.subtitle1)
                          ],
                        ),
                      ),
                      Container(
                        margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
                        child: Column(
                          children: <Widget>[
                            Padding(
                              padding: const EdgeInsets.symmetric(
                                  horizontal: 30, vertical: 10),
                              child: SizedBox(
                                height: 60.0,
                                width: MediaQuery.of(context).size.width * 1.0,
                                child: ElevatedButton.icon(
                                  icon: const Icon(
                                    Icons.edit,
                                    color: Color.fromARGB(255, 0, 0, 0),
                                    size: 24.0,
                                  ),
                                  style: ButtonStyle(
                                    shape: MaterialStateProperty.all<
                                            RoundedRectangleBorder>(
                                        RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(5.0),
                                      side: const BorderSide(
                                          color:
                                              Color.fromARGB(255, 128, 255, 0),
                                          width: 1.0),
                                    )),
                                    backgroundColor: MaterialStateProperty.all<
                                            Color>(
                                        const Color.fromARGB(255, 255, 200, 0)),
                                  ),
                                  onPressed: () async {
                                    Navigator.pushReplacement(
                                        context,
                                        SlideRightRoute(
                                            page: EditUserScreen(
                                          users: users,
                                        )));
                                  },
                                  label: const Text('EDIT',
                                      style: TextStyle(
                                        height: 1.171875,
                                        fontSize: 24.0,
                                        fontFamily: 'Roboto',
                                        fontWeight: FontWeight.w400,
                                        color: Color.fromARGB(255, 0, 0, 0),
                                      )),
                                ),
                              ),
                            ),
                            Padding(
                              padding: const EdgeInsets.symmetric(
                                  horizontal: 30, vertical: 10),
                              child: SizedBox(
                                height: 60.0,
                                width: MediaQuery.of(context).size.width * 1.0,
                                child: ElevatedButton.icon(
                                  icon: const Icon(
                                    Icons.delete,
                                    color: Color.fromARGB(255, 0, 0, 0),
                                    size: 24.0,
                                  ),
                                  style: ButtonStyle(
                                    shape: MaterialStateProperty.all<
                                            RoundedRectangleBorder>(
                                        RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(5.0),
                                      side: const BorderSide(
                                          color:
                                              Color.fromARGB(255, 128, 255, 0),
                                          width: 1.0),
                                    )),
                                    backgroundColor: MaterialStateProperty.all<
                                            Color>(
                                        const Color.fromARGB(255, 255, 200, 0)),
                                  ),
                                  onPressed: () async {
                                    _confirmDialog();
                                  },
                                  label: const Text('DELETE',
                                      style: TextStyle(
                                        height: 1.171875,
                                        fontSize: 24.0,
                                        fontFamily: 'Roboto',
                                        fontWeight: FontWeight.w400,
                                        color: Color.fromARGB(255, 0, 0, 0),
                                      )),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ))),
        ),
      ),
    );
  }

  Future<void> _confirmDialog() async {
    return showDialog<void>(
      context: context,
      barrierDismissible: false, // user must tap button!
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Warning!'),
          content: SingleChildScrollView(
            child: ListBody(
              children: const <Widget>[
                Text('Are you sure want delete this item?'),
              ],
            ),
          ),
          actions: <Widget>[
            ElevatedButton(
              child: const Text('Yes'),
              onPressed: () async {
                EasyLoading.show();
                var res = await api.deleteUser(widget.users.id.toString());

                switch (res.statusCode) {
                  case 200:
                    EasyLoading.dismiss();
                    Navigator.pushReplacement(
                        context, SlideRightRoute(page: const UsersScreen(errMsg: 'Deleted Successfully',)));
                    break;
                  case 400:
                    EasyLoading.dismiss();
                    var data = jsonDecode(res.body);
                    if (data["msg"] != null) {
                      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                        content: Text(data["msg"].toString()),
                      ));
                    }
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                      content: Text("Delete Failed"),
                    ));
                    break;
                  case 403:
                    EasyLoading.dismiss();
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                      content: Text("Permission Denied"),
                    ));
                    break;
                  default:
                    EasyLoading.dismiss();
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                      content: Text("Delete Failed"),
                    ));
                    break;
                }
              },
            ),
            ElevatedButton(
              child: const Text('No'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}

Those codes build widgets combination of Container, Card, Column, Icon, Text, and ElevatedButton. The ElevatedButton has onPressed event that action to navigate to the EditDataWidget and trigger delete confirm dialog. Next, before the closing of _DetailWidgetState class body add this method or function to navigate to the EditDataWidget with cases object params.

Next, open and edit `lib/screens/edituserscreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:email_validator/email_validator.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/models/users.dart';
import 'package:flutter_auth_roleperm/screens/userdetailsscreen.dart';
import 'package:flutter_auth_roleperm/screens/usersscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';

class EditUserScreen extends StatelessWidget {
  const EditUserScreen({Key? key, required this.users}) : super(key: key);
  final Users users;
  static const String _title = 'Edit User';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulEditUserWidget(users: users),
    );
  }
}

class StatefulEditUserWidget extends StatefulWidget {
  const StatefulEditUserWidget({Key? key, required this.users}) : super(key: key);
  final Users users;

  @override
  // ignore: no_logic_in_create_state
  _EditUserWidgetState createState() => _EditUserWidgetState(users: users);
}

class _EditUserWidgetState extends State<StatefulEditUserWidget> {
  _EditUserWidgetState({required this.users});
  final Users users;
  List<dynamic> roles = [];
  final ApiService api = ApiService();
  final _editUserFormKey = GlobalKey<FormState>();
  int? _valRole;
  final _emailController = TextEditingController();
  final _nameController = TextEditingController();
  final _phoneController = TextEditingController();

  void loadRolesList() async {
    final resp = await api.getRoleList();
    setState(() {
      roles = jsonDecode(resp.body);
    });
  }

  @override
  void initState() {
    super.initState();
    loadRolesList();
    _valRole = users.roles!.id;
    _emailController.text = users.email.toString();
    _nameController.text = users.fullname.toString();
    _phoneController.text = users.phone.toString();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Edit User',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () =>
              Navigator.pushReplacement(context, SlideRightRoute(page: UserDetailsScreen(users: users))),
        ),

      ),
      body: SingleChildScrollView(
        child: Form(
          key: _editUserFormKey,
          child: Column(
            children: [
              const Padding(
                padding: EdgeInsets.fromLTRB(15, 80, 15, 20),
                child: Text(
                  'Please edit your role, email, password, name, and phone',
                  overflow: TextOverflow.visible,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    height: 1.171875,
                    fontSize: 18.0,
                    fontFamily: 'Roboto',
                    fontWeight: FontWeight.w500,
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
              Padding(
                padding:
                const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: InputDecorator(
                  decoration: const InputDecoration(
                    icon: Icon(
                      Icons.group_work,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24,
                    ),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                  ),
                  child: DropdownButtonHideUnderline(
                      child: DropdownButton(
                        style: const TextStyle(
                            color: Color.fromARGB(255, 0, 0, 0), fontSize: 24.0),
                        dropdownColor: const Color.fromARGB(255, 0, 0, 0),
                        borderRadius: const BorderRadius.all(Radius.circular(5.0)),
                        isExpanded: true,
                        value: _valRole,
                        hint: const Text(
                          'Select Role',
                          style: TextStyle(
                              height: 1.171875,
                              fontSize: 24.0,
                              fontFamily: 'Roboto',
                              fontWeight: FontWeight.w300,
                              color: Color.fromARGB(255, 0, 0, 0)),
                        ),
                        items: roles.map((item) {
                          return DropdownMenuItem(
                            child: Text(
                              item['role_name'],
                              style: const TextStyle(
                                height: 2.171875,
                                fontSize: 24.0,
                                fontFamily: 'Roboto',
                                fontWeight: FontWeight.w300,
                                color: Color.fromARGB(255, 26, 255, 1),
                              ),
                            ),
                            value: item['id'],
                          );
                        }).toList(),
                        onChanged: (value) {
                          setState(() {
                            _valRole = value as int;
                          });
                        },
                      )),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _emailController,
                  validator: (value) {
                    if(value==null) {
                      return 'Please enter your email';
                    } else {
                      return EmailValidator.validate(value) ? null: 'Please fill with the valid email';
                    }
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    labelText: 'Email',
                    hintText: 'Email',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.email,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _nameController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your name';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Name',
                    hintText: 'Name',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.perm_identity,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _phoneController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your name';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Nomor HP',
                    hintText: 'Nomor HP',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.perm_identity,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
                child: SizedBox(
                  height: 60.0,
                  width: MediaQuery.of(context).size.width * 1.0,
                  child: ElevatedButton.icon(
                    icon: const Icon(
                      Icons.login,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24.0,
                    ),
                    style: ButtonStyle(
                      shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                          RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(5.0),
                            side: const BorderSide(color: Color.fromARGB(255, 128, 255, 0), width: 1.0),
                          )),
                      backgroundColor: MaterialStateProperty.all<Color>(
                          const Color.fromARGB(255, 255, 200, 0)),
                    ),
                    onPressed: () async {
                      if (_editUserFormKey.currentState!.validate()) {
                        _editUserFormKey.currentState!.save();
                        EasyLoading.show();
                        var res = await api.updateUser(
                            users.id, _valRole!, _emailController.text, _nameController.text, _phoneController.text);

                        switch (res.statusCode) {
                          case 200:
                            EasyLoading.dismiss();
                            Navigator.pushReplacement(
                                context, SlideRightRoute(page: const UsersScreen(errMsg: 'Updated Successfully',)));
                            break;
                          case 400:
                            EasyLoading.dismiss();
                            var data = jsonDecode(res.body);
                            if (data["msg"] != null) {
                              ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                                content: Text(data["msg"].toString()),
                              ));
                            }
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Update Failed"),
                            ));
                            break;
                          case 403:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Permission Denied"),
                            ));
                            break;
                          default:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Update Failed"),
                            ));
                            break;
                        }
                      }
                    },
                    label: const Text('UPDATE',
                        style: TextStyle(
                          height: 1.171875,
                          fontSize: 24.0,
                          fontFamily: 'Roboto',
                          fontWeight: FontWeight.w400,
                          color: Color.fromARGB(255, 0, 0, 0),
                        )),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

The content of those edit user screens is just a Form with a Dropdown menu with the items get from the REST API, a few of the TextFormFields, and ElevatedButton that will submit the updated data to the REST API.

Next, open and edit `lib/screens/adduserscreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:email_validator/email_validator.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/screens/usersscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';

class AddUserScreen extends StatelessWidget {
  const AddUserScreen({Key? key}) : super(key: key);
  static const String _title = 'Add User';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: StatefulAddUsersWidget(),
    );
  }
}

class StatefulAddUsersWidget extends StatefulWidget {
  const StatefulAddUsersWidget({Key? key}) : super(key: key);

  @override
  // ignore: no_logic_in_create_state
  _AddUserWidgetState createState() => _AddUserWidgetState();
}

class _AddUserWidgetState extends State<StatefulAddUsersWidget> {
  List<dynamic> roles = [];
  final ApiService api = ApiService();
  final _addUserFormKey = GlobalKey<FormState>();
  int? _valRole;
  final _passwordController = TextEditingController();
  final _emailController = TextEditingController();
  final _nameController = TextEditingController();
  final _phoneController = TextEditingController();

  void loadRolesList() async {
    final resp = await api.getRoleList();
    setState(() {
      roles = jsonDecode(resp.body);
    });
  }

  @override
  void initState() {
    super.initState();
    loadRolesList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Add User',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () => Navigator.pushReplacement(
              context, SlideRightRoute(page: const UsersScreen(errMsg: '',))),
        ),
      ),
      body: SingleChildScrollView(
        child: Form(
          key: _addUserFormKey,
          child: Column(
            children: [
              const Padding(
                padding: EdgeInsets.fromLTRB(15, 80, 15, 20),
                child: Text(
                  'Please fill your role, email, password, name, and phone',
                  overflow: TextOverflow.visible,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    height: 1.171875,
                    fontSize: 18.0,
                    fontFamily: 'Roboto',
                    fontWeight: FontWeight.w500,
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: InputDecorator(
                  decoration: const InputDecoration(
                    icon: Icon(
                      Icons.group_work,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24,
                    ),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                  ),
                  child: DropdownButtonHideUnderline(
                      child: DropdownButton(
                    dropdownColor: const Color.fromARGB(255, 0, 0, 0),
                    borderRadius: const BorderRadius.all(Radius.circular(5.0)),
                    isExpanded: true,
                    value: _valRole,
                    hint: const Text(
                      'Select Role',
                      style: TextStyle(
                          height: 1.171875,
                          fontSize: 24.0,
                          fontFamily: 'Roboto',
                          fontWeight: FontWeight.w300,
                          color: Color.fromARGB(255, 0, 0, 0)),
                    ),
                    items: roles.map((item) {
                      return DropdownMenuItem(
                        child: Text(
                          item['role_name'],
                          style: const TextStyle(
                            height: 2.171875,
                            fontSize: 24.0,
                            fontFamily: 'Roboto',
                            fontWeight: FontWeight.w300,
                            color: Color.fromARGB(255, 26, 255, 1),
                          ),
                        ),
                        value: item['id'],
                      );
                    }).toList(),
                    onChanged: (value) {
                      setState(() {
                        _valRole = value as int;
                      });
                    },
                  )),
                ),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _emailController,
                  validator: (value) {
                    if (value == null) {
                      return 'Please enter your email';
                    } else {
                      return EmailValidator.validate(value)
                          ? null
                          : 'Please fill with the valid email';
                    }
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle:
                        TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    labelText: 'Email',
                    hintText: 'Email',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.email,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _passwordController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your password';
                    } else if (value.length < 6) {
                      return 'Minimum 6 characters';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  obscureText: true,
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    errorStyle:
                        TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    labelText: 'Password',
                    hintText: 'Password',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.password,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _nameController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your name';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle:
                        TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Name',
                    hintText: 'Name',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.perm_identity,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _phoneController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your phone';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(
                    errorStyle:
                        TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Phone',
                    hintText: 'Phone',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.perm_identity,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
                child: SizedBox(
                  height: 60.0,
                  width: MediaQuery.of(context).size.width * 1.0,
                  child: ElevatedButton.icon(
                    icon: const Icon(
                      Icons.save,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24.0,
                    ),
                    style: ButtonStyle(
                      shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                          RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(5.0),
                        side: const BorderSide(
                            color: Color.fromARGB(255, 128, 255, 0),
                            width: 1.0),
                      )),
                      backgroundColor: MaterialStateProperty.all<Color>(
                          const Color.fromARGB(255, 255, 200, 0)),
                    ),
                    onPressed: () async {
                      if (_addUserFormKey.currentState!.validate()) {
                        _addUserFormKey.currentState!.save();
                        EasyLoading.show();
                        var res = await api.addUser(
                            _valRole!,
                            _emailController.text,
                            _passwordController.text,
                            _nameController.text,
                            _phoneController.text);

                        switch (res.statusCode) {
                          case 201:
                            EasyLoading.dismiss();
                            Navigator.pushReplacement(
                                context, SlideRightRoute(page: const UsersScreen(errMsg: 'User Added Successfully',)));
                            break;
                          case 400:
                            EasyLoading.dismiss();
                            var data = jsonDecode(res.body);
                            if (data["msg"]) {
                              ScaffoldMessenger.of(context)
                                  .showSnackBar(SnackBar(
                                content: Text(data["msg"].toString()),
                              ));
                            }
                            ScaffoldMessenger.of(context)
                                .showSnackBar(const SnackBar(
                              content: Text("Failed to Add User"),
                            ));
                            break;
                          case 403:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Permission Denied"),
                            ));
                            break;
                          default:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context)
                                .showSnackBar(const SnackBar(
                              content: Text("Failed to Add User"),
                            ));
                            break;
                        }
                      }
                    },
                    label: const Text('SAVE',
                        style: TextStyle(
                          height: 1.171875,
                          fontSize: 24.0,
                          fontFamily: 'Roboto',
                          fontWeight: FontWeight.w400,
                          color: Color.fromARGB(255, 0, 0, 0),
                        )),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

That class declares all required variables that will save to the REST API, data binding, database helper initiation, and Form declaration. The form contains the Drowndown Button, TextFormField, and Submit Button. The action on submit will save the data to the REST API then redirect back to the list view. 


Step #9: Create Role CRUD Screen

One of the action buttons in the drawer menu will navigate to the Roles screen that contains a list of roles that added manually from the backend or this Flutter app. Create the new files `lib/screens/rolesscreen.dart`, `lib/screens/roledetailsscreen.dart`, `lib/screens/editrolescreen.dart`, `lib/screens/addrolescreen.dart`, `lib/screens/addpermissionsscreen.dart`, and a folder and file `lib/widgets/rolelistwidget.dart`. We will use the separate list widget to display the list of users. Open and edit `lib/screens/rolesscreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/models/roles.dart';
import 'package:flutter_auth_roleperm/screens/addrolescreen.dart';
import 'package:flutter_auth_roleperm/screens/homescreen.dart';
import 'package:flutter_auth_roleperm/screens/loginscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_auth_roleperm/widgets/rolelistwidget.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart';

class RolesScreen extends StatelessWidget {
  const RolesScreen({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;
  static const String _title = 'Roles';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulRolesWidget(errMsg: errMsg,),
    );
  }
}

class StatefulRolesWidget extends StatefulWidget {
  const StatefulRolesWidget({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;

  @override
  // ignore: no_logic_in_create_state
  State<StatefulRolesWidget> createState() => _StatefulRolesWidget(errMsg: errMsg);
}

class _StatefulRolesWidget extends State<StatefulRolesWidget> {
  _StatefulRolesWidget({required this.errMsg});
  final String errMsg;
  final ApiService api = ApiService();
  late List<Roles> roles = [];

  @override
  void initState() {
    super.initState();

    if (errMsg.isNotEmpty) {
      Future.delayed(Duration.zero, () {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(errMsg),
        ));
      });
    }
  }

  Future<Response> get listRoles async {
    EasyLoading.show();
    return await api.getRoleList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Roles',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () =>
              Navigator.pushReplacement(context, SlideRightRoute(page: const HomeScreen(errMsg: '',))),
        ),
      ),
      body: Center(
      child: FutureBuilder(
        future: listRoles,
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            Response resp = snapshot.data as Response;
            if (resp.statusCode == 200) {
              EasyLoading.dismiss();
              final jsonMap = json.decode(resp.body);
              roles = (jsonMap as List)
                  .map((roleItem) => Roles.fromJson(roleItem))
                  .toList();
              return roles.isNotEmpty
                  ? Padding(
                padding: const EdgeInsets.symmetric(
                    vertical: 10.0, horizontal: 10.0),
                child: RoleListWidget(roles: roles),
              )
                  : const Center(
                child: Padding(
                  padding: EdgeInsets.symmetric(
                      vertical: 10.0, horizontal: 20.0),
                  child: Text(
                    'No roles found',
                    overflow: TextOverflow.visible,
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      height: 1.171875,
                      fontSize: 24.0,
                      fontFamily: 'Roboto',
                      fontWeight: FontWeight.w300,
                      color: Color.fromARGB(255, 255, 255, 255),
                    ),
                  ),
                ),
              );
            } else if (resp.statusCode == 401) {
              EasyLoading.dismiss();
              Future.delayed(Duration.zero, () {
                Navigator.push(
                    context,
                    MaterialPageRoute(
                        builder: (context) => const LoginScreen(errMsg: 'Unauthenticated',)));
              });
            } else if (resp.statusCode == 403) {
              EasyLoading.dismiss();
              Future.delayed(Duration.zero, () {
                Navigator.push(
                    context,
                    MaterialPageRoute(
                        builder: (context) => const HomeScreen(errMsg: 'Permission Denied',)));
              });
            }
          } else if (snapshot.hasError) {
            EasyLoading.dismiss();
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('${snapshot.error}'),
            ));
          }
          return const Center(
            child: Text(''''''),
          );
        },
      ),
    ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
              context,
              MaterialPageRoute(
                  builder: (context) => const AddRoleScreen()));
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
      ),
    );
  }
}

Those screens are similar to users' list screens. Next, open and edit `lib/widgets/rolelistwidget.dart` then add these lines of Dart codes.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/models/roles.dart';
import 'package:flutter_auth_roleperm/screens/roledetailsscreen.dart';

class RoleListWidget extends StatelessWidget {
  final List<Roles> roles;

  const RoleListWidget({Key? key, required this.roles}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: roles.isEmpty ? 0 : roles.length,
      itemBuilder: (BuildContext context, int index) {
        return Padding(
          padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0),
          child: Card(
            color: const Color.fromARGB(255, 252, 142, 54),
            shape: RoundedRectangleBorder(
              side: const BorderSide(
                  color: Color.fromARGB(255, 0, 0, 0), width: 1),
              borderRadius: BorderRadius.circular(10),
            ),
            child: InkWell(
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) =>
                          RoleDetailsScreen(roles: roles[index], errMsg: '',)),
                );
              },
              child: ListTile(
                leading: const Icon(
                  Icons.group_work,
                  size: 48,
                  color: Color.fromARGB(255, 0, 0, 0),
                ),
                title: Text(
                  roles[index].roleName.toString(),
                  style: const TextStyle(
                    fontFamily: 'Roboto',
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
                subtitle: Text(
                  roles[index].roleDescription.toString(),
                  style: const TextStyle(
                    fontFamily: 'Roboto',
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

Next, open and edit `lib/screens/roledetailsscreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/models/roles.dart';
import 'package:flutter_auth_roleperm/screens/addpermissionsscreen.dart';
import 'package:flutter_auth_roleperm/screens/editrolescreen.dart';
import 'package:flutter_auth_roleperm/screens/rolesscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';

class RoleDetailsScreen extends StatelessWidget {
  const RoleDetailsScreen({Key? key, required this.roles, required this.errMsg}) : super(key: key);
  final Roles roles;
  final String errMsg;
  static const String _title = 'Users';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulRoleDetailsWidget(roles: roles, errMsg: errMsg,),
    );
  }
}

class StatefulRoleDetailsWidget extends StatefulWidget {
  const StatefulRoleDetailsWidget({Key? key, required this.roles, required this.errMsg})
      : super(key: key);
  final Roles roles;
  final String errMsg;

  @override
  // ignore: no_logic_in_create_state
  _RoleDetailsWidgetState createState() => _RoleDetailsWidgetState(roles: roles, errMsg: errMsg);
}

class _RoleDetailsWidgetState extends State<StatefulRoleDetailsWidget> {
  _RoleDetailsWidgetState({required this.roles, required this.errMsg});

  final Roles roles;
  final String errMsg;
  final ApiService api = ApiService();

  @override
  void initState() {
    super.initState();

    if (errMsg.isNotEmpty) {
      Future.delayed(Duration.zero, () {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(errMsg),
        ));
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Role Details',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () => Navigator.pushReplacement(
              context, SlideRightRoute(page: const RolesScreen(errMsg: '',))),
        ),
      ),
      body: SingleChildScrollView(
        child: Container(
          padding: const EdgeInsets.all(20.0),
          child: Card(
              child: Container(
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(5.0),
                    color: const Color.fromARGB(255, 252, 142, 54),
                    border: Border.all(color: const Color.fromARGB(255, 0, 0, 0)),
                  ),
                  padding: const EdgeInsets.all(10.0),
                  width: 440,
                  child: Column(
                    children: <Widget>[
                      Container(
                        margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
                        child: Column(
                          children: <Widget>[
                            Text('Role Name:',
                                style: TextStyle(
                                    color: Colors.black.withOpacity(0.8))),
                            Text(widget.roles.roleName.toString(),
                                style: Theme.of(context).textTheme.subtitle1)
                          ],
                        ),
                      ),
                      Container(
                        margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
                        child: Column(
                          children: <Widget>[
                            Text('Role Description:',
                                style: TextStyle(
                                    color: Colors.black.withOpacity(0.8))),
                            Text(widget.roles.roleDescription.toString(),
                                style: Theme.of(context).textTheme.subtitle1)
                          ],
                        ),
                      ),
                      Container(
                        margin: const EdgeInsets.fromLTRB(20, 0, 20, 10),
                        child: Column(
                          children: <Widget>[
                            Text('Permissions:',
                                style: TextStyle(
                                    color: Colors.black.withOpacity(0.8))),
                            Text(widget.roles.permissions.toString(),
                                style: Theme.of(context).textTheme.subtitle1)
                          ],
                        ),
                      ),
                      Container(
                        margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
                        child: Column(
                          children: <Widget>[
                            Padding(
                              padding: const EdgeInsets.symmetric(
                                  horizontal: 30, vertical: 10),
                              child: SizedBox(
                                height: 60.0,
                                width: MediaQuery.of(context).size.width * 1.0,
                                child: ElevatedButton.icon(
                                  icon: const Icon(
                                    Icons.edit,
                                    color: Color.fromARGB(255, 0, 0, 0),
                                    size: 24.0,
                                  ),
                                  style: ButtonStyle(
                                    shape: MaterialStateProperty.all<
                                        RoundedRectangleBorder>(
                                        RoundedRectangleBorder(
                                          borderRadius: BorderRadius.circular(5.0),
                                          side: const BorderSide(
                                              color:
                                              Color.fromARGB(255, 128, 255, 0),
                                              width: 1.0),
                                        )),
                                    backgroundColor: MaterialStateProperty.all<
                                        Color>(
                                        const Color.fromARGB(255, 255, 200, 0)),
                                  ),
                                  onPressed: () async {
                                    Navigator.pushReplacement(
                                        context,
                                        SlideRightRoute(
                                            page: EditRoleScreen(
                                              roles: roles,
                                            )));
                                  },
                                  label: const Text('EDIT',
                                      style: TextStyle(
                                        height: 1.171875,
                                        fontSize: 24.0,
                                        fontFamily: 'Roboto',
                                        fontWeight: FontWeight.w400,
                                        color: Color.fromARGB(255, 0, 0, 0),
                                      )),
                                ),
                              ),
                            ),
                            Padding(
                              padding: const EdgeInsets.symmetric(
                                  horizontal: 30, vertical: 10),
                              child: SizedBox(
                                height: 60.0,
                                width: MediaQuery.of(context).size.width * 1.0,
                                child: ElevatedButton.icon(
                                  icon: const Icon(
                                    Icons.delete,
                                    color: Color.fromARGB(255, 0, 0, 0),
                                    size: 24.0,
                                  ),
                                  style: ButtonStyle(
                                    shape: MaterialStateProperty.all<
                                        RoundedRectangleBorder>(
                                        RoundedRectangleBorder(
                                          borderRadius: BorderRadius.circular(5.0),
                                          side: const BorderSide(
                                              color:
                                              Color.fromARGB(255, 128, 255, 0),
                                              width: 1.0),
                                        )),
                                    backgroundColor: MaterialStateProperty.all<
                                        Color>(
                                        const Color.fromARGB(255, 255, 200, 0)),
                                  ),
                                  onPressed: () async {
                                    _confirmDialog();
                                  },
                                  label: const Text('DELETE',
                                      style: TextStyle(
                                        height: 1.171875,
                                        fontSize: 24.0,
                                        fontFamily: 'Roboto',
                                        fontWeight: FontWeight.w400,
                                        color: Color.fromARGB(255, 0, 0, 0),
                                      )),
                                ),
                              ),
                            ),
                            Padding(
                              padding: const EdgeInsets.symmetric(
                                  horizontal: 30, vertical: 10),
                              child: SizedBox(
                                height: 60.0,
                                width: MediaQuery.of(context).size.width * 1.0,
                                child: ElevatedButton.icon(
                                  icon: const Icon(
                                    Icons.update,
                                    color: Color.fromARGB(255, 0, 0, 0),
                                    size: 24.0,
                                  ),
                                  style: ButtonStyle(
                                    shape: MaterialStateProperty.all<
                                        RoundedRectangleBorder>(
                                        RoundedRectangleBorder(
                                          borderRadius: BorderRadius.circular(5.0),
                                          side: const BorderSide(
                                              color:
                                              Color.fromARGB(255, 128, 255, 0),
                                              width: 1.0),
                                        )),
                                    backgroundColor: MaterialStateProperty.all<
                                        Color>(
                                        const Color.fromARGB(255, 255, 200, 0)),
                                  ),
                                  onPressed: () async {
                                    Navigator.pushReplacement(
                                        context,
                                        SlideRightRoute(
                                            page: AddPermissionsScreen(
                                              role: roles,
                                            )));
                                  },
                                  label: const Text('PERMISSIONS',
                                      style: TextStyle(
                                        height: 1.171875,
                                        fontSize: 24.0,
                                        fontFamily: 'Roboto',
                                        fontWeight: FontWeight.w400,
                                        color: Color.fromARGB(255, 0, 0, 0),
                                      )),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ))),
        ),
      ),
    );
  }

  Future<void> _confirmDialog() async {
    return showDialog<void>(
      context: context,
      barrierDismissible: false, // user must tap button!
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Warning!'),
          content: SingleChildScrollView(
            child: ListBody(
              children: const <Widget>[
                Text('Are you sure want delete this item?'),
              ],
            ),
          ),
          actions: <Widget>[
            ElevatedButton(
              child: const Text('Yes'),
              onPressed: () async {
                EasyLoading.show();
                var res = await api.deleteRole(widget.roles.id.toString());

                switch (res.statusCode) {
                  case 200:
                    EasyLoading.dismiss();
                    Navigator.pushReplacement(
                        context, SlideRightRoute(page: const RolesScreen(errMsg: 'Deleted Successfully',)));
                    break;
                  case 400:
                    EasyLoading.dismiss();
                    var data = jsonDecode(res.body);
                    if (data["msg"] != null) {
                      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                        content: Text(data["msg"].toString()),
                      ));
                    }
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                      content: Text("Delete Failed"),
                    ));
                    break;
                  case 403:
                    EasyLoading.dismiss();
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                      content: Text("Permission Denied"),
                    ));
                    break;
                  default:
                    EasyLoading.dismiss();
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                      content: Text("Delete Failed"),
                    ));
                    break;
                }
              },
            ),
            ElevatedButton(
              child: const Text('No'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}

Next, open and edit `lib/screens/editrolescreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/models/roles.dart';
import 'package:flutter_auth_roleperm/screens/roledetailsscreen.dart';
import 'package:flutter_auth_roleperm/screens/rolesscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';

class EditRoleScreen extends StatelessWidget {
  const EditRoleScreen({Key? key, required this.roles}) : super(key: key);
  final Roles roles;
  static const String _title = 'Edit Role';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulEditRoleWidget(roles: roles),
    );
  }
}

class StatefulEditRoleWidget extends StatefulWidget {
  const StatefulEditRoleWidget({Key? key, required this.roles}) : super(key: key);
  final Roles roles;

  @override
  // ignore: no_logic_in_create_state
  _EditRoleWidgetState createState() => _EditRoleWidgetState(roles: roles);
}

class _EditRoleWidgetState extends State<StatefulEditRoleWidget> {
  _EditRoleWidgetState({required this.roles});
  final Roles roles;
  final ApiService api = ApiService();
  final _editRoleFormKey = GlobalKey<FormState>();
  final _roleNameController = TextEditingController();
  final _roleDescriptionController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _roleNameController.text = roles.roleName.toString();
    _roleDescriptionController.text = roles.roleDescription.toString();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Edit Role',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () =>
              Navigator.pushReplacement(context, SlideRightRoute(page: RoleDetailsScreen(roles: roles, errMsg: '',))),
        ),

      ),
      body: SingleChildScrollView(
        child: Form(
          key: _editRoleFormKey,
          child: Column(
            children: [
              const Padding(
                padding: EdgeInsets.fromLTRB(15, 80, 15, 20),
                child: Text(
                  'Please edit your role name and description',
                  overflow: TextOverflow.visible,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    height: 1.171875,
                    fontSize: 18.0,
                    fontFamily: 'Roboto',
                    fontWeight: FontWeight.w500,
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _roleNameController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your role name';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    labelText: 'Role Name',
                    hintText: 'Role Name',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.group_work,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _roleDescriptionController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your role description';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Role Description',
                    hintText: 'Role Description',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.text_fields,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
                child: SizedBox(
                  height: 60.0,
                  width: MediaQuery.of(context).size.width * 1.0,
                  child: ElevatedButton.icon(
                    icon: const Icon(
                      Icons.login,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24.0,
                    ),
                    style: ButtonStyle(
                      shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                          RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(5.0),
                            side: const BorderSide(color: Color.fromARGB(255, 128, 255, 0), width: 1.0),
                          )),
                      backgroundColor: MaterialStateProperty.all<Color>(
                          const Color.fromARGB(255, 255, 200, 0)),
                    ),
                    onPressed: () async {
                      if (_editRoleFormKey.currentState!.validate()) {
                        _editRoleFormKey.currentState!.save();
                        EasyLoading.show();
                        var res = await api.updateRole(
                            roles.id, _roleNameController.text, _roleDescriptionController.text);

                        switch (res.statusCode) {
                          case 200:
                            EasyLoading.dismiss();
                            Navigator.pushReplacement(
                                context, SlideRightRoute(page: const RolesScreen(errMsg: 'Updated Successfully',)));
                            break;
                          case 400:
                            EasyLoading.dismiss();
                            var data = jsonDecode(res.body);
                            if (data["msg"] != null) {
                              ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                                content: Text(data["msg"].toString()),
                              ));
                            }
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Update Failed"),
                            ));
                            break;
                          case 403:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Permission Denied"),
                            ));
                            break;
                          default:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Update Failed"),
                            ));
                            break;
                        }
                      }
                    },
                    label: const Text('UPDATE',
                        style: TextStyle(
                          height: 1.171875,
                          fontSize: 24.0,
                          fontFamily: 'Roboto',
                          fontWeight: FontWeight.w400,
                          color: Color.fromARGB(255, 0, 0, 0),
                        )),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Next, open and edit `lib/screens/addrolescreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/screens/rolesscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';

class AddRoleScreen extends StatelessWidget {
  const AddRoleScreen({Key? key}) : super(key: key);
  static const String _title = 'Add Role';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: StatefulAddRoleWidget(),
    );
  }
}

class StatefulAddRoleWidget extends StatefulWidget {
  const StatefulAddRoleWidget({Key? key}) : super(key: key);

  @override
  // ignore: no_logic_in_create_state
  _AddRoleWidgetState createState() => _AddRoleWidgetState();
}

class _AddRoleWidgetState extends State<StatefulAddRoleWidget> {
  final ApiService api = ApiService();
  final _addRoleFormKey = GlobalKey<FormState>();
  final _roleNameController = TextEditingController();
  final _roleDescriptionController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Add Role',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () => Navigator.pushReplacement(
              context, SlideRightRoute(page: const RolesScreen(errMsg: ''))),
        ),
      ),
      body: SingleChildScrollView(
        child: Form(
          key: _addRoleFormKey,
          child: Column(
            children: [
              const Padding(
                padding: EdgeInsets.fromLTRB(15, 80, 15, 20),
                child: Text(
                  'Please fill your role name and description',
                  overflow: TextOverflow.visible,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    height: 1.171875,
                    fontSize: 18.0,
                    fontFamily: 'Roboto',
                    fontWeight: FontWeight.w500,
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
              Padding(
                padding:
                const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _roleNameController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your role name';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  obscureText: true,
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    errorStyle:
                    TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    labelText: 'Role Name',
                    hintText: 'Role Name',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.group_work,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding:
                const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _roleDescriptionController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your role description';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    errorStyle:
                    TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Role Description',
                    hintText: 'Role Description',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.text_fields,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding:
                const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
                child: SizedBox(
                  height: 60.0,
                  width: MediaQuery.of(context).size.width * 1.0,
                  child: ElevatedButton.icon(
                    icon: const Icon(
                      Icons.save,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24.0,
                    ),
                    style: ButtonStyle(
                      shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                          RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(5.0),
                            side: const BorderSide(
                                color: Color.fromARGB(255, 128, 255, 0),
                                width: 1.0),
                          )),
                      backgroundColor: MaterialStateProperty.all<Color>(
                          const Color.fromARGB(255, 255, 200, 0)),
                    ),
                    onPressed: () async {
                      if (_addRoleFormKey.currentState!.validate()) {
                        _addRoleFormKey.currentState!.save();
                        EasyLoading.show();
                        var res = await api.addRole(
                            _roleNameController.text,
                            _roleDescriptionController.text);

                        switch (res.statusCode) {
                          case 201:
                            EasyLoading.dismiss();
                            Navigator.pushReplacement(
                                context, SlideRightRoute(page: const RolesScreen(errMsg: 'Role Added Successfully',)));
                            break;
                          case 400:
                            EasyLoading.dismiss();
                            var data = jsonDecode(res.body);
                            if (data["msg"]) {
                              ScaffoldMessenger.of(context)
                                  .showSnackBar(SnackBar(
                                content: Text(data["msg"].toString()),
                              ));
                            }
                            ScaffoldMessenger.of(context)
                                .showSnackBar(const SnackBar(
                              content: Text("Failed to Add Role"),
                            ));
                            break;
                          case 403:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Permission Denied"),
                            ));
                            break;
                          default:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context)
                                .showSnackBar(const SnackBar(
                              content: Text("Failed to Add Role"),
                            ));
                            break;
                        }
                      }
                    },
                    label: const Text('SAVE',
                        style: TextStyle(
                          height: 1.171875,
                          fontSize: 24.0,
                          fontFamily: 'Roboto',
                          fontWeight: FontWeight.w400,
                          color: Color.fromARGB(255, 0, 0, 0),
                        )),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Those screens are similar to users' screens.


Step #10: Create Add Permissions to Role Screen

In the previous Role details screen, there is a button to add some permissions to a role. Also, the dart file was already created in the previous step. Open and edit `lib/screens/addpermissionsscreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/models/permissions.dart';
import 'package:flutter_auth_roleperm/models/roles.dart';
import 'package:flutter_auth_roleperm/screens/loginscreen.dart';
import 'package:flutter_auth_roleperm/screens/roledetailsscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart';

class AddPermissionsScreen extends StatelessWidget {
  const AddPermissionsScreen({Key? key, required this.role}) : super(key: key);
  final Roles role;
  static const String _title = 'Add/Update Permissions';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulAddPermissionWidget(role: role,),
    );
  }
}

class StatefulAddPermissionWidget extends StatefulWidget {
  const StatefulAddPermissionWidget({Key? key, required this.role}) : super(key: key);
  final Roles role;

  @override
  // ignore: no_logic_in_create_state
  _AddPermissionWidgetState createState() => _AddPermissionWidgetState(role: role);
}

class _AddPermissionWidgetState extends State<StatefulAddPermissionWidget> {
  _AddPermissionWidgetState({ required this.role });
  final Roles role;
  final ApiService api = ApiService();
  final _addPermissionFormKey = GlobalKey<FormState>();
  late List<Permissions> permissions = [];

  Future<Response> get listPermissions async {
    EasyLoading.show();
    return await api.getPermissionList();
  }

  bool setValue(int index) {
    bool permVal = false;
    var matchPerm = role.permissions!.where((perm) => perm.id == permissions[index].id);
    if(matchPerm.isNotEmpty) {
      permVal = true;
    }
    return permVal;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Add/Update Permissions',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () => Navigator.pushReplacement(
              context, SlideRightRoute(page: RoleDetailsScreen(roles: role, errMsg: ''))),
        ),
      ),
      body: Center(
        child: FutureBuilder(
          future: listPermissions,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              Response resp = snapshot.data as Response;
              if (resp.statusCode == 200) {
                EasyLoading.dismiss();
                final jsonMap = json.decode(resp.body);
                permissions = (jsonMap as List)
                    .map((roleItem) => Permissions.fromJson(roleItem))
                    .toList();
                return permissions.isNotEmpty
                    ? Padding(
                  padding: const EdgeInsets.symmetric(
                      vertical: 10.0, horizontal: 10.0),
                  child: ListView.builder(
                    itemCount: permissions.isEmpty ? 0 : permissions.length,
                    itemBuilder: (BuildContext context, int index) {
                      return Padding(
                        padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0),
                        child: CheckboxListTile(
                          title: Text(
                            permissions[index].permDescription.toString(),
                            style: const TextStyle(
                              fontFamily: 'Roboto',
                              color: Color.fromARGB(255, 0, 0, 0),
                            ),
                          ),
                          value: setValue(index),
                          controlAffinity: ListTileControlAffinity.leading,
                          onChanged: (bool? newValue) {
                            if (newValue == true) {
                              var matchPerm = role.permissions!.where((perm) => perm.id == permissions[index].id);
                              if(matchPerm.isEmpty) {
                                setState(() {
                                  role.permissions!.add(permissions[index]);
                                });
                              }
                            } else {
                              var matchPerm = role.permissions!.where((perm) => perm.id == permissions[index].id);
                              if(matchPerm.isNotEmpty) {
                                setState(() {
                                  role.permissions!.removeWhere((item) => item.id == permissions[index].id);
                                });
                              }
                            }
                          },
                        ),
                      );
                    },
                  ),
                )
                    : const Center(
                  child: Padding(
                    padding: EdgeInsets.symmetric(
                        vertical: 10.0, horizontal: 20.0),
                    child: Text(
                      'No roles found',
                      overflow: TextOverflow.visible,
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 255, 255, 255),
                      ),
                    ),
                  ),
                );
              } else if (resp.statusCode == 401) {
                EasyLoading.dismiss();
                Future.delayed(Duration.zero, () {
                  Navigator.push(
                      context,
                      MaterialPageRoute(
                          builder: (context) => const LoginScreen(errMsg: 'Unauthenticated',)));
                });
              } else if (resp.statusCode == 403) {
                EasyLoading.dismiss();
                Future.delayed(Duration.zero, () {
                  Navigator.push(
                      context,
                      MaterialPageRoute(
                          builder: (context) => RoleDetailsScreen(roles: role, errMsg: 'Permission Denied')));
                });
              }
            } else if (snapshot.hasError) {
              EasyLoading.dismiss();
              ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                content: Text('${snapshot.error}'),
              ));
            }
            return const Center(
              child: Text(''''''),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          EasyLoading.show();
          List<int?> permissionsInt = role.permissions!.map((e) => e.id).toList();
          var res = await api.addPermissiontoRole(role.id, permissionsInt);

          switch (res.statusCode) {
            case 200:
              EasyLoading.dismiss();
              Navigator.pushReplacement(
                  context, SlideRightRoute(page: RoleDetailsScreen(roles: role, errMsg: 'Permission added to role successfully')));
              break;
            case 400:
              EasyLoading.dismiss();
              var data = jsonDecode(res.body);
              ScaffoldMessenger.of(context)
                  .showSnackBar(const SnackBar(
                content: Text("Failed to Add Permissions"),
              ));
              break;
            case 403:
              EasyLoading.dismiss();
              ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                content: Text("Permission Denied"),
              ));
              break;
            default:
              EasyLoading.dismiss();
              ScaffoldMessenger.of(context)
                  .showSnackBar(const SnackBar(
                content: Text("Failed to Add Permissions"),
              ));
              break;
          }
        },
        tooltip: 'Increment',
        child: const Icon(Icons.save),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
      ),
    );
  }
}

This time we are using the list of CheckBoxTile generated from the list of permissions that get from the REST API. The selected or checked CheckBoxTile set from the default permissions that are owned by a role.


Step #11: Create Products CRUD Screen

One of the action buttons in the drawer menu will navigate to the Products screen that contains a list of products that were added manually from the backend or this Flutter app. Create the new files `lib/screens/productsscreen.dart`, `lib/screens/productdetailsscreen.dart`, `lib/screens/editproductscreen.dart`, `lib/screens/addproductscreen.dart`, and a folder and file `lib/widgets/productlistwidget.dart`. We will use the separate list widget to display the list of users. Open and edit `lib/screens/productsscreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/models/products.dart';
import 'package:flutter_auth_roleperm/screens/addproductscreen.dart';
import 'package:flutter_auth_roleperm/screens/homescreen.dart';
import 'package:flutter_auth_roleperm/screens/loginscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_auth_roleperm/widgets/productlistwidget.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart';

class ProductsScreen extends StatelessWidget {
  const ProductsScreen({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;
  static const String _title = 'Products';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulProductsWidget(errMsg: errMsg,),
    );
  }
}

class StatefulProductsWidget extends StatefulWidget {
  const StatefulProductsWidget({Key? key, required this.errMsg}) : super(key: key);
  final String errMsg;

  @override
  // ignore: no_logic_in_create_state
  State<StatefulProductsWidget> createState() => _StatefulProductsWidget(errMsg: errMsg);
}

class _StatefulProductsWidget extends State<StatefulProductsWidget> {
  _StatefulProductsWidget({required this.errMsg});
  final String errMsg;
  final ApiService api = ApiService();
  late List<Products> products = [];

  @override
  void initState() {
    super.initState();

    if (errMsg.isNotEmpty) {
      Future.delayed(Duration.zero, () {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(errMsg),
        ));
      });
    }
  }

  Future<Response> get listProducts async {
    EasyLoading.show();
    return await api.getProductList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Products',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () =>
              Navigator.pushReplacement(context, SlideRightRoute(page: const HomeScreen(errMsg: '',))),
        ),
      ),
      body: Center(
        child: FutureBuilder(
          future: listProducts,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              Response resp = snapshot.data as Response;
              if (resp.statusCode == 200) {
                EasyLoading.dismiss();
                final jsonMap = json.decode(resp.body);
                products = (jsonMap as List)
                    .map((prodItem) => Products.fromJson(prodItem))
                    .toList();
                return products.isNotEmpty
                    ? Padding(
                  padding: const EdgeInsets.symmetric(
                      vertical: 10.0, horizontal: 10.0),
                  child: ProductListWidget(products: products),
                )
                    : const Center(
                  child: Padding(
                    padding: EdgeInsets.symmetric(
                        vertical: 10.0, horizontal: 20.0),
                    child: Text(
                      'No products found',
                      overflow: TextOverflow.visible,
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 255, 255, 255),
                      ),
                    ),
                  ),
                );
              } else if (resp.statusCode == 401) {
                EasyLoading.dismiss();
                Future.delayed(Duration.zero, () {
                  Navigator.push(
                      context,
                      MaterialPageRoute(
                          builder: (context) => const LoginScreen(errMsg: 'Unauthenticated',)));
                });
              } else if (resp.statusCode == 403) {
                EasyLoading.dismiss();
                Future.delayed(Duration.zero, () {
                  Navigator.push(
                      context,
                      MaterialPageRoute(
                          builder: (context) => const HomeScreen(errMsg: 'Permission Denied',)));
                });
              }
            } else if (snapshot.hasError) {
              EasyLoading.dismiss();
              ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                content: Text('${snapshot.error}'),
              ));
            }
            return const Center(
              child: Text(''''''),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
              context,
              MaterialPageRoute(
                  builder: (context) => const AddProductScreen()));
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
      ),
    );
  }
}

Next, open and edit `lib/widgets/productlistwidget.dart` then add these lines of Dart codes.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/models/products.dart';
import 'package:flutter_auth_roleperm/screens/productdetailsscreen.dart';

class ProductListWidget extends StatelessWidget {
  final List<Products> products;

  const ProductListWidget({Key? key, required this.products}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: products.isEmpty ? 0 : products.length,
      itemBuilder: (BuildContext context, int index) {
        return Padding(
          padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0),
          child: Card(
            color: const Color.fromARGB(255, 252, 142, 54),
            shape: RoundedRectangleBorder(
              side: const BorderSide(
                  color: Color.fromARGB(255, 0, 0, 0), width: 1),
              borderRadius: BorderRadius.circular(10),
            ),
            child: InkWell(
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) =>
                          ProductDetailsScreen(products: products[index])),
                );
              },
              child: ListTile(
                leading: const Icon(
                  Icons.group_work,
                  size: 48,
                  color: Color.fromARGB(255, 0, 0, 0),
                ),
                title: Text(
                  products[index].prodName.toString(),
                  style: const TextStyle(
                    fontFamily: 'Roboto',
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
                subtitle: Text(
                  products[index].prodDescription.toString(),
                  style: const TextStyle(
                    fontFamily: 'Roboto',
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

Next, open and edit `lib/screens/productdetailsscreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/models/products.dart';
import 'package:flutter_auth_roleperm/screens/editproductscreen.dart';
import 'package:flutter_auth_roleperm/screens/productsscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:intl/intl.dart';

class ProductDetailsScreen extends StatelessWidget {
  const ProductDetailsScreen({Key? key, required this.products}) : super(key: key);
  final Products products;
  static const String _title = 'Users';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulProductDetailsWidget(products: products),
    );
  }
}

class StatefulProductDetailsWidget extends StatefulWidget {
  const StatefulProductDetailsWidget({Key? key, required this.products})
      : super(key: key);
  final Products products;

  @override
  // ignore: no_logic_in_create_state
  _ProductDetailsWidgetState createState() => _ProductDetailsWidgetState(products: products);
}

class _ProductDetailsWidgetState extends State<StatefulProductDetailsWidget> {
  _ProductDetailsWidgetState({required this.products});

  final Products products;
  final ApiService api = ApiService();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Product Details',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () => Navigator.pushReplacement(
              context, SlideRightRoute(page: const ProductsScreen(errMsg: '',))),
        ),
      ),
      body: SingleChildScrollView(
        child: Container(
          padding: const EdgeInsets.all(20.0),
          child: Card(
              child: Container(
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(5.0),
                    color: const Color.fromARGB(255, 252, 142, 54),
                    border: Border.all(color: const Color.fromARGB(255, 0, 0, 0)),
                  ),
                  padding: const EdgeInsets.all(10.0),
                  width: 440,
                  child: Column(
                    children: <Widget>[
                      Container(
                        margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
                        child: Column(
                          children: <Widget>[
                            Text('Product Name:',
                                style: TextStyle(
                                    color: Colors.black.withOpacity(0.8))),
                            Text(widget.products.prodName.toString(),
                                style: Theme.of(context).textTheme.subtitle1)
                          ],
                        ),
                      ),
                      Container(
                        margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
                        child: Column(
                          children: <Widget>[
                            Text('Product Description:',
                                style: TextStyle(
                                    color: Colors.black.withOpacity(0.8))),
                            Text(widget.products.prodDescription.toString(),
                                style: Theme.of(context).textTheme.subtitle1)
                          ],
                        ),
                      ),
                      Container(
                        margin: const EdgeInsets.fromLTRB(20, 0, 20, 10),
                        child: Column(
                          children: <Widget>[
                            Text('Product Image:',
                                style: TextStyle(
                                    color: Colors.black.withOpacity(0.8))),
                            Image.network(widget.products.prodImage.toString()),
                          ],
                        ),
                      ),
                      Container(
                        margin: const EdgeInsets.fromLTRB(20, 0, 20, 10),
                        child: Column(
                          children: <Widget>[
                            Text('Product Price:',
                                style: TextStyle(
                                    color: Colors.black.withOpacity(0.8))),
                            Text(NumberFormat.currency(locale: 'id', symbol: 'Rp')
                                .format(widget.products.prodPrice),
                                style: Theme.of(context).textTheme.subtitle1)
                          ],
                        ),
                      ),
                      Container(
                        margin: const EdgeInsets.fromLTRB(0, 0, 0, 10),
                        child: Column(
                          children: <Widget>[
                            Padding(
                              padding: const EdgeInsets.symmetric(
                                  horizontal: 30, vertical: 10),
                              child: SizedBox(
                                height: 60.0,
                                width: MediaQuery.of(context).size.width * 1.0,
                                child: ElevatedButton.icon(
                                  icon: const Icon(
                                    Icons.edit,
                                    color: Color.fromARGB(255, 0, 0, 0),
                                    size: 24.0,
                                  ),
                                  style: ButtonStyle(
                                    shape: MaterialStateProperty.all<
                                        RoundedRectangleBorder>(
                                        RoundedRectangleBorder(
                                          borderRadius: BorderRadius.circular(5.0),
                                          side: const BorderSide(
                                              color:
                                              Color.fromARGB(255, 128, 255, 0),
                                              width: 1.0),
                                        )),
                                    backgroundColor: MaterialStateProperty.all<
                                        Color>(
                                        const Color.fromARGB(255, 255, 200, 0)),
                                  ),
                                  onPressed: () async {
                                    Navigator.pushReplacement(
                                        context,
                                        SlideRightRoute(
                                            page: EditProductScreen(
                                              products: products,
                                            )));
                                  },
                                  label: const Text('EDIT',
                                      style: TextStyle(
                                        height: 1.171875,
                                        fontSize: 24.0,
                                        fontFamily: 'Roboto',
                                        fontWeight: FontWeight.w400,
                                        color: Color.fromARGB(255, 0, 0, 0),
                                      )),
                                ),
                              ),
                            ),
                            Padding(
                              padding: const EdgeInsets.symmetric(
                                  horizontal: 30, vertical: 10),
                              child: SizedBox(
                                height: 60.0,
                                width: MediaQuery.of(context).size.width * 1.0,
                                child: ElevatedButton.icon(
                                  icon: const Icon(
                                    Icons.delete,
                                    color: Color.fromARGB(255, 0, 0, 0),
                                    size: 24.0,
                                  ),
                                  style: ButtonStyle(
                                    shape: MaterialStateProperty.all<
                                        RoundedRectangleBorder>(
                                        RoundedRectangleBorder(
                                          borderRadius: BorderRadius.circular(5.0),
                                          side: const BorderSide(
                                              color:
                                              Color.fromARGB(255, 128, 255, 0),
                                              width: 1.0),
                                        )),
                                    backgroundColor: MaterialStateProperty.all<
                                        Color>(
                                        const Color.fromARGB(255, 255, 200, 0)),
                                  ),
                                  onPressed: () async {
                                    _confirmDialog();
                                  },
                                  label: const Text('DELETE',
                                      style: TextStyle(
                                        height: 1.171875,
                                        fontSize: 24.0,
                                        fontFamily: 'Roboto',
                                        fontWeight: FontWeight.w400,
                                        color: Color.fromARGB(255, 0, 0, 0),
                                      )),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ))),
        ),
      ),
    );
  }

  Future<void> _confirmDialog() async {
    return showDialog<void>(
      context: context,
      barrierDismissible: false, // user must tap button!
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Warning!'),
          content: SingleChildScrollView(
            child: ListBody(
              children: const <Widget>[
                Text('Are you sure want delete this item?'),
              ],
            ),
          ),
          actions: <Widget>[
            ElevatedButton(
              child: const Text('Yes'),
              onPressed: () async {
                EasyLoading.show();
                var res = await api.deleteProduct(widget.products.id.toString());

                switch (res.statusCode) {
                  case 200:
                    EasyLoading.dismiss();
                    Navigator.pushReplacement(
                        context, SlideRightRoute(page: const ProductsScreen(errMsg: 'Deleted Successfully',)));
                    break;
                  case 400:
                    EasyLoading.dismiss();
                    var data = jsonDecode(res.body);
                    if (data["msg"] != null) {
                      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                        content: Text(data["msg"].toString()),
                      ));
                    }
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                      content: Text("Delete Failed"),
                    ));
                    break;
                  case 403:
                    EasyLoading.dismiss();
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                      content: Text("Permission Denied"),
                    ));
                    break;
                  default:
                    EasyLoading.dismiss();
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                      content: Text("Delete Failed"),
                    ));
                    break;
                }
              },
            ),
            ElevatedButton(
              child: const Text('No'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}

Next, open and edit `lib/screens/editproductscreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/models/products.dart';
import 'package:flutter_auth_roleperm/screens/productdetailsscreen.dart';
import 'package:flutter_auth_roleperm/screens/productsscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';

class EditProductScreen extends StatelessWidget {
  const EditProductScreen({Key? key, required this.products}) : super(key: key);
  final Products products;
  static const String _title = 'Edit Product';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: StatefulEditProductWidget(products: products),
    );
  }
}

class StatefulEditProductWidget extends StatefulWidget {
  const StatefulEditProductWidget({Key? key, required this.products}) : super(key: key);
  final Products products;

  @override
  // ignore: no_logic_in_create_state
  _EditProductWidgetState createState() => _EditProductWidgetState(products: products);
}

class _EditProductWidgetState extends State<StatefulEditProductWidget> {
  _EditProductWidgetState({required this.products});
  final Products products;
  final ApiService api = ApiService();
  final _editProductFormKey = GlobalKey<FormState>();
  final _productNameController = TextEditingController();
  final _productDescriptionController = TextEditingController();
  final _productImageController = TextEditingController();
  final _productPriceController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _productNameController.text = products.prodName.toString();
    _productDescriptionController.text = products.prodDescription.toString();
    _productImageController.text = products.prodImage.toString();
    _productPriceController.text = products.prodPrice.toString();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Edit Product',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () =>
              Navigator.pushReplacement(context, SlideRightRoute(page: ProductDetailsScreen(products: products))),
        ),

      ),
      body: SingleChildScrollView(
        child: Form(
          key: _editProductFormKey,
          child: Column(
            children: [
              const Padding(
                padding: EdgeInsets.fromLTRB(15, 80, 15, 20),
                child: Text(
                  'Please edit your product name, description, image, and price',
                  overflow: TextOverflow.visible,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    height: 1.171875,
                    fontSize: 18.0,
                    fontFamily: 'Roboto',
                    fontWeight: FontWeight.w500,
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _productNameController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your product name';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    labelText: 'Product Name',
                    hintText: 'Product Name',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.text_format,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _productDescriptionController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your product description';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Product Description',
                    hintText: 'Product Description',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.text_fields,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _productImageController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your product image URL';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.url,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Product Image URL',
                    hintText: 'Product Image URL',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.image,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _productPriceController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your product price';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.number,
                  decoration: const InputDecoration(
                    errorStyle: TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Product Price',
                    hintText: 'Product Price',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.monetization_on,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
                child: SizedBox(
                  height: 60.0,
                  width: MediaQuery.of(context).size.width * 1.0,
                  child: ElevatedButton.icon(
                    icon: const Icon(
                      Icons.login,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24.0,
                    ),
                    style: ButtonStyle(
                      shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                          RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(5.0),
                            side: const BorderSide(color: Color.fromARGB(255, 128, 255, 0), width: 1.0),
                          )),
                      backgroundColor: MaterialStateProperty.all<Color>(
                          const Color.fromARGB(255, 255, 200, 0)),
                    ),
                    onPressed: () async {
                      if (_editProductFormKey.currentState!.validate()) {
                        _editProductFormKey.currentState!.save();
                        EasyLoading.show();
                        var res = await api.updateProduct(
                            products.id, _productNameController.text, _productDescriptionController.text, _productImageController.text, _productPriceController.text);

                        switch (res.statusCode) {
                          case 200:
                            EasyLoading.dismiss();
                            Navigator.pushReplacement(
                                context, SlideRightRoute(page: const ProductsScreen(errMsg: 'Updated Successfully',)));
                            break;
                          case 400:
                            EasyLoading.dismiss();
                            var data = jsonDecode(res.body);
                            if (data["msg"] != null) {
                              ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                                content: Text(data["msg"].toString()),
                              ));
                            }
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Update Failed"),
                            ));
                            break;
                          case 403:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Permission Denied"),
                            ));
                            break;
                          default:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Update Failed"),
                            ));
                            break;
                        }
                      }
                    },
                    label: const Text('UPDATE',
                        style: TextStyle(
                          height: 1.171875,
                          fontSize: 24.0,
                          fontFamily: 'Roboto',
                          fontWeight: FontWeight.w400,
                          color: Color.fromARGB(255, 0, 0, 0),
                        )),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Next, open and edit `lib/screens/addproductscreen.dart` then add these lines of Dart codes.

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth_roleperm/helpers/sliderightroute.dart';
import 'package:flutter_auth_roleperm/screens/productsscreen.dart';
import 'package:flutter_auth_roleperm/services/api_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';

class AddProductScreen extends StatelessWidget {
  const AddProductScreen({Key? key}) : super(key: key);
  static const String _title = 'Add Product';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: StatefulAddProductWidget(),
    );
  }
}

class StatefulAddProductWidget extends StatefulWidget {
  const StatefulAddProductWidget({Key? key}) : super(key: key);

  @override
  // ignore: no_logic_in_create_state
  _AddProductWidgetState createState() => _AddProductWidgetState();
}

class _AddProductWidgetState extends State<StatefulAddProductWidget> {
  final ApiService api = ApiService();
  final _addProductFormKey = GlobalKey<FormState>();
  final _productNameController = TextEditingController();
  final _productDescriptionController = TextEditingController();
  final _productImageController = TextEditingController();
  final _productPriceController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 252, 142, 54),
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color.fromARGB(255, 26, 255, 1)),
        title: const Text(
          'Add Product',
          style: TextStyle(
            height: 1.171875,
            fontSize: 18.0,
            fontFamily: 'Roboto Condensed',
            fontWeight: FontWeight.w500,
            color: Color.fromARGB(255, 26, 255, 1),
          ),
        ),
        backgroundColor: const Color.fromARGB(255, 0, 0, 0),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back,
              color: Color.fromARGB(255, 26, 255, 1)),
          onPressed: () => Navigator.pushReplacement(
              context, SlideRightRoute(page: const ProductsScreen(errMsg: ''))),
        ),
      ),
      body: SingleChildScrollView(
        child: Form(
          key: _addProductFormKey,
          child: Column(
            children: [
              const Padding(
                padding: EdgeInsets.fromLTRB(15, 80, 15, 20),
                child: Text(
                  'Please fill your product name, description, image, and price',
                  overflow: TextOverflow.visible,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    height: 1.171875,
                    fontSize: 18.0,
                    fontFamily: 'Roboto',
                    fontWeight: FontWeight.w500,
                    color: Color.fromARGB(255, 0, 0, 0),
                  ),
                ),
              ),
              Padding(
                padding:
                const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _productNameController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your product name';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    errorStyle:
                    TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    labelText: 'Product Name',
                    hintText: 'Product Name',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.text_format,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding:
                const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _productDescriptionController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your product description';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.text,
                  decoration: const InputDecoration(
                    errorStyle:
                    TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Product Description',
                    hintText: 'Product Description',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.text_fields,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding:
                const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _productImageController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your product image URL';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.url,
                  decoration: const InputDecoration(
                    errorStyle:
                    TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Product Image URL',
                    hintText: 'Product Image URL',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.image,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding:
                const EdgeInsets.symmetric(horizontal: 30, vertical: 5),
                child: TextFormField(
                  controller: _productPriceController,
                  validator: (value) {
                    if (value!.isEmpty) {
                      return 'Please enter your product price';
                    }
                    return null;
                  },
                  onChanged: (value) {},
                  autocorrect: true,
                  keyboardType: TextInputType.number,
                  decoration: const InputDecoration(
                    errorStyle:
                    TextStyle(color: Color.fromARGB(255, 26, 255, 1)),
                    fillColor: Color.fromARGB(255, 0, 0, 0),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 128, 255, 0), width: 1),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(5.0)),
                      borderSide: BorderSide(
                          color: Color.fromARGB(255, 235, 235, 235), width: 1),
                    ),
                    labelText: 'Product Price',
                    hintText: 'Product Price',
                    prefixIcon: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
                      child: Icon(
                        Icons.monetization_on,
                        color: Color.fromARGB(255, 128, 255, 0),
                        size: 24,
                      ),
                    ),
                    labelStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    hintStyle: TextStyle(
                        height: 1.171875,
                        fontSize: 24.0,
                        fontFamily: 'Roboto',
                        fontWeight: FontWeight.w300,
                        color: Color.fromARGB(255, 128, 255, 0)),
                    filled: true,
                  ),
                  style: const TextStyle(
                      color: Color.fromARGB(255, 128, 255, 0), fontSize: 24.0),
                ),
              ),
              Padding(
                padding:
                const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
                child: SizedBox(
                  height: 60.0,
                  width: MediaQuery.of(context).size.width * 1.0,
                  child: ElevatedButton.icon(
                    icon: const Icon(
                      Icons.save,
                      color: Color.fromARGB(255, 0, 0, 0),
                      size: 24.0,
                    ),
                    style: ButtonStyle(
                      shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                          RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(5.0),
                            side: const BorderSide(
                                color: Color.fromARGB(255, 128, 255, 0),
                                width: 1.0),
                          )),
                      backgroundColor: MaterialStateProperty.all<Color>(
                          const Color.fromARGB(255, 255, 200, 0)),
                    ),
                    onPressed: () async {
                      if (_addProductFormKey.currentState!.validate()) {
                        _addProductFormKey.currentState!.save();
                        EasyLoading.show();
                        var res = await api.addProduct(
                            _productNameController.text,
                            _productDescriptionController.text,
                            _productImageController.text,
                            _productPriceController.text);

                        switch (res.statusCode) {
                          case 201:
                            EasyLoading.dismiss();
                            Navigator.pushReplacement(
                                context, SlideRightRoute(page: const ProductsScreen(errMsg: 'Product Added Successfully',)));
                            break;
                          case 400:
                            EasyLoading.dismiss();
                            var data = jsonDecode(res.body);
                            if (data["msg"]) {
                              ScaffoldMessenger.of(context)
                                  .showSnackBar(SnackBar(
                                content: Text(data["msg"].toString()),
                              ));
                            }
                            ScaffoldMessenger.of(context)
                                .showSnackBar(const SnackBar(
                              content: Text("Failed to Add Product"),
                            ));
                            break;
                          case 403:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                              content: Text("Permission Denied"),
                            ));
                            break;
                          default:
                            EasyLoading.dismiss();
                            ScaffoldMessenger.of(context)
                                .showSnackBar(const SnackBar(
                              content: Text("Failed to Add Product"),
                            ));
                            break;
                        }
                      }
                    },
                    label: const Text('SAVE',
                        style: TextStyle(
                          height: 1.171875,
                          fontSize: 24.0,
                          fontFamily: 'Roboto',
                          fontWeight: FontWeight.w400,
                          color: Color.fromARGB(255, 0, 0, 0),
                        )),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

All of those screens are similar to users' screens.


Step #12: Run and Test Flutter Application to Android and iOS Devices

Before running and testing Flutter Application to Android or iOS Devices or Simulators, run the Node-Express authentication with role-permissions first. You can read this article to prepare and run that REST API server. So, at the end, we just run this Node-Express application using this command.

nodemon

Make sure the MySQL/MariaDB server is running before running that command. To run this Flutter apps to Android, simply click the play button in the Android Studio toolbar. Make sure your Android device, iOS device, or Simulator is connected to your computer and appears in Android Studio Toolbar.

Flutter Tutorial: Login, Role, and Permissions - 11

Here the working Flutter apps on iPhone 12 simulator look like.

Flutter Tutorial: Login, Role, and Permissions - 12

That it's, the Flutter Tutorial: Login, Role, and Permissions. You can get the full source code from our GitHub.

That just the basic. If you need more deep learning about Flutter, Dart, or related you can take the following cheap course:

Thanks!