Flutter

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.

dart
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.

dart
// 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.

dart
// 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.

dart
// 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),
    ),
  ),
)