What You'll Build in this Workshop:

You will also learn about the following:

Login Screen

The following illustration shows a schematic view of the widget composition we'll accomplish while building the layout for our login screen widget:

Login Screen

Flesh out the FlutterBankLogin page widget

In the previous lab, we created the skeleton of our login page, consisting of a StatefulWidget called FlutterBankLogin. We made it stateful since this widget will maintain its own state internally as well as externally, and rebuild itself as the state of its components (textfields, buttons, etc.) change.

Let's start building the structure of this page and focus on this widget's build method.

To the Scaffold widget, set its background to white, and as its body, add a Container widget with 30px of padding all around:

Login Screen

As the sole child of this Container, add a Column widget with crossAxisAlignment set to start so its children are left aligned, and an empty children list for now:

Login Screen

Let's add the first child to this Column - the container representing the app's logo. Let's add a Container of 80px by 80px, with a 7px border, with 100px of border radius, and using the mainThemeColor as its color. As the child of this Container, set it to be an Icon widget, 45px in size with the same mainThemeColor for consistency:

Splash Screen

Run it on DartPad and you should get this output in the preview:

Splash Screen

Let's add some spacing and some of the labels we have in our design:

Login Screen

And run it on DartPad real quick and it should look as follows:

Login Screen

Let's work on the middle section of this page, where we'll add the input fields.

Add an Expanded widget since it will occupy most of the real estate in the Column widget. Add as its inmediate child a Center widget wrapping another Column widget, with its items centered vertically (MainAxisAlignment.center) and stretched horizontally (CrossAxisAlignment.stretch) as follows:

Login Screen

Let's add the first Text widget and some spacing underneath it:

Login Screen

We're going to now add both textfields for the user to input the email and password. At the top of the FlutterBankLoginState class, add two TextEditingController instances, one for each of our fields. The TextEditingController allows us to controller the actions on a TextField widget, get notifications on text field updates, set initial values, get its provided input, etc.

Login Screen

Back on our widget structure, let's create the first TextField widget, wrapped inside a Container that gives it that rounded-edge style. Add the Container widget first, with the following specs:

Login Screen

Now, as the child of that Container, add a TextField widget with the following specs:

Your username TextField widget should look as follows:

Login Screen

Running this on DartPad, you should get the following so far:

Login Screen

Apply the same treatment for the TextField representing the password field, adding first its corresponding wrapper Container with some spacing right above it:

Login Screen

As the child of this Container, add the textfield representing the password (similar to the username field) but with some additional properties:

Your password TextField widget should look as follows:

Login Screen

And running it on DartPad, we get the following output:

Login Screen

Now that the top portion of this login page is pretty much done, let's focus on the two action buttons (Sign In and Register), which we'll create as custom widgets so we can reuse them throughout.

Login Screen

The following is a schematic of how they are composed and how we'll create them:

Login Screen

Let's get back to the main Column widget laying out the main components on this page, and start placing our buttons under the Expanded widget we just worked on.

Start by creating a separate class called FlutterBankMainButton that extends StatelessWidget. Return an empty Column widget from its build method since it will serve as our button's foundation. Add a constructor that populates three internal class properties:

Your code should look as follows:

Login Screen

Add a crossAxisAlignment of stretch to the Column widget so the contents stretch horizontally. Add a Container widget with 15px padding all around, 50px border radius, and as its child, a Text widget, feeding as its content the value provided by the label property. Center align the text, and make it white and bold, as follows:

Login Screen

To give it is clickability and button-like behavior, wrap the Container inside an InkWell widget. Add the following properties to the InkWell:

Your code should look as follows:

Login Screen

Now, to give the InkWell its "Material" behavior, it must be wrapped inside a (yeah, you guessed it - a Material widget). Wrap the InkWell inside a Material widget, and set its color depending on whether the enabled property is true or not, so set its opacity to 50% when disabled which will give it that disabled look, as follows:

Login Screen

Not quite there yet. We want now to give this widget the rounded edges to match our design, so we need to clip thos edges using a ClipRRect widget, so wrap the whole hierarchy - including the Material widget inside a ClipRRect widget, with a 50px border radius applied to it:

Login Screen

I believe we are ready to consume this newly created widget!

Back on the FlutterBankLoginState widget, below the Expanded widget inside the main Column widget, add this newly created FlutterBankMainButton, feeding into it the following values:

Your FlutterBankMainButton shoud look like this once in place:

Login Screen

Take it for a spin on DartPad, and you get the following on the screen:

Login Screen

Not bad! we've just created a very reusable component with very minimum effort and highly configurable!

Let's proceed now and modify this button so we can reuse it as a passive button, and make it yet more configurable so it can accept an icon as well.

Refactoring the FlutterBankMainButton

Let's change a few things on this widget, such as the ability to take in a background color and an icon so we can reuse it in other places.

Back on the FlutterBankMainButton class, add the following additional properties and their corresponding constructor parameters, as follows:

Login Screen

Change the logic on the Material widget's backgroundColor, as such:

Login Screen

Change the inner structure of this widget's Container - instead of being just a Text widget, we'll make it a nested structure composed of a Row widget that holds the Icon and the Text. Let's work on the following:

Your code should look like this:

Login Screen

Just like we did originally with the initial version of the FlutterBankMainButton, let's add it to the main login page structure, feeding into it the required properties that will make it look as a passive button, and adding some spacing on top of it for aesthetics:

Login Screen

Let's run it through DartPad and see what we've achieved so far:

Login Screen

Looking good so far! We've accomplished quite a bit up to this point! Pat yourself on the back if you've made it this far - congrats!

I think it's time for the second phase of this workshop, which is setting up our Firebase project so we can hook up the authentication functionality to this great UI we've built so far.

Are you ready? Let's go! - See you there on codelab #3

In this codelab, we accomplished the following:

In the next codelab, we'll set up our Firebase project so we can start hooking up all the good backend functionality and bring this app to life, as well as continue fleshing out our login page, adding validation, setting up the state management and business logic that will encapsulate the authentication functionality, plus lots of good stuff!

Please don't forget to follow me on social media:

In case you fell behind on this codelab, below is the whole code for this codelab in a way you can copy / paste directly into DartPad:

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

void main() {
  runApp(FlutterBankApp());
}

class FlutterBankApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        textTheme: GoogleFonts.poppinsTextTheme(
          Theme.of(context).textTheme
        )
      ),
      debugShowCheckedModeBanner: false,
      home: FlutterBankSplash() 
    );
  }
}

class FlutterBankSplash extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {

    Future.delayed(const Duration(seconds: 2), () {
       Navigator.of(context).push(
        MaterialPageRoute(builder: (context) => FlutterBankLogin())
       );   
     });

    return Scaffold(
      backgroundColor: Utils.mainThemeColor,
      body: Stack(
        children: const [
          Center(
            child: Icon(Icons.savings, color: Colors.white, size: 60)
          ),
          Center(
            child: SizedBox(
              width: 100,
              height: 100,
              child: CircularProgressIndicator(
                strokeWidth: 8,
                valueColor: AlwaysStoppedAnimation<Color>(Colors.white)
              )
            )
          )
        ],
      )
    );
  }
}

class FlutterBankLogin extends StatefulWidget {
  @override
  FlutterBankLoginState createState() => FlutterBankLoginState();
}

class FlutterBankLoginState extends State<FlutterBankLogin>{

  TextEditingController usernameController = TextEditingController();
  TextEditingController passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Container(
        padding: const EdgeInsets.all(30),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              width: 80,
              height: 80,
              decoration: BoxDecoration(
                border: Border.all(
                  width: 7,
                  color: Utils.mainThemeColor
                ),
                borderRadius: BorderRadius.circular(100)
              ),
              child: const Icon(Icons.savings, color: Utils.mainThemeColor, size: 45)
            ),
            const SizedBox(height: 30),
            const Text('Welcome to', style: TextStyle(color: Colors.grey, fontSize: 15)),
            const Text('Flutter\nSavings Bank', 
            style: TextStyle(color: Utils.mainThemeColor, fontSize: 30)),
            Expanded(
              child: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center, 
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    const Text('Sign Into Your Bank Account', 
                         textAlign: TextAlign.center, 
                         style: TextStyle(color: Colors.grey, fontSize: 12)
                    ),
                    const SizedBox(height: 10),
                    Container(
                      padding: const EdgeInsets.all(5),
                      decoration: BoxDecoration(
                        color: Colors.grey.withOpacity(0.2),
                        borderRadius: BorderRadius.circular(50)
                      ),
                      child: TextField(
                        onChanged: (text) {
                          setState(() {});
                        },
                        decoration: const InputDecoration(
                          border: InputBorder.none,
                          prefixIcon: Icon(Icons.email, color: Utils.mainThemeColor),
                          focusedBorder: InputBorder.none,
                          enabledBorder: InputBorder.none,
                          errorBorder: InputBorder.none,
                          disabledBorder: InputBorder.none,
                          contentPadding: EdgeInsets.only(
                            left: 20, bottom: 11, top: 11, right: 15
                          ),
                          hintText: "Email"
                        ),
                        style: const TextStyle(fontSize: 16),
                        controller: usernameController
                      ) 
                    ),
                    const SizedBox(height: 20),
                    Container(
                      padding: const EdgeInsets.all(5),
                      decoration: BoxDecoration(
                        color: Colors.grey.withOpacity(0.2),
                        borderRadius: BorderRadius.circular(50)
                      ),
                      child: TextField(
                        onChanged: (text) {
                          setState(() {});
                        },
                        obscureText: true,
                        obscuringCharacter: "*",
                        decoration: const InputDecoration(
                          prefixIcon: Icon(Icons.lock, color: Utils.mainThemeColor),
                          border: InputBorder.none,
                          focusedBorder: InputBorder.none,
                          enabledBorder: InputBorder.none,
                          errorBorder: InputBorder.none,
                          disabledBorder: InputBorder.none,
                          contentPadding: EdgeInsets.only(
                            left: 15, bottom: 11, top: 11, right: 15
                          ),
                          hintText: "Password"
                        ),
                        controller: passwordController,
                        style: const TextStyle(fontSize: 16),
                      )
                    )
                  ]
                )
              )
            ),
            FlutterBankMainButton(
              label: 'Sign In',
              enabled: true,
              onTap: () {}
            ),
            const SizedBox(height: 10),
            FlutterBankMainButton(
              label: 'Register',
              icon: Icons.account_circle,
              onTap: () {},
              backgroundColor: Utils.mainThemeColor.withOpacity(0.05),
              iconColor: Utils.mainThemeColor,
              labelColor: Utils.mainThemeColor
            ) 
          ]
        ),
      ),
    );
  }
}

class FlutterBankMainButton extends StatelessWidget {
  
  final Function? onTap;
  final String? label;
  final bool? enabled;
  final IconData? icon;
  final Color? backgroundColor;
  final Color? iconColor;
  final Color? labelColor;
  
  const FlutterBankMainButton({
    Key? key, this.label, this.onTap, 
    this.icon, 
    this.backgroundColor = Utils.mainThemeColor, 
    this.iconColor = Colors.white,
    this.labelColor = Colors.white,
    this.enabled = true })
  : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        ClipRRect(
          borderRadius: BorderRadius.circular(50),
          child: Material(
            color: enabled! ? backgroundColor : backgroundColor!.withOpacity(0.5),
              child: InkWell(
              onTap: enabled! ? () {
                onTap!();
              } : null,
              highlightColor: Colors.white.withOpacity(0.2),
              splashColor: Colors.white.withOpacity(0.1),
              child: Container(
                padding: const EdgeInsets.all(15),
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(50)
                ),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Visibility(
                        visible: icon != null,
                        child: Container(
                          margin: const EdgeInsets.only(right: 20),
                          child: Icon(icon, color: iconColor, size: 20),
                        )
                      ),
                      Text(label!, textAlign: TextAlign.center, 
                        style: TextStyle(
                          color: labelColor, 
                          fontWeight: FontWeight.bold
                        )
                    )
                  ]
                )
              ),
            ),
          ),
        )
      ],
    );
  }
}


class Utils {
  static const Color mainThemeColor = Color(0xFF8700C3);
}