Back

Adding in-app tours to Flutter apps

Adding in-app tours to Flutter apps

User Interfaces and app features can be quite complicated to grasp at first sight for new users. Still, with the advent of In-App Tours, specific user interfaces and features can be broken down to users, eliminating the need, stress, and time taken for hectic app exploration.

An in-app tour is a feature that helps users understand and navigate a new app by providing a guided tour of its various features and functions. The tour is typically presented as a series of overlays or pop-ups that appear on the screen, each highlighting a specific feature and providing a brief description or tutorial on how to use it. They can also help introduce users to new features or updates in an existing app.

In this article, we’ll learn how to create an In-app tour using the tutorial_coach_mark package in our Flutter application.

Scaffolding Flutter app

Let’s begin with creating our Flutter project for this application. First, run the command below in your terminal to scaffold a Flutter project.

flutter create tour_app

To go along with this tutorial, we’ll be using the tutorial_coach_mark package. Run the command below to install it.

flutter pub add tutorial_coach_mark

Building User Interface

Let’s start with creating our bank page. To achieve this, create a bank_page.dart file in the lib/ folder and then paste the code block below into it.

import 'package:flutter/material.dart';
import 'package:tour_app/tour_target.dart';

class BankPage extends StatefulWidget {
  const BankPage({super.key});
  @override
  State<BankPage> createState() => _BankPageState();
}

class _BankPageState extends State<BankPage> {
  final moneyKey = GlobalKey();
  final withdrawKey = GlobalKey();
  final investKey = GlobalKey(); 


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF292D32),
      body: Center(
        child: Container(
          margin: const EdgeInsets.symmetric(horizontal: 30),
          decoration: BoxDecoration(
            boxShadow: [
              BoxShadow(
                  color: Colors.white.withOpacity(0.1),
                  offset: const Offset(-6.0, -6.0),
                  blurRadius: 16.0),
              BoxShadow(
                  color: Colors.black.withOpacity(0.4),
                  offset: const Offset(6.0, 6.0),
                  blurRadius: 16.0),
            ],
            color: const Color(0xFF292D32),
            borderRadius: BorderRadius.circular(12.0),
          ),
          padding: const EdgeInsets.all(20),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('Total Amount:\n \$240,000,000',
                  key: moneyKey,
                  style: const TextStyle(
                      color: Color.fromARGB(255, 212, 212, 212),
                      fontWeight: FontWeight.bold,
                      fontSize: 25)),
              const SizedBox(height: 50),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  ElevatedButton(
                      key: withdrawKey,
                      onPressed: () {},
                      style:
                          ElevatedButton.styleFrom(backgroundColor: Colors.red),
                      child: const Text('Withdraw',
                          style: TextStyle(color: Colors.white))),
                  ElevatedButton(
                      key: investKey,
                      onPressed: () {},
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.green,
                      ),
                      child: const Text('Invest',
                          style: TextStyle(color: Colors.white))),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

In the code block above, we’re creating the BankScreen page, a stateful flutter widget. The _BankPageState widget overrides a build function that returns a Scaffold widget with a background color of Color(0xFF292D32). Inside the Scaffold widget is the SafeArea widget with a child of Padding that has a Center widget as its child. Inside the Center widget is the Container widget with a Column widget as its child. The Column widget has children with a Text widget displaying a message, a SizedBox widget for spacing, and a Row widget with two ElevatedButton widgets. The ElevatedButton widgets are labeled “Withdraw” and “Invest,” respectively, and have onPressed callbacks that are currently empty. The Container widget and the Text and ElevatedButton widgets have GlobalKeys assigned to them.

Below is a preview of our BankScreen.

-

Building Home page

With our bank page done, let’s create our home screen to display some interactive widgets. To accomplish this, navigate to the lib/ folder, create a home_screen.dart file, and paste the code below.

import 'package:flutter/material.dart';
import 'package:tour_app/bank_page.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
  final wealthKey = GlobalKey();
  final profileKey = GlobalKey();
  final buttonKey = GlobalKey();
  bool reveal = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xff040404),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
                const Text('Home',
                    style: TextStyle(color: Colors.white, fontSize: 22)),
                IconButton(
                    key: bankKey,
                    onPressed: () => Navigator.push(
                        context,
                        MaterialPageRoute(
                            builder: (context) => const BankPage())),
                    color: Colors.white,
                    icon: const Icon(Icons.safety_check_rounded)),
              ]),
              const Spacer(),
              const Text('You are Rich !!',
                  style: TextStyle(
                      fontSize: 26,
                      fontWeight: FontWeight.bold,
                      color: Colors.white)),
              const SizedBox(height: 10),
              Center(
                  child: FlutterLogo(
                      key: wealthKey,
                      size: MediaQuery.of(context).size.height * 0.3)),
              const SizedBox(height: 40),
              ElevatedButton(
                  key: buttonKey,
                  onPressed: () => setState(() => reveal = !reveal),
                  child: const Text('Find out More...')),
              const SizedBox(height: 20),
              Visibility(
                visible: reveal,
                child: Container(
                  padding: const EdgeInsets.all(20),
                  color: Colors.white,
                  child: const Text(
                    'Your Diamond is Worth\n \$34,000,000',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 22,
                    ),
                  ),
                ),
              ),
              const Spacer(),
            ],
          ),
        ),
      ),
    );
  }
}

In the code block above, we’ve created several GlobalKey objects in the _HomePageState class, which uniquely identify elements within the widget tree and allow those elements to be manipulated. We’ve also created a boolean variable, reveal, which controls the visibility of a piece of text. The build method, which is executed whenever the state of the widget changes, returns a Scaffold widget that contains a number of other widgets, including text labels, images, and buttons.

The ElevatedButton widget has an onPressed callback that toggles the value of reveal, which is used to toggle the visibility of the Container widget.

When the button is pressed, it will call setState (() => reveal = !reveal), which will cause the build method to be called again, and the Visibility widget will re-render based on the updated value of reveal, thus making the container with text visible or invisible.

Finally, IconButton displays the safety_check_rounded icon, and when clicked, it routes to the BankPage.

Updating Main.dart

Next, Let’s update our main.dart file to set our initial route to the home screen we created earlier. Copy the code below and replace it with the entire code in the main.dart file.

import 'package:flutter/material.dart';
import 'package:tour_app/home_page.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'In-App Tour',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.teal,
      ),
      home: const HomePage(),
    );
  }
}

In the code block above, we’re setting our home to the HomePage widget we created earlier. We’ve also updated the title and primarySwatch color to teal.

Next, run the application on an emulator using the below command to review what we’ve done.

flutter run 

-

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

Configuring In-App Tour

Now that our Home screen has been successfully created, let’s configure our in-app tour. To achieve this, create a tour_target.dart file in the lib/ folder and paste the code below.

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

List<TargetFocus> addTourTargets({
  required GlobalKey bankKey,
  required GlobalKey wealthKey,
  required GlobalKey buttonKey,
}) {
  List<TargetFocus> targets = [];

  targets.add(
    TargetFocus(
      keyTarget: bankKey,
      alignSkip: Alignment.bottomLeft,
      shape: ShapeLightFocus.Circle,
      radius: 10,
      contents: [
        TargetContent(
          align: ContentAlign.bottom,
          builder: (context, controller) => Container(
            alignment: Alignment.center,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: const [
                Text('This is your Bank!!',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      color: Colors.white,
                      fontSize: 20,
                    ))
              ],
            ),
          ),
        )
      ],
    ),
  );

  targets.add(
    TargetFocus(
      keyTarget: wealthKey,
      alignSkip: Alignment.topRight,
      shape: ShapeLightFocus.RRect,
      radius: 10,
      contents: [
        TargetContent(
          align: ContentAlign.bottom,
          builder: (context, controller) => Container(
            alignment: Alignment.center,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: const [
                Text(
                  'Hey!!\n This is your Worth',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.white,
                    fontSize: 20,
                  ),
                )
              ],
            ),
          ),
        )
      ],
    ),
  );

  targets.add(
    TargetFocus(
      keyTarget: buttonKey,
      alignSkip: Alignment.topRight,
      shape: ShapeLightFocus.RRect,
      radius: 10,
      contents: [
        TargetContent(
          align: ContentAlign.bottom,
          builder: (context, controller) => Container(
            alignment: Alignment.center,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: const [
                Text(
                  'Click to find out how much you worth',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    color: Colors.white,
                    fontWeight: FontWeight.bold,
                    fontSize: 20,
                  ),
                ),
              ],
            ),
          ),
        )
      ],
    ),
  );

  return targets;
}

In the code block above, we create a separate function that returns a list of TargetFocus objects. TargetFocus objects are used to display the tutorial coach mark in the TutorialCoachMark package.

The addTourTarget function is created with three required parameters of GlobalKey type: bankKey, wealthKey, and buttonKey. These GlobalKey objects are used to identify specific elements of the UI in the HomeScreen widget, which the coach marks will be displayed on.

The function then creates four TargetFocus objects, one for each of the keys passed as arguments. Each TargetFocus object defines a target on the UI by specifying the key of the element it should be displayed on, the alignment of the coach mark relative to the target, the shape of the highlight, a radius, and the content of the coach mark.

Finally, the list of all TargetFocus objects are then returned after they are created.

Configuring In-app tour for Bank page

Let’s configure our in-app tour that will be displayed on the bank page. To achieve this, create a bank_tour_target.dart file in the lib/ folder and paste the code below into it.

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

List<TargetFocus> addBankTargets({
 required GlobalKey moneyKey,
  required GlobalKey withdrawKey,
  required GlobalKey investKey,
}) {
  List<TargetFocus> targets = [];
  
   targets.add(
    TargetFocus(
      keyTarget: moneyKey,
      alignSkip: Alignment.bottomLeft,
      shape: ShapeLightFocus.RRect,
      radius: 10,
      contents: [
        TargetContent(
          align: ContentAlign.top,
          builder: (context, controller) => Container(
            alignment: Alignment.center,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: const [
                Text(
                  'This is your Current amount',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.black,
                    fontSize: 25,
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  );

  targets.add(
    TargetFocus(
      keyTarget: withdrawKey,
      alignSkip: Alignment.topRight,
      shape: ShapeLightFocus.RRect,
      radius: 10,
      contents: [
        TargetContent(
          align: ContentAlign.bottom,
          builder: (context, controller) => Container(
            alignment: Alignment.center,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: const [
                Text(
                  'You can withdraw your cash!!',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.black,
                    fontSize: 25,
                  ),
                )
              ],
            ),
          ),
        )
      ],
    ),
  );

  targets.add(
    TargetFocus(
      keyTarget: investKey,
      alignSkip: Alignment.topRight,
      shape: ShapeLightFocus.RRect,
      radius: 10,
      contents: [
        TargetContent(
          align: ContentAlign.bottom,
          builder: (context, controller) => Container(
            alignment: Alignment.center,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: const [
                Text(
                  'Or, invest it and get a double',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    color: Colors.black,
                    fontWeight: FontWeight.bold,
                    fontSize: 25,
                  ),
                )
              ],
            ),
          ),
        )
      ],
    ),
  );

  return targets;
}

As seen in the code block above, we are utilizing the same configuration pattern that we previously discussed in the previous section to personalize our bank tour.

Configuring Targets on the Home Screen

With our targets all set, Let’s initialize the tour targets on our Home page. To achieve this, import the Tutorial Coach Mark package and the tour_target file into the Home page.

import 'package:tour_app/tour_target.dart';
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';

Then, copy and paste the code below into the _HomePageState class.


class _HomePageState extends State<HomePage> {
  final wealthKey = GlobalKey();
  final bankKey= GlobalKey();
  final buttonKey = GlobalKey();
  bool reveal = false;

//-------> Copy from here
  late TutorialCoachMark _tutorialCoachMark;
  void _initialPageTour() {
    _tutorialCoachMark = TutorialCoachMark(
        targets: addTourTargets(
            bankKey: bankKey,
            wealthKey: wealthKey,
            buttonKey: buttonKey),
        colorShadow: Colors.teal,
        paddingFocus: 10,
        hideSkip: false,
        opacityShadow: 0.8,
        onFinish: () => Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => const BankPage(),
            )),
        onSkip: () => print('Skipped'));
  }
  void _ShowTour() => Future.delayed(const Duration(seconds: 1),
      () => _tutorialCoachMark.show(context: context)); 

  @override
  void initState() {
    super.initState();
    _initialPageTour();
    _ShowTour();
  }
//-------> To this point

  @override
  Widget build(BuildContext context) {
  // Other code blocks here
  ...

  }
}

This code creates and shows a tutorial coach mark for our Home screen using the TutorialCoachMark class.

The _initialPageTour method creates an instance of TutorialCoachMark and assigns it to the _tutorialCoachMark variable. It sets several properties of the coach mark, such as the targets that will be highlighted during the tour (which are provided by the addTourTargets method), the color of the shadow, the padding around the focus widget, whether the skip button is hidden, and the opacity of the shadow. It also sets two callbacks, onFinish and onSkip, which will be called when the user finishes the tour or skips the tour, respectively. When the tour finishes, the onFinished callback will navigate to the bank screen.

The _ShowTour method is responsible for showing the coach mark. It uses Future.delayed to delay the showing of the coach mark for 1 second, and then calls the show method on the _tutorialCoachMark object, passing in the context of the widget.

Finally, the initState method is responsible for initializing the coach mark and showing it when the HomePage is first created.

Configuring Targets on Bank Screen

With our home page ready, Let’s initialize the tour targets on our bank page. To achieve this, navigate to the bank_page.dart file and import the bank_tour_targer and tutorial_coach_mark into it.

import 'package:tour_app/bank_tour_target.dart';
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';

Then, copy the code below into the _BankPageState class.

class _BankPageState extends State<BankPage> {
  final moneyKey = GlobalKey();
  final withdrawKey = GlobalKey();
  final investKey = GlobalKey();

  late TutorialCoachMark _tutorialCoachMark;

  void _initialPageTour() {
    _tutorialCoachMark = TutorialCoachMark(
      targets: addBankTargets(
          moneyKey: moneyKey, withdrawKey: withdrawKey, investKey: investKey),
      colorShadow: Colors.white,
      paddingFocus: 10,
      hideSkip: false,
      opacityShadow: 0.3,
      onFinish: () => print('Finished Tour'),
      onSkip: () => print('Skipped'),
    );
  }

  void _ShowTour() => Future.delayed(const Duration(seconds: 1),
      () => _tutorialCoachMark.show(context: context));

  @override
  void initState() {
    super.initState();

    _initialPageTour();
    _ShowTour();
  }


  @override
  Widget build(BuildContext context) {
    
  ...

In the code block above, we’re initializing the Tutorial Coach Mark package on our Bank page using the same process used on the home page.

Finally, restart the application to see the final result.

-

Conclusion

The In-App Tour is an essential feature in mobile development since it’s widely used in the mobile development world to give first-time users instructions, hints, and tutorials on how to use an application without the help of a knowledge base or documentation.

Here is the link to the complete source code on GitHub.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs and track user frustrations. Get complete visibility into your frontend with OpenReplay, the most advanced open-source session replay tool for developers.

OpenReplay