Created
January 25, 2024 16:57
-
-
Save hawkkiller/2cf72e99770104ee3ddb240ae3cb73a5 to your computer and use it in GitHub Desktop.
Async validation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(const MainApp()); | |
} | |
class UsernameValidator { | |
final UserRepository _userRepository; | |
UsernameValidator({ | |
UserRepository? userRepository, | |
}) : _userRepository = userRepository ?? UserRepository(); | |
// Validate username | |
// This returns true if the username is valid, false otherwise | |
Future<bool> checkUsernameIsUnique(String usrname) async { | |
try { | |
final isUnique = await _userRepository.checkNameUnique(usrname); | |
return isUnique; | |
} on Object catch (e) { | |
// logger.error(e); report error to logger and tracking system | |
// Logic to handle error; return true so that the user can continue | |
return true; | |
} | |
} | |
} | |
class UserRepository { | |
/// Returns true if value is unique, false otherwise | |
Future<bool> checkNameUnique(String value) async { | |
await Future.delayed(const Duration(seconds: 1)); | |
if (value == 'Michelle') { | |
return false; | |
} | |
return true; | |
} | |
} | |
class MainApp extends StatelessWidget { | |
const MainApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData( | |
colorSchemeSeed: Colors.deepPurple, | |
), | |
home: const Home(), | |
); | |
} | |
} | |
class Home extends StatefulWidget { | |
const Home({super.key}); | |
@override | |
State<Home> createState() => _HomeState(); | |
} | |
class _HomeState extends State<Home> { | |
/* Username */ | |
late final ValueNotifier<String?> usernameError; | |
late final TextEditingController usernameController; | |
late final UsernameValidator usernameValidator; | |
/* Password */ | |
late final ValueNotifier<String?> passwordError; | |
late final TextEditingController passwordController; | |
/* Button */ | |
late final ValueNotifier<bool> inProgress; | |
@override | |
void initState() { | |
/* Username */ | |
usernameController = TextEditingController(); | |
usernameError = ValueNotifier(null); | |
usernameValidator = UsernameValidator(); | |
/* Password */ | |
passwordController = TextEditingController(); | |
passwordError = ValueNotifier(null); | |
/* Button */ | |
inProgress = ValueNotifier(false); | |
super.initState(); | |
} | |
Future<bool> _validateFields() async { | |
var username = _validateUsername(usernameController.text); | |
var password = _validatePassword(passwordController.text); | |
passwordError.value = password; | |
username ??= await _validateUsernameUnique(usernameController.text); | |
usernameError.value = username; | |
return username == null && password == null; | |
} | |
String? _validateUsername(String username) { | |
if (username.length < 6) { | |
return 'Username must be at least 6 characters long'; | |
} | |
if (!RegExp(r'^[a-zA-Z0-9]+$').hasMatch(username)) { | |
return 'Username must only contain alphanumeric characters'; | |
} | |
return null; | |
} | |
Future<String?> _validateUsernameUnique(String username) async { | |
final isUnique = await usernameValidator.checkUsernameIsUnique(username); | |
if (!isUnique) { | |
return 'Username is already taken'; | |
} | |
return null; | |
} | |
String? _validatePassword(String password) { | |
if (password.length < 8) { | |
return 'Password must be at least 8 characters long'; | |
} | |
return null; | |
} | |
Future<void> _onSignUp() async { | |
inProgress.value = true; | |
final res = await _validateFields(); | |
inProgress.value = false; | |
if (res) { | |
if (context.mounted) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar( | |
content: Text('Sign up successful!'), | |
), | |
); | |
} | |
return; | |
} | |
if (context.mounted) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar( | |
content: Text('Sign up failed!'), | |
), | |
); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
backgroundColor: Theme.of(context).colorScheme.background, | |
body: Center( | |
child: ConstrainedBox( | |
constraints: BoxConstraints.loose( | |
const Size.fromWidth(400), | |
), | |
child: Card( | |
child: Padding( | |
padding: const EdgeInsets.all(16), | |
child: Builder( | |
builder: (context) { | |
return Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
const Text( | |
'Sign Up', | |
style: TextStyle( | |
fontSize: 24, | |
fontWeight: FontWeight.w600, | |
), | |
), | |
Padding( | |
padding: const EdgeInsets.only(top: 16), | |
child: ValueListenableBuilder( | |
valueListenable: usernameError, | |
builder: (context, value, __) { | |
return TextField( | |
controller: usernameController, | |
style: Theme.of(context).textTheme.bodyMedium, | |
decoration: InputDecoration( | |
labelText: 'Username', | |
border: const OutlineInputBorder(), | |
errorText: value, | |
), | |
); | |
}), | |
), | |
Padding( | |
padding: const EdgeInsets.only(top: 16), | |
child: ValueListenableBuilder( | |
valueListenable: passwordError, | |
builder: (context, value, __) { | |
return TextField( | |
controller: passwordController, | |
style: Theme.of(context).textTheme.bodyMedium, | |
decoration: InputDecoration( | |
labelText: 'Password', | |
border: const OutlineInputBorder(), | |
errorText: value, | |
), | |
); | |
}), | |
), | |
Padding( | |
padding: const EdgeInsets.only(top: 8), | |
child: Text( | |
'Username should be at least 6 characters long and only contain alphanumeric characters. ' | |
'Password should be at least 8 characters long.', | |
style: Theme.of(context).textTheme.bodySmall?.copyWith( | |
color: Theme.of(context).colorScheme.outline, | |
), | |
), | |
), | |
Padding( | |
padding: const EdgeInsets.only(top: 16), | |
child: ValueListenableBuilder( | |
valueListenable: inProgress, | |
builder: (context, progress, _) { | |
return FilledButton.icon( | |
label: const Text('Send'), | |
onPressed: progress ? null : _onSignUp, | |
icon: progress | |
? const CircularProgressIndicator.adaptive() | |
: const Icon(Icons.send), | |
); | |
}, | |
), | |
), | |
], | |
); | |
}, | |
), | |
), | |
), | |
), | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment