let’s build a clean, practical Flutter login page step-by-step. I’ll give you a working example (with full main.dart, login_page.dart, and home_page.dart) plus explanations and tips to extend it for a real backend.
1) Create a new Flutter project
Run in terminal:
flutter create flutter_login_demo
cd flutter_login_demo
No extra packages are required for the basic demo. (If you later want real auth, you’ll add http, flutter_secure_storage, or a state manager like bloc/provider.)
2) Project structure (simple)
lib/
main.dart
pages/
login_page.dart
home_page.dart
3) Full code — copy into files
lib/main.dart
import 'package:flutter/material.dart';
import 'pages/login_page.dart';
import 'pages/home_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Login Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.indigo,
),
initialRoute: '/',
routes: {
'/': (context) => const LoginPage(),
'/home': (context) => const HomePage(),
},
);
}
}
lib/pages/login_page.dart
import 'package:flutter/material.dart';
import 'dart:async';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _loading = false;
String? _errorMessage;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
// Mock authentication function to simulate a network call
Future<bool> _fakeAuthenticate(String email, String password) async {
await Future.delayed(const Duration(seconds: 2)); // simulate network
// Example: accept only this credential pair
return email.trim().toLowerCase() == 'test@example.com' && password == 'password';
}
void _submit() async {
setState(() {
_errorMessage = null;
});
if (!_formKey.currentState!.validate()) return;
final email = _emailController.text;
final password = _passwordController.text;
setState(() => _loading = true);
try {
final ok = await _fakeAuthenticate(email, password);
if (ok) {
// Navigate to home and remove login from stack
if (!mounted) return;
Navigator.of(context).pushReplacementNamed('/home');
} else {
setState(() {
_errorMessage = 'Invalid email or password.';
});
}
} catch (e) {
setState(() {
_errorMessage = 'An error occurred. Try again.';
});
} finally {
if (mounted) {
setState(() => _loading = false);
}
}
}
String? _validateEmail(String? value) {
if (value == null || value.trim().isEmpty) return 'Please enter your email';
final email = value.trim();
final emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
if (!emailRegex.hasMatch(email)) return 'Enter a valid email';
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) return 'Please enter your password';
if (value.length < 6) return 'Password must be at least 6 characters';
return null;
}
@override
Widget build(BuildContext context) {
// Responsive width constraint
final width = MediaQuery.of(context).size.width;
final formWidth = width > 600 ? 480.0 : width * 0.9;
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 24),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: formWidth),
child: Card(
elevation: 6,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const FlutterLogo(size: 72),
const SizedBox(height: 12),
const Text('Welcome back', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
const SizedBox(height: 20),
if (_errorMessage != null) ...[
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_errorMessage!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
validator: _validateEmail,
autofillHints: const [AutofillHints.username, AutofillHints.email],
),
const SizedBox(height: 12),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
validator: _validatePassword,
autofillHints: const [AutofillHints.password],
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
// stub: implement forgot password flow
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Forgot password tapped (not implemented)')),
);
},
child: const Text('Forgot password?'),
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Sign in'),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Don't have an account?"),
TextButton(
onPressed: () {
// stub: implement registration flow
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Register tapped (not implemented)')),
);
},
child: const Text('Register'),
)
],
),
],
),
),
),
),
),
),
),
);
}
}
Notes on the login code
- Uses
Form+ validators (email + password length). - Uses controllers and
dispose()correctly. - Displays an inline error message area and a loading spinner in the button.
_fakeAuthenticatesimulates network latency and accepts onlytest@example.com/password. Change for real auth.
lib/pages/home_page.dart
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
// navigate back to login (clear stack)
Navigator.of(context).pushReplacementNamed('/');
},
)
],
),
body: const Center(
child: Text('You are signed in!', style: TextStyle(fontSize: 18)),
),
);
}
}
4) Run the app
flutter run
Open an emulator or physical device. Use credentials:
- Email:
test@example.com - Password:
password
5) How to connect to a real backend (overview)
- Add
httppackage:flutter pub add http - In
_submit()replace_fakeAuthenticatewith an HTTP POST to your API (send email/password). - On success, store token securely: use
flutter_secure_storageor platform secure storage. - Use token in subsequent API calls (add Authorization header).
- Implement error handling for network/timeouts and show friendly messages.
6) Improvements & best practices
- Move auth logic into a separate service class (e.g.,
AuthService) → easier testing. - Use state management (Provider / Riverpod / Bloc) for larger apps.
- Add form focus traversal and improved accessibility (labels, semantics).
- Use HTTPS, validate tokens on app start, refresh tokens if needed.
- Add unit/widget tests: test validation and navigation flows.
7) Quick checklist before production
- Use secure storage for tokens.
- Protect API keys and secrets.
- Validate server responses and show specific error messages.
- Implement signup / forgot-password flows and email verification.
- Add rate-limiting/backoff for retries.