flutter_smart_otp 1.0.0
flutter_smart_otp: ^1.0.0 copied to clipboard
A highly customizable, production-ready OTP / PIN input field for Flutter with auto-focus, paste support, validation, RTL/LTR support, and rich styling options.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_otp/flutter_smart_otp.dart';
void main() {
runApp(const SmartOtpExampleApp());
}
/// Root widget of the example application.
class SmartOtpExampleApp extends StatelessWidget {
/// Creates the example app.
const SmartOtpExampleApp({super.key});
@override
Widget build(BuildContext context) {
final baseScheme = ColorScheme.fromSeed(seedColor: const Color(0xFF2962FF));
return MaterialApp(
title: 'flutter_smart_otp demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: baseScheme,
scaffoldBackgroundColor: const Color(0xFFF5F7FB),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
elevation: 0,
foregroundColor: Colors.black87,
),
),
home: const OtpShowcasePage(),
);
}
}
/// A page that showcases every capability of [SmartOtpField] inside
/// individually explained cards.
class OtpShowcasePage extends StatefulWidget {
/// Creates the showcase page.
const OtpShowcasePage({super.key});
@override
State<OtpShowcasePage> createState() => _OtpShowcasePageState();
}
class _OtpShowcasePageState extends State<OtpShowcasePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('flutter_smart_otp'),
centerTitle: false,
),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
children: const [
_SectionHeader(
title: 'Basic verification',
subtitle:
'Default 4-digit field with auto-focus and completion callback.',
),
_BasicDemoCard(),
SizedBox(height: 24),
_SectionHeader(
title: 'Custom styling',
subtitle:
'Custom colors, radius, spacing, box size, and text style.',
),
_StyledDemoCard(),
SizedBox(height: 24),
_SectionHeader(
title: 'Obscured PIN entry',
subtitle:
'Six-digit PIN with obscured characters, like a banking app.',
),
_ObscuredDemoCard(),
SizedBox(height: 24),
_SectionHeader(
title: 'Validation & server errors',
subtitle:
'Combines a local validator with a simulated server rejection.',
),
_ValidationDemoCard(),
SizedBox(height: 24),
_SectionHeader(
title: 'Resend timer with imperative control',
subtitle:
'Uses a GlobalKey to clear the field and manage external controllers.',
),
_ResendTimerDemoCard(),
SizedBox(height: 24),
_SectionHeader(
title: 'Disabled & read-only states',
subtitle: 'Toggle between enabled, disabled, and read-only.',
),
_StatesDemoCard(),
SizedBox(height: 24),
_SectionHeader(
title: 'Right-to-left layout',
subtitle: 'Same widget, mirrored for RTL locales such as Arabic.',
),
_RtlDemoCard(),
],
),
);
}
}
class _SectionHeader extends StatelessWidget {
const _SectionHeader({required this.title, required this.subtitle});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: theme.textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700)),
const SizedBox(height: 4),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(color: Colors.black54),
),
],
),
);
}
}
class _DemoCard extends StatelessWidget {
const _DemoCard({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 16,
offset: const Offset(0, 8),
),
],
),
child: child,
);
}
}
class _BasicDemoCard extends StatefulWidget {
const _BasicDemoCard();
@override
State<_BasicDemoCard> createState() => _BasicDemoCardState();
}
class _BasicDemoCardState extends State<_BasicDemoCard> {
String _status = 'Enter the 4-digit code sent to your phone.';
@override
Widget build(BuildContext context) {
return _DemoCard(
child: Column(
children: [
SmartOtpField(
length: 4,
autofocus: false,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) =>
setState(() => _status = 'Current value: $value'),
onCompleted: (value) =>
setState(() => _status = 'Completed with $value!'),
),
const SizedBox(height: 16),
Text(_status, style: Theme.of(context).textTheme.bodySmall),
],
),
);
}
}
class _StyledDemoCard extends StatelessWidget {
const _StyledDemoCard();
@override
Widget build(BuildContext context) {
return _DemoCard(
child: SmartOtpField(
length: 5,
boxWidth: 52,
boxHeight: 60,
spacing: 10,
borderRadius: 16,
borderWidth: 2,
borderColor: const Color(0xFFE0E0E0),
focusedBorderColor: const Color(0xFF7C4DFF),
errorBorderColor: const Color(0xFFFF5252),
fillColor: const Color(0xFFF3F1FF),
focusedFillColor: const Color(0xFFEDE7FF),
textStyle: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: Color(0xFF311B92),
),
cursorColor: const Color(0xFF7C4DFF),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
);
}
}
class _ObscuredDemoCard extends StatelessWidget {
const _ObscuredDemoCard();
@override
Widget build(BuildContext context) {
return _DemoCard(
child: SmartOtpField(
length: 6,
obscureText: true,
obscuringCharacter: '●',
boxWidth: 42,
boxHeight: 52,
spacing: 8,
borderRadius: 10,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textStyle: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
);
}
}
class _ValidationDemoCard extends StatefulWidget {
const _ValidationDemoCard();
@override
State<_ValidationDemoCard> createState() => _ValidationDemoCardState();
}
class _ValidationDemoCardState extends State<_ValidationDemoCard> {
final GlobalKey<SmartOtpFieldState> _fieldKey =
GlobalKey<SmartOtpFieldState>();
bool _isVerifying = false;
String? _validate(String? value) {
if (value == null || value.length < 4) {
return 'Please enter all 4 digits.';
}
return null;
}
Future<void> _verify() async {
final isLocallyValid = _fieldKey.currentState!.validate();
if (!isLocallyValid) return;
setState(() => _isVerifying = true);
await Future<void>.delayed(const Duration(seconds: 1));
setState(() => _isVerifying = false);
final code = _fieldKey.currentState!.value;
if (code != '1234') {
_fieldKey.currentState!
.setError('Incorrect code. Try 1234 for this demo.');
} else {
_fieldKey.currentState!.setError(null);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Verified successfully!')),
);
}
}
}
@override
Widget build(BuildContext context) {
return _DemoCard(
child: Column(
children: [
SmartOtpField(
key: _fieldKey,
length: 4,
validator: _validate,
autovalidateMode: AutovalidateMode.onUserInteraction,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
const SizedBox(height: 16),
FilledButton(
onPressed: _isVerifying ? null : _verify,
child: _isVerifying
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
)
: const Text('Verify code'),
),
],
),
);
}
}
class _ResendTimerDemoCard extends StatefulWidget {
const _ResendTimerDemoCard();
@override
State<_ResendTimerDemoCard> createState() => _ResendTimerDemoCardState();
}
class _ResendTimerDemoCardState extends State<_ResendTimerDemoCard> {
static const int _totalSeconds = 30;
late final List<TextEditingController> _controllers;
late final List<FocusNode> _focusNodes;
final GlobalKey<SmartOtpFieldState> _fieldKey =
GlobalKey<SmartOtpFieldState>();
Timer? _timer;
int _secondsRemaining = _totalSeconds;
@override
void initState() {
super.initState();
_controllers =
List<TextEditingController>.generate(4, (_) => TextEditingController());
_focusNodes = List<FocusNode>.generate(4, (_) => FocusNode());
_startTimer();
}
void _startTimer() {
_timer?.cancel();
setState(() => _secondsRemaining = _totalSeconds);
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_secondsRemaining <= 1) {
timer.cancel();
setState(() => _secondsRemaining = 0);
} else {
setState(() => _secondsRemaining -= 1);
}
});
}
void _resend() {
_fieldKey.currentState!.clear();
_startTimer();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('A new code has been sent.')),
);
}
@override
void dispose() {
_timer?.cancel();
for (final controller in _controllers) {
controller.dispose();
}
for (final node in _focusNodes) {
node.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final canResend = _secondsRemaining == 0;
return _DemoCard(
child: Column(
children: [
SmartOtpField(
key: _fieldKey,
length: 4,
controllers: _controllers,
focusNodes: _focusNodes,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
const SizedBox(height: 16),
TextButton(
onPressed: canResend ? _resend : null,
child: Text(
canResend
? 'Resend code'
: 'Resend available in ${_secondsRemaining}s',
),
),
],
),
);
}
}
class _StatesDemoCard extends StatefulWidget {
const _StatesDemoCard();
@override
State<_StatesDemoCard> createState() => _StatesDemoCardState();
}
enum _FieldMode { enabled, disabled, readOnly }
class _StatesDemoCardState extends State<_StatesDemoCard> {
_FieldMode _mode = _FieldMode.enabled;
@override
Widget build(BuildContext context) {
return _DemoCard(
child: Column(
children: [
SmartOtpField(
length: 4,
enabled: _mode != _FieldMode.disabled,
readOnly: _mode == _FieldMode.readOnly,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
const SizedBox(height: 16),
SegmentedButton<_FieldMode>(
segments: const [
ButtonSegment(value: _FieldMode.enabled, label: Text('Enabled')),
ButtonSegment(
value: _FieldMode.disabled, label: Text('Disabled')),
ButtonSegment(
value: _FieldMode.readOnly, label: Text('Read-only')),
],
selected: {_mode},
onSelectionChanged: (selection) =>
setState(() => _mode = selection.first),
),
],
),
);
}
}
class _RtlDemoCard extends StatelessWidget {
const _RtlDemoCard();
@override
Widget build(BuildContext context) {
return _DemoCard(
child: SmartOtpField(
length: 4,
textDirection: TextDirection.rtl,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
semanticLabel: 'حقل رمز التحقق المكون من أربعة أرقام',
),
);
}
}