Flutter and Rive: Adding Life to the Authentication Process— Episode 2
--
Welcome to the second episode of Rive and Flutter: A Match Made in Animation Heaven. In this episode, we will be building a sign-in model and custom text fields with icons that are functional. We will also learn how to create custom model transitions on the sign-in view. Once the button animation is complete, the sign-in dialog will slide down from the top. Lastly, we will create a loading animation that will be displayed during the sign-in process. If there are any errors, an error animation will be shown. If the sign-in is successful, a success animation with confetti will be displayed.
Here is the link to Episode 1 in case you missed it
Sign in dialog
We can use showGeneralDialog
to create such sing in dialog. Let’s create a function called customSigninDialog
Future<Object?> customSigninDialog(BuildContext context,
{required ValueChanged onCLosed}) {
return showGeneralDialog(
barrierDismissible: true,
barrierLabel: "Sign In",
context: context,
// TODO: Custom transition
pageBuilder: (context, _, __) => Center(
child: Container(
height: 620,
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.94),
borderRadius: const BorderRadius.all(Radius.circular(40)),
),
child: Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
clipBehavior: Clip.none,
children: [
Column(
children: [
// TODO: Sign in text and decsription
// TODO: Sign in form
// TODO: Divider
// TODO: Socal login
],
),
// TODO: Close button
],
),
),
),
),
).then(onCLosed);
}
When the button is pressed, we will wait for a short delay before calling the customSigninDialog
function to allow the button to display its animation. Later, I will explain why we need the onClosed
event.
Future.delayed(
const Duration(milliseconds: 800),
() {
// TODO: Update dialog status
customSigninDialog(context, onCLosed: (_) {});
},
);
To include the large sign in text and description, replace Sign in text and description
with the following code.
const Text(
"Sign In",
style: TextStyle(fontSize: 34, fontFamily: "Poppins"),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Text(
"Access to 240+ hours of content. Learn design and code, by building real apps with Flutter and Swift.",
textAlign: TextAlign.center,
),
),
To add the divider, replace TODO: Divider
with the following code.
Row(
children: const [
Expanded(child: Divider()),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
"OR",
style: TextStyle(
color: Colors.black26,
),
),
),
Expanded(child: Divider()),
],
),
To add the social buttons, replace TODO: Social login
with the following code.
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
padding: EdgeInsets.zero,
onPressed: () {},
icon: SvgPicture.asset(
"assets/icons/email_box.svg",
height: 64,
width: 64,
),
),
IconButton(
padding: EdgeInsets.zero,
onPressed: () {},
icon: SvgPicture.asset(
"assets/icons/apple_box.svg",
height: 64,
width: 64,
),
),
IconButton(
padding: EdgeInsets.zero,
onPressed: () {},
icon: SvgPicture.asset(
"assets/icons/google_box.svg",
height: 64,
width: 64,
),
),
],
)
We are almost finished. All that is left to do is create a separate widget for the sign in form and Sign in button which called SignInForm
.
class SignInForm extends StatefulWidget {
const SignInForm({
Key? key,
}) : super(key: key);
@override
State<SignInForm> createState() => _SignInFormState();
}
class _SignInFormState extends State<SignInForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Stack(
children: [
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Email",
style: TextStyle(color: Colors.black54),
),
Padding(
padding: const EdgeInsets.only(top: 8, bottom: 16),
child: TextFormField(
validator: (value) {
if (value!.isEmpty) {
return "";
}
return null;
},
onSaved: (email) {},
decoration: InputDecoration(
prefixIcon: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SvgPicture.asset("assets/icons/email.svg"),
),
),
),
),
const Text(
"Password",
style: TextStyle(color: Colors.black54),
),
Padding(
padding: const EdgeInsets.only(top: 8, bottom: 16),
child: TextFormField(
validator: (value) {
if (value!.isEmpty) {
return "";
}
return null;
},
onSaved: (password) {},
obscureText: true,
decoration: InputDecoration(
prefixIcon: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SvgPicture.asset("assets/icons/password.svg"),
),
),
),
),
Padding(
padding: const EdgeInsets.only(top: 8, bottom: 24),
child: ElevatedButton.icon(
onPressed: () {
// TODO: Show the loading animation
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF77D8E),
minimumSize: const Size(double.infinity, 56),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(25),
bottomRight: Radius.circular(25),
bottomLeft: Radius.circular(25),
),
),
),
icon: const Icon(
CupertinoIcons.arrow_right,
color: Color(0xFFFE0037),
),
label: const Text("Sign In"),
),
),
],
),
),
// TODO: Loading animation
// TODO: Confetti animation
],
);
}
}
And replace TODO: Sign in form
with SignInForm()
.
SignInForm()
Check/Error Animation
We have completed the Sign in dialog and can now display the animation. The animation we are using can be found for free at this link: Check/Error. It has already been added to the asset file as check.rive
The animation has 3 inputs, which allow us to control which animation is displayed at a given time.
We have created two Booleans to show and hide the Check/Error and Confetti animations, as well as three variables of type SMITrigger, which are late variables, to control the Check, Error, and Reset inputs.
bool isShowLoading = false;
bool isShowConfetti = false;
late SMITrigger check;
late SMITrigger error;
late SMITrigger reset;
// TODO: SMITrigger for confetti
It’s time to add the Check/Error animation. I want to display it above the Sign in button and set the height and width to 100. I also want to do the same for the confetti animation, so I have created a new component/widget called CustomPositioned
, which will allow us to align it as desired.
class CustomPositioned extends StatelessWidget {
const CustomPositioned({super.key, required this.child, this.size = 100});
final Widget child;
final double size;
@override
Widget build(BuildContext context) {
return Positioned.fill(
child: Column(
children: [
const Spacer(),
SizedBox(
height: size,
width: size,
child: child,
),
const Spacer(flex: 2),
],
),
);
}
}
It’s time to add the animation. Adding it is as simple as adding an image.
CustomPositioned(
child: RiveAnimation.asset(
"assets/RiveAssets/check.riv",
onInit: (artboard) {
// TODO: Set the trigger
},
),
),
Replace the TODO: Set the trigger
with the following code
StateMachineController? controller = StateMachineController.fromArtboard(artboard, "State Machine 1");
artboard.addController(controller!);
check = controller.findSMI("Check") as SMITrigger;
error = controller.findSMI("Error") as SMITrigger;
reset = controller.findSMI("Reset") as SMITrigger;
As you can see, the loading animation is currently always displayed. However, we only want it to show when the Sign in button is pressed. To fix this, we can use the isShowLoading
variable that we created earlier.
isShowLoading
? CustomPositioned(
child: RiveAnimation.asset( ... ),
)
: const SizedBox(),
The plan for the Sign in button is as follows: when the user clicks the button, we will set isShowLoading
to true
. Then, we will validate the text fields and display the success animation if they are valid, or the error animation if they are not. To implement this, we are going to create a function called signIn
and call it when the button is pressed.
void signIn(BuildContext context) {
setState(() {
isShowLoading = true;
// TODO: Show confetti aniamton
});
Future.delayed(
const Duration(seconds: 1),
() {
if (_formKey.currentState!.validate()) {
check.fire();
Future.delayed(
const Duration(seconds: 2),
() {
setState(() {
isShowLoading = false;
});
// TODO: Fire confetti animation
// TODO: Navigate to next screen
},
);
} else {
error.fire();
Future.delayed(
const Duration(seconds: 2),
() {
setState(() {
isShowLoading = false;
});
},
);
}
},
);
}
To do this, replace the TODO: Show the loading animation
with a call to signIn(context)
.
signIn(context);
Great, this is what we wanted. If the TextField is empty, it shows the error animation; if all the fields are valid, it shows the success animation. For simplicity, I have not implemented any authentication here, but the process would be similar.
The last thing we need to do is show the confetti animation after a successful check. To do this, we should first create a variable called isShowConfetti
that we can use to control the display of the confetti animation also a trigger.
bool isShowConfetti = false;
late SMITrigger confetti;
Replace the TODO: Confetti animation
with the following code.
isShowConfetti
? CustomPositioned(
child: Transform.scale(
scale: 7,
child: RiveAnimation.asset(
"assets/RiveAssets/confetti.riv",
onInit: (artboard) {
StateMachineController? controller = StateMachineController.fromArtboard(artboard, "State Machine 1");
artboard.addController(controller!);
confetti = controller.findSMI("Trigger explosion") as SMITrigger;
},
),
),
)
: const SizedBox(),
We have added the animation and set the confetti
trigger. It’s now time to activate it. To do this, go to the signIn
function and replace the TODO: Fire confetti animation
with
confetti.fire();
There is just one more thing we need to do. At first, we need to set isShowConfetti
to true
. Replace the TODO: Show confetti animation
with the following code to do this
isShowConfetti = true;
🔥 Bonus — Shop UI Kit
Looking to create a stunning online store? Look no further than FlutterShop, the premium UI kit for Flutter that lets you build beautiful, intuitive, and professional-grade e-commerce app with ease.
We have completed all the necessary steps. Once the sign in is successful, you will probably want to navigate to the next page. To do this, replace the TODO: Navigate to next screen
with the destination screen you want to navigate to. Check the example, I have added a 1 second delay because the confetti animation takes 1 second to complete. Once it is done, we will navigate to the new page.
Future.delayed(
Duration(seconds: 1),
() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => // destination screen
),
);
},
);
We have finished episode 2. Thank you for reading this article. In the next episode, I will show you how to create an animated bottom navigation bar in Flutter.
🔥 Complete source code https://cutt.ly/b14ZAuh (Patreon)
Working on the Episode 3…
I would love to hear your thoughts on this tutorial and how you found it helpful. If you enjoyed it and found it useful, please don’t forget to give it a clap 👏. Your feedback is important to me and helps me to create content that is valuable and useful to you. Thank you for taking the time to read this tutorial, and I hope that it has been helpful in your animation journey with Rive and Flutter.