UI Designs: Login Screen

This page is different from the rest of the track: instead of just reading about widgets, you're looking at an actual login screen design — a mockup on the left rendered to look exactly like a real phone screen, and the real Flutter/Dart code that produces it on the right. This is the first entry in what will grow into a small gallery of hands-on UI designs (a sign-up screen, a dashboard, a profile page, and more are planned) — each one taking a common real-world screen and showing you precisely how it's built, widget by widget.

The Design

What's in this design

  • An app icon/logo, a title, and a short subtitle for context
  • Two text fields (email, password) using Material's underline text field style
  • A right-aligned "Forgot Password?" link
  • A full-width, high-emphasis "Log In" button
  • A visual divider for an "or continue with" alternative sign-in section
  • A "Sign Up" prompt pinned toward the bottom of the screen
Note: The mockup above is an HTML/CSS recreation for illustration — this website doesn't run a real Flutter build. The Dart code below it is correct, real Flutter code that produces this exact screen.

The Full Flutter Code

Here's the complete LoginScreen widget that builds the design above, section by section.

1. The Scaffold and Controllers

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _formKey = GlobalKey<FormState>();

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

2. The Logo, Title, and Subtitle

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 24),
          child: Form(
            key: _formKey,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const SizedBox(height: 40),
                Container(
                  width: 64, height: 64,
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(16),
                    gradient: const LinearGradient(
                      colors: [Color(0xFF0175C2), Color(0xFF02569B)],
                    ),
                  ),
                  child: const Icon(Icons.bolt, color: Colors.white),
                ),
                const SizedBox(height: 18),
                Text('Welcome Back',
                    style: Theme.of(context).textTheme.headlineSmall),
                const SizedBox(height: 4),
                Text('Sign in to continue to your account',
                    style: Theme.of(context).textTheme.bodySmall),
                const SizedBox(height: 28),
Cross-reference: This screen leans heavily on layout widgets (Column, Padding, SizedBox) covered in Layout Widgets and text styling from Themes & Styling.

3. The Email and Password Fields

                TextFormField(
                  controller: _emailController,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(labelText: 'Email'),
                  validator: (value) {
                    if (value == null || !value.contains('@')) {
                      return 'Enter a valid email';
                    }
                    return null;
                  },
                ),
                const SizedBox(height: 16),
                TextFormField(
                  controller: _passwordController,
                  obscureText: true,
                  decoration: const InputDecoration(labelText: 'Password'),
                  validator: (value) {
                    if (value == null || value.length < 6) {
                      return 'Password must be at least 6 characters';
                    }
                    return null;
                  },
                ),
Note: obscureText: true is exactly what turns the password field into dots on screen. The full validation pattern here is covered in depth in Forms & Input Handling.

4. The Forgot Password Link and Login Button

                const SizedBox(height: 6),
                Align(
                  alignment: Alignment.centerRight,
                  child: TextButton(
                    onPressed: () {},
                    child: const Text('Forgot Password?'),
                  ),
                ),
                const SizedBox(height: 16),
                ElevatedButton(
                  onPressed: () {
                    if (_formKey.currentState!.validate()) {
                      // Both fields passed validation — proceed with login
                    }
                  },
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(vertical: 14),
                  ),
                  child: const Text('LOG IN'),
                ),
                const SizedBox(height: 24),
                Row(
                  children: const [
                    Expanded(child: Divider()),
                    Padding(
                      padding: EdgeInsets.symmetric(horizontal: 8),
                      child: Text('OR CONTINUE WITH'),
                    ),
                    Expanded(child: Divider()),
                  ],
                ),

5. The Sign-Up Prompt

                const Spacer(),
                Center(
                  child: RichText(
                    text: TextSpan(
                      style: Theme.of(context).textTheme.bodySmall,
                      children: [
                        const TextSpan(text: "Don't have an account? "),
                        TextSpan(
                          text: 'Sign Up',
                          style: const TextStyle(fontWeight: FontWeight.bold),
                          recognizer: TapGestureRecognizer()..onTap = () {},
                        ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 24),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
Note: Spacer() pushes the sign-up prompt down toward the bottom of the available space — the same technique covered in Layout Widgets.

Design Choices Worth Noticing

ChoiceWhy it's made this way
Underline-style text fieldsMaterial Design's default look — clean, doesn't compete visually with the primary Login button
One high-emphasis buttonOnly ONE primary action (Log In) uses a filled button — "Forgot Password?" and "Sign Up" are lower-emphasis text buttons/links, guiding the eye correctly
Validation on submit, not on every keystrokeTextFormField.validator runs when _formKey.currentState!.validate() is called, not on every character typed — less naggy for the user
const constructors everywhere possibleWidgets that never change (like static text and icons) are marked const, which Flutter can skip rebuilding entirely — a small but real performance habit, foreshadowed in Performance Tips
More designs coming: this Login Screen is the first in a growing gallery — a Sign-Up screen, a Dashboard, and a Profile page are natural next additions. Continue to Animations Basics for now.