How to Build an OTP Verification Screen in Flutter with GetX: Complete Guide with Beautified UI

Flutter OTP Verification with Pin Code Field and Resend Button Using GetX


Creating an OTP (One-Time Password) verification screen is a crucial part of many apps for security purposes. In this post, we’ll guide you through setting up a modern OTP verification screen in Flutter using GetX for state management. We'll also provide a beautiful UI that resembles the design shared.


Step 1: Add Dependencies

Add the necessary dependencies in your pubspec.yaml file:

 dependencies:
  flutter:
    sdk: flutter
  get: ^4.6.5
  pin_code_fields: ^7.4.0

Run flutter pub get to fetch the packages.


Step 2: Create the OTP Controller with GetX

We’ll create a controller that manages the timer, OTP input, and validation.

otp_controller.dart

 import 'package:get/get.dart';
 import 'package:flutter/material.dart';
 import 'dart:async';

 class OTPController extends GetxController {
  var isCodeExpired = false.obs;
  var otp = ''.obs;
  var remainingTime = 60.obs; // 1 minute in seconds
  Timer? timer;
  TextEditingController otpController = TextEditingController(); // Create a TextEditingController

  @override
  void onInit() {
    startTimer();
    super.onInit();
  }

  void startTimer() {
    timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (remainingTime.value > 0) {
        remainingTime.value--;
      } else {
        isCodeExpired.value = true;
        timer.cancel();
      }
    });
  }

  void resendCode() {
    remainingTime.value = 60; // Reset timer to 60 seconds
    isCodeExpired.value = false;
    otp.value = ''; // Clear the OTP field in the controller
    otpController.clear(); // Clear the text fields in PinCodeTextField
    startTimer(); // Restart the timer
    update(); // Notify UI to update
  }

  void onOTPComplete(String value) {
    otp.value = value;
  }

  @override
  void onClose() {
    timer?.cancel();
    otpController.dispose(); // Dispose of the controller
    super.onClose();
   }
 }

Step 3: Create the Beautified OTP Verification UI

We'll design the UI similar to the one you shared with a countdown timer, input fields for the OTP, and a "Resend Code" option.

otp_view.dart

 import 'package:flutter/material.dart';
 import 'package:flutter_svg/svg.dart';
 import 'package:get/get.dart';
 import 'package:getx_tutorials/widgets/custom_button/custom_button.dart';
 import 'package:google_fonts/google_fonts.dart';
 import 'package:pin_code_fields/pin_code_fields.dart';
 import 'package:sizer/sizer.dart';
 import '../widgets/app_color/app_color.dart';
 import '../widgets/app_image/app_image.dart';
 import 'otp_controller.dart';

 class OtpVerificationScreen extends GetView {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            SvgPicture.asset(
              AppImages.scanIcon,
              height: 65.0, // Adjust the size of the icon if necessary
              width: 65.0,
            ), // Add your logo here
            const SizedBox(height: 20),
            Text(
              'OTP Verification',
              style: GoogleFonts.poppins(
                  fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 10),
            Text(
              'We have sent an OTP on given number +91 2224 555 333',
              textAlign: TextAlign.center,
              style: GoogleFonts.poppins(fontSize: 16, color: Colors.grey),
            ),
            const SizedBox(height: 20),
            Obx(() {
              return Text(
                controller.isCodeExpired.value
                    ? "Your otp has expired please resend"
                    : "00:${controller.remainingTime.value.toString().padLeft(2, '0')}",
                style: GoogleFonts.poppins(
                  color: controller.isCodeExpired.value
                      ? AppColors.appColor
                      : AppColors.appColor,
                  fontSize: 18,
                ),
              );
            }),
            SizedBox(height: 4.h),
            PinCodeTextField(
              appContext: context,
              length: 4,
              onChanged: (value) {
                controller.update();
              },
              onCompleted: controller.onOTPComplete,
              controller: controller.otpController,
              autoDismissKeyboard: true,
              enablePinAutofill: true,
              pinTheme: PinTheme(
                shape: PinCodeFieldShape.box,
                borderRadius: BorderRadius.circular(15),
                fieldHeight: 75,
                fieldWidth: 75,
                activeColor: AppColors.appColor,
                activeFillColor: AppColors.appbarColor,
                selectedColor: AppColors.appColor,
                selectedFillColor: AppColors.btnColor,
                inactiveColor: Colors.grey,
              ),
            ),
            const SizedBox(height: 20),
            const SizedBox(height: 20),
            Obx(
              () => CustomButton(
                onPressed: controller.resendCode,
                backgroundColor: controller.otp.value.length == 4
                    ? AppColors.appColor
                    : controller.isCodeExpired.value
                        ? AppColors.appColor
                        : AppColors.btnColor,
                text:
                    controller.isCodeExpired.value ? "Send Code Again" : 'Next',
                image: AppImages.rightArrow,
                imageColor: controller.isCodeExpired.value
                    ? AppColors.white
                    : controller.otp.value.length == 4
                        ? AppColors.whiteColor
                        : AppColors.optionIconColor,
                color: controller.isCodeExpired.value
                    ? AppColors.white
                    : controller.otp.value.length == 4
                        ? AppColors.whiteColor
                        : AppColors.optionIconColor,
               ),
             )
           ],
         ),
       ),
     );
   }
 }

Step 4: Binding the Controller

Bind the OTPController to the view using GetX bindings.

otp_binding.dart

 import 'package:get/get.dart';
 import 'otp_controller.dart';

 class OtpVerificationBindings extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => OTPController());
   }
 }

Step 5: Main Application Setup

In your main.dart, configure the routes and bindings using GetX.

main.dart

 import 'package:flutter/material.dart';
 import 'package:get/get_navigation/src/root/get_material_app.dart';
 import 'package:getx_tutorials/widgets/app_color/app_color.dart';
 import 'package:getx_tutorials/widgets/app_routes/app_routes.dart';
 import 'package:responsive_framework/responsive_wrapper.dart';
 import 'package:responsive_framework/utils/scroll_behavior.dart';
 import 'package:sizer/sizer.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    Map color = {
      50: const Color(0xFF7DEF83),
      100: const Color(0xFF7DEF83),
      200: const Color(0xFF7DEF83),
      300: const Color(0xFF7DEF83),
      400: const Color(0xFF7DEF83),
      500: const Color(0xFF7DEF83),
      600: const Color(0xFF7DEF83),
      700: const Color(0xFF7DEF83),
      800: const Color(0xFF7DEF83),
      900: const Color(0xFF7DEF83),
    };
    return Sizer(
      builder: (context, orientation, deviceType) {
        return GetMaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'EV Assert',
          theme: ThemeData(
            primarySwatch: MaterialColor(0xFFA4573B, color),
            splashColor: AppColors.appColor,
            highlightColor: AppColors.appColor,
          ),
          // home:  OnboardingScreen(),
          builder: (context, child) => ResponsiveWrapper.builder(
              BouncingScrollWrapper.builder(context, child!),
              maxWidth: 1400,
              minWidth: 450,
              defaultScale: true,
              breakpoints: [
                const ResponsiveBreakpoint.resize(450, name: MOBILE),
                const ResponsiveBreakpoint.autoScale(800, name: TABLET),
                const ResponsiveBreakpoint.autoScale(1000, name: TABLET),
                const ResponsiveBreakpoint.resize(1200, name: DESKTOP),
                const ResponsiveBreakpoint.autoScale(2460, name: "4K"),
              ],
              background: Container(color: const Color(0xFFF5F5F5))),
          // initialBinding: BindingsBuilder(() {
          //   printAction("Data coming here");
          // }),
          initialRoute: AppRoutes.otpScreen,
          getPages: AppRoutes.pages,
         );
       },
     );
 
   }
 }

This complete example demonstrates how to implement a functional and beautiful OTP verification screen in Flutter using GetX. The interface includes features like a countdown timer, OTP input field, and the ability to resend the code if it expires.

By following this guide, you can easily integrate a professional OTP verification system into your Flutter app with a modern and clean UI.