Showcase and highlight your widgets using ShowCaseView

Flutter Aug 12, 2023

Make your Flutter application more user-friendly by using the showcaseview package. The package enables you to highlight essential widgets and functionalities. This post shows you how to install and use the package. We will go over different implementations to start the showcase automatically or with a simple tap. The showcase we will be building will be for an authentication page.

flutter_show_case_view_email_field

Installing

To get started, we need to install the showcaseview package into our project. The installation process is simple. Just execute the following command: flutter pub add showcaseview.

Once the command is executed, make sure to check your pubspec.yaml file for the added dependencies. You should see the showcaseview package included in the dependencies section, like this:

dependencies:
  flutter:
    sdk: flutter
  showcaseview: ^2.0.3

Creating the authentication screen

In this post, we will be creating a showcase for an authentication screen. This authentication screen is a result of previous work discussed in another post. If you are interested in learning more, you can check out that related post:

How to Create an Authentication Screen in Flutter
In this tutorial, we will learn how to create an authentication screen in Flutter. An authentication screen is an essential part of many mobile applications, as it allows users to sign in or register for an account. We will build a simple authentication screen with email and password fields

However, we are also going to cover everything you need right here, including the code.

Folder structure

First, take a look at the folder structure for the authentication screen:

|-- lib/
  |-- screens/
    |-- authentication_screen.dart 
  |-- widgets/
    |-- authentication_text_form_field.dart
    |-- wave.dart 
  |-- main.dart 

Code

Once you created the above folder structure including the files, you can copy the code below into each file.

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_authentication_screen/screens/authentication_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Authentication Screen',
      theme: ThemeData(
        primaryColor: Colors.blueAccent,
        colorScheme: ThemeData().colorScheme.copyWith(
          primary: Colors.blueAccent,
        ),
      ),
      home: const AuthenticationScreen(),
    );
  }
}

authentication_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_authentication_screen/widgets/authentication_text_form_field.dart';
import 'package:flutter_authentication_screen/widgets/wave.dart';

class AuthenticationScreen extends StatefulWidget {
  const AuthenticationScreen({Key? key}) : super(key: key);

  @override
  State<AuthenticationScreen> createState() => _AuthenticationScreenState();
}

class _AuthenticationScreenState extends State<AuthenticationScreen> {
  final GlobalKey<FormState> _formKey = GlobalKey();
  final emailController = TextEditingController();
  final passwordController = TextEditingController();
  final passwordConfirmationController = TextEditingController();
  bool register = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          const Wave(),
          Form(
            key: _formKey,
            child: Padding(
              padding: const EdgeInsets.only(left: 25.0, right: 25.0),
              child: Column(
                children: [
                  const SizedBox(height: 25),
                  AuthenticationTextFormField(
                    icon: Icons.email,
                    label: 'Email',
                    textEditingController: emailController,
                  ),
                  AuthenticationTextFormField(
                    icon: Icons.vpn_key,
                    label: 'Password',
                    textEditingController: passwordController,
                  ),
                  if (register == true)
                    AuthenticationTextFormField(
                      icon: Icons.password,
                      label: 'Password Confirmation',
                      textEditingController: passwordConfirmationController,
                    ),
                  const SizedBox(height: 25),
                  ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      minimumSize: const Size.fromHeight(50),
                      shape: const StadiumBorder(),
                    ),
                    onPressed: () => {},
                    child: Text(
                      register == true ? 'Register' : 'Login',
                      style: const TextStyle(fontSize: 17.5),
                    ),
                  ),
                  const SizedBox(height: 20),
                  InkWell(
                    onTap: () => setState(() {
                      register = !register;
                      _formKey.currentState?.reset();
                    }),
                    child: Text(
                      register == true ? 'Login instead' : 'Register instead',
                    ),
                  )
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}

authentication_text_form_field.dart

import 'package:flutter/material.dart';

class AuthenticationTextFormField extends StatelessWidget {
  const AuthenticationTextFormField({
    Key? key,
    required this.icon,
    required this.label,
    required this.textEditingController,
  }) : super(key: key);

  final IconData icon;
  final String label;
  final TextEditingController textEditingController;

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: textEditingController,
      obscureText: label.toLowerCase().contains('password'),
      decoration: InputDecoration(
        floatingLabelStyle: const TextStyle(fontSize: 20),
        icon: Icon(
          icon,
          color: Theme.of(context).primaryColor,
        ),
        labelText: label,
      ),
    );
  }
}

wave.dart

import 'package:flutter/material.dart';

class Wave extends StatelessWidget {
  const Wave({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper: WaveClipper(),
      child: Container(
        color: Theme.of(context).primaryColor,
        height: 200,
      ),
    );
  }
}

class WaveClipper extends CustomClipper<Path> {
  @override
  getClip(Size size) {
    var path = Path();
    path.lineTo(0, 175);

    path.quadraticBezierTo(size.width * 0.25, size.height * 0.50 - 25,
        size.width * 0.50, size.height * 0.75);

    path.quadraticBezierTo(
        size.width * 0.75, size.height + 25, size.width, size.height * 0.75);

    path.lineTo(size.width, 0);
    path.close();

    return path;
  }

  @override
  bool shouldReclip(CustomClipper oldClipper) {
    return false;
  }
}

Once you have followed these steps and built your application, you should see the resulting screen. In this case, this will be the starting point of the showcase implementation.

flutter_authentication_screen

Creating the showcase

To start with the implementation we need to wrap every widget we want to showcase with the Showcase widget, see the code below:

authentication_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_authentication_screen/widgets/authentication_text_form_field.dart';
import 'package:flutter_authentication_screen/widgets/wave.dart';
import 'package:showcaseview/showcaseview.dart';

class AuthenticationScreen extends StatefulWidget {
  const AuthenticationScreen({Key? key}) : super(key: key);

  @override
  State<AuthenticationScreen> createState() => _AuthenticationScreenState();
}

class _AuthenticationScreenState extends State<AuthenticationScreen> {
  final GlobalKey<FormState> _formKey = GlobalKey();
  final emailController = TextEditingController();
  final passwordController = TextEditingController();
  final passwordConfirmationController = TextEditingController();
  bool register = true;

  final GlobalKey _emailKey = GlobalKey();
  final GlobalKey _passwordKey = GlobalKey();
  final GlobalKey _submitKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) =>
        ShowCaseWidget.of(context).startShowCase([_emailKey, _passwordKey, _submitKey])
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        child: Column(
          children: [
            const Wave(),
            Form(
              key: _formKey,
              child: Padding(
                padding: const EdgeInsets.only(left: 25.0, right: 25.0),
                child: Column(
                  children: [
                    const SizedBox(height: 25),
                    Showcase(
                      key: _emailKey,
                      title: 'Email Showcase',
                      description: 'This is where you enter your email.',
                      child: AuthenticationTextFormField(
                        key: const Key('email'),
                        icon: Icons.email,
                        label: 'Email',
                        textEditingController: emailController,
                      ),
                    ),
                    Showcase(
                      key: _passwordKey,
                      title: 'Password Showcase',
                      description: 'This is where you enter your password.',
                      child: AuthenticationTextFormField(
                        key: const Key('password'),
                        icon: Icons.vpn_key,
                        label: 'Password',
                        textEditingController: passwordController,
                      ),
                    ),
                    if (register == true)
                      AuthenticationTextFormField(
                        key: const Key('password_confirmation'),
                        icon: Icons.password,
                        label: 'Password Confirmation',
                        textEditingController: passwordConfirmationController,
                      ),
                    const SizedBox(height: 25),
                    Showcase(
                      key: _submitKey,
                      title: 'Authentication Button',
                      description: 'Tap here to register or login.',
                      child: ElevatedButton(
                        style: ElevatedButton.styleFrom(
                          minimumSize: const Size.fromHeight(50),
                          shape: const StadiumBorder(),
                        ),
                        onPressed: () => {},
                        child: Text(
                          register == true ? 'Register' : 'Login',
                          style: const TextStyle(fontSize: 17.5),
                        ),
                      ),
                    ),
                    const SizedBox(height: 20),
                    InkWell(
                      onTap: () => setState(() {
                        register = !register;
                      }),
                      child: Text(
                        register == true ? 'Login instead' : 'Register instead',
                      ),
                    )
                  ],
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

In this code snippet, we updated the AuthenticationScreen widget to use the showcaseview package to provide a showcase of three widgets of the authentication screen. The following changes have been made:

1. Added showcaseview import: This import is necessary to use the showcaseview package.

import 'package:showcaseview/showcaseview.dart';

2. Added key variables: _emailKey, _passwordKey, _submitKey: These keys are used to identify the widgets that will be showcased. Each key corresponds to a specific widget on the screen.

final GlobalKey _emailKey = GlobalKey();
final GlobalKey _passwordKey = GlobalKey();
final GlobalKey _submitKey = GlobalKey();

3. Added initstate method: The initState method has been added to the _AuthenticationScreenState class. It is responsible for starting the showcase after the screen has been built. It uses the ShowCaseWidget.of(context).startShowCase method to initiate the showcase and pass in the keys of the widgets to be showcased.

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) =>
      ShowCaseWidget.of(context).startShowCase([_emailKey, _passwordKey, _submitKey])
  );
}

4. Wrapped widgets with Showcase widget: The Showcase widget has been wrapped around certain widgets, such as the email field, password field, and authentication button. This wraps these widgets with the showcase effect, and the package will highlight and provide information about them. Each Showcase widget includes a key, title and description property. The key is needed to start the Showcase widget and the title and description will be displayed during the showcase, explaining the purpose of the widget to the user.

Email and Password field

Showcase(
  key: _emailKey,
  title: 'Email Showcase',
  description: 'This is where you enter your email.',
  child: AuthenticationTextFormField(
    key: const Key('email'),
    icon: Icons.email,
    label: 'Email',
    textEditingController: emailController,
  ),
),
Showcase(
  key: _passwordKey,
  title: 'Password Showcase',
  description: 'This is where you enter your password.',
  child: AuthenticationTextFormField(
    key: const Key('password'),
    icon: Icons.vpn_key,
    label: 'Password',
    textEditingController: passwordController,
  ),
),

Authentication button

Showcase(
  key: _submitKey,
  title: 'Authentication Button',
  description: 'Tap here to register or login.',
  child: ElevatedButton(
    style: ElevatedButton.styleFrom(
      minimumSize: const Size.fromHeight(50),
      shape: const StadiumBorder(),
    ),
    onPressed: () => {},
    child: Text(
      register == true ? 'Register' : 'Login',
      style: const TextStyle(fontSize: 17.5),
    ),
  ),
),

This concludes the changes to the AuthenticationScreen, however, we still need to make some changes to the main.dart file.

main.dart

In the main.dart file we have to make sure that our AuthenticationScreen widget has access to the context of the ShowCaseWidget, see the code below:

import 'package:flutter/material.dart';
import 'package:flutter_authentication_screen/screens/authentication_screen.dart';
import 'package:showcaseview/showcaseview.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Authentication Screen',
      theme: ThemeData(
        primaryColor: Colors.blueAccent,
        colorScheme: ThemeData().colorScheme.copyWith(
              primary: Colors.blueAccent,
              error: Colors.redAccent,
            ),
      ),
      home: ShowCaseWidget(
        builder: Builder(
          builder: (BuildContext buildContext) => const AuthenticationScreen()
        ),
      ),
    );
  }
}

In this code snippet, we wrap our AuthenticationScreen widget with the ShowCaseWidget. The ShowCaseWidget takes a builder. On this builder property we assigned a Builder and build our AuthenticationScreen widget.

Starting the showcase manually

So far we have created an implementation that will automatically start the showcase. It is also possible to start the showcase manually:

Align(
  alignment: Alignment.topRight,
  child: InkWell(
    onTap: () => ShowCaseWidget.of(context).startShowCase([_emailKey, _passwordKey, _submitKey]),
    child: Icon(
      Icons.question_mark,
      color: Theme.of(context).primaryColor,
    ),
  ),
),

In this code snippet, we created a small clickable Icon widget at the top-right corner. When you tap the icon it will start the showcase using the ShowCaseWidget.of(context).startShowCase function.

flutter_manually_starting_showcase_with_tap_on_icon

Conclusion

In this post, we went over the showcaseview package to make our Flutter application more user-friendly. It helps to highlight important parts of your application for users. We covered how to install it and how to use it in different ways. We focused on an example where we improved an authentication screen.

We talked about the steps to set up the screen and the code needed for that. We explored how to make the showcase for the email field, password field, and login button. Finally, we discussed how to start the showcase either automatically or when a user taps an icon.

Tags