You will also learn about the following:
The following illustration shows a schematic view of the widget composition we'll accomplish while building the layout for our login screen 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:
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:
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:
Run it on DartPad and you should get this output in the preview:
Let's add some spacing and some of the labels we have in our design:
And run it on DartPad real quick and it should look as follows:
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:
Let's add the first Text widget and some spacing underneath it:
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.
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:
Now, as the child of that Container, add a TextField widget with the following specs:
Your username TextField widget should look as follows:
Running this on DartPad, you should get the following so far:
Apply the same treatment for the TextField representing the password field, adding first its corresponding wrapper Container with some spacing right above it:
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:
And running it on DartPad, we get the following output:
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.
The following is a schematic of how they are composed and how we'll create them:
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:
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:
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:
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:
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:
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:
Take it for a spin on DartPad, and you get the following on the 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.
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:
Change the logic on the Material widget's backgroundColor, as such:
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:
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:
Let's run it through DartPad and see what we've achieved so far:
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!
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);
}