Login Intent to Home
Build a login screen with TextFormField validation, understand Navigator.push vs pushReplacement, set up named routes, and use StatefulWidget for form state.
1. StatefulWidget Login Form
A login screen needs state to manage form input and loading state. Use TextFormField inside a Form widget with a GlobalKey<FormState> to validate and read values.
import "package:flutter/material.dart";
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailCtrl = TextEditingController();
final _passCtrl = TextEditingController();
bool _obscurePass = true;
bool _isLoading = false;
@override
void dispose() {
_emailCtrl.dispose();
_passCtrl.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
// Simulate API call
await Future.delayed(const Duration(seconds: 2));
setState(() => _isLoading = false);
if (mounted) {
// Go to home and remove login from back stack
Navigator.pushReplacementNamed(context, "/home");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(Icons.lock_outline, size: 80, color: Colors.indigo),
const SizedBox(height: 24),
const Text(
"Welcome Back",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 32),
// Email field
TextFormField(
controller: _emailCtrl,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: "Email",
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
validator: (value) {
if (value == null || value.isEmpty) return "Email is required";
if (!value.contains("@")) return "Enter a valid email";
return null;
},
),
const SizedBox(height: 16),
// Password field
TextFormField(
controller: _passCtrl,
obscureText: _obscurePass,
decoration: InputDecoration(
labelText: "Password",
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(_obscurePass
? Icons.visibility_off
: Icons.visibility),
onPressed: () =>
setState(() => _obscurePass = !_obscurePass),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
validator: (value) {
if (value == null || value.isEmpty) return "Password is required";
if (value.length < 6) return "At least 6 characters";
return null;
},
),
const SizedBox(height: 24),
// Login button
SizedBox(
height: 50,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text("Login", style: TextStyle(fontSize: 16)),
),
),
],
),
),
),
),
),
);
}
}
2. Navigator.push vs pushReplacement vs pushAndRemoveUntil
Choose the right navigation method based on whether you want the previous screen to remain in the back stack. For login → home, always use pushReplacement so the user can't go back to the login screen.
// push – adds on top of stack (back button returns to previous)
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const DetailScreen()),
);
// pushReplacement – replaces current screen (no going back)
// Best for: Login → Home, Splash → Home
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
// pushAndRemoveUntil – clears all previous routes from stack
// Best for: Logout (go to Login and clear everything)
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const LoginScreen()),
(route) => false, // Remove all previous routes
);
// pop – go back to previous screen
Navigator.pop(context);
// pop with return data
Navigator.pop(context, "result_data");
// Receiving return data from a pushed screen
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SelectionScreen()),
);
print(result); // Prints whatever was passed to pop()
3. Named Routes Setup
Define named routes in MaterialApp for centralized navigation management. Named routes make large apps easier to maintain and allow navigation from anywhere without importing screen files.
// lib/utils/routes.dart – centralize route names
class AppRoutes {
static const String splash = "/";
static const String login = "/login";
static const String home = "/home";
static const String profile = "/profile";
static const String detail = "/detail";
}
// lib/app.dart – register routes
MaterialApp(
initialRoute: AppRoutes.splash,
routes: {
AppRoutes.splash: (_) => const SplashScreen(),
AppRoutes.login: (_) => const LoginScreen(),
AppRoutes.home: (_) => const HomeScreen(),
AppRoutes.profile: (_) => const ProfileScreen(),
},
// For routes needing arguments, use onGenerateRoute
onGenerateRoute: (settings) {
if (settings.name == AppRoutes.detail) {
final args = settings.arguments as Map<String, dynamic>;
return MaterialPageRoute(
builder: (_) => DetailScreen(id: args["id"]),
);
}
return null;
},
)
// Navigate with named routes
Navigator.pushNamed(context, AppRoutes.home);
Navigator.pushReplacementNamed(context, AppRoutes.home);
// Pass arguments with named routes
Navigator.pushNamed(
context,
AppRoutes.detail,
arguments: {"id": 42, "title": "My Item"},
);
// Receive arguments in destination screen
final args = ModalRoute.of(context)!.settings.arguments
as Map<String, dynamic>;
print(args["id"]);
4. TextFormField Variants & InputDecoration
Common TextFormField configurations for different input types: email, password, phone, multiline text, and number input.
// Email input
TextFormField(
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next, // Shows "Next" on keyboard
decoration: const InputDecoration(
labelText: "Email",
hintText: "example@mail.com",
prefixIcon: Icon(Icons.email),
filled: true,
fillColor: Colors.white,
),
)
// Phone number input
TextFormField(
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: "Phone",
prefixText: "+62 ",
prefixIcon: Icon(Icons.phone),
),
)
// Multiline / textarea
TextFormField(
maxLines: 4,
minLines: 3,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
decoration: const InputDecoration(
labelText: "Description",
alignLabelWithHint: true,
border: OutlineInputBorder(),
),
)
// Number input
TextFormField(
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(
labelText: "Price",
prefixText: "Rp ",
suffixText: ".00",
),
)
// Styled InputDecoration with theme override
TextFormField(
decoration: InputDecoration(
labelText: "Username",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.grey),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.indigo, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.red),
),
),
)