Login Screen
Loading preview
Starting the Flutter app
A Flutter login screen with a hero illustration, validated email and password fields, remember-me, and sign-in callbacks.
import 'package:flutter/material.dart';
class LoginHero extends StatefulWidget {
/// Called with the trimmed email and the password once both validate.
final void Function(String email, String password)? onSignIn;
/// Called by 'Forgot password?'.
final VoidCallback? onForgotPassword;
/// Called by 'Sign up'.
final VoidCallback? onSignUp;
/// Each callback is optional. Without one, the demo shows a SnackBar.
const LoginHero({
super.key,
this.onSignIn,
this.onForgotPassword,
this.onSignUp,
});
@override
State<LoginHero> createState() => _LoginHeroState();
}
class _LoginHeroState extends State<LoginHero> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _remember = true;
bool _obscure = true;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _showMessage(String message) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(message)));
}
String? _validateEmail(String? value) {
final email = value?.trim() ?? '';
if (email.isEmpty) return 'Enter your email';
final valid = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$').hasMatch(email);
return valid ? null : 'Enter a valid email';
}
String? _validatePassword(String? value) =>
(value ?? '').isEmpty ? 'Enter your password' : null;
void _signIn() {
if (!_formKey.currentState!.validate()) return;
final email = _emailController.text.trim();
final onSignIn = widget.onSignIn;
if (onSignIn != null) {
onSignIn(email, _passwordController.text);
} else {
_showMessage('Signing in as $email (demo)');
}
}
void _forgotPassword() {
final onForgotPassword = widget.onForgotPassword;
if (onForgotPassword != null) {
onForgotPassword();
} else {
_showMessage('Password reset goes here (demo)');
}
}
void _signUp() {
final onSignUp = widget.onSignUp;
if (onSignUp != null) {
onSignUp();
} else {
_showMessage('Sign up goes here (demo)');
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scheme = theme.colorScheme;
return Scaffold(
body: SafeArea(
child: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 24),
children: [
const SizedBox(height: 24),
Center(
child: Container(
height: 160,
width: double.infinity,
decoration: BoxDecoration(
color: scheme.primaryContainer,
borderRadius: BorderRadius.circular(24),
),
child: Icon(
Icons.lock_person_rounded,
size: 72,
color: scheme.onPrimaryContainer,
),
),
),
const SizedBox(height: 28),
Text(
'Welcome back',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Sign in to continue to your account',
style: theme.textTheme.bodyMedium?.copyWith(
color: scheme.onSurfaceVariant,
),
),
const SizedBox(height: 24),
TextFormField(
controller: _emailController,
validator: _validateEmail,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
textInputAction: TextInputAction.next,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.mail_outline),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
validator: _validatePassword,
obscureText: _obscure,
autofillHints: const [AutofillHints.password],
onFieldSubmitted: (_) => _signIn(),
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: const Icon(Icons.lock_outline),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
tooltip: _obscure ? 'Show password' : 'Hide password',
icon: Icon(
_obscure ? Icons.visibility_off : Icons.visibility,
),
onPressed: () => setState(() => _obscure = !_obscure),
),
),
),
// Wrap, so the two drop onto separate lines at large text sizes.
Wrap(
alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: _remember,
onChanged: (v) =>
setState(() => _remember = v ?? false),
),
const Text('Remember me'),
],
),
TextButton(
onPressed: _forgotPassword,
child: const Text('Forgot password?'),
),
],
),
const SizedBox(height: 8),
SizedBox(
height: 52,
child: FilledButton(
onPressed: _signIn,
child: const Text('Sign in'),
),
),
const SizedBox(height: 16),
Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
"Don't have an account?",
style: TextStyle(color: scheme.onSurfaceVariant),
),
TextButton(onPressed: _signUp, child: const Text('Sign up')),
],
),
],
),
),
),
);
}
}
Add it to your project
Copies the Dart source into your app, so you own it. Flutter SDK only, no extra packages.
First time? Run dart pub global activate fluttertemplates_dev, or read the CLI guide.
How it works
_remember drives the Checkbox and _obscure the password visibility toggle. _signIn validates the Form (an email-shaped address and a non-empty password), then calls onSignIn with the trimmed email and password. ‘Forgot password?’ and ‘Sign up’ call onForgotPassword and onSignUp. All three are optional, and each shows a demo SnackBar when omitted, so nothing is authenticated by default. The hero is a tinted Container holding an icon, not an image.
Adapting it
- Call your auth service from
onSignIn. - Track a loading flag around that call, disable the
FilledButtonduring it, and show failures inline. _rememberisn’t stored anywhere, so use it to decide whether the session token persists.