Adding in-app tours to Flutter apps
Tags: flutter, mobile
Published: 2023-04-05 23:54:41


by [Sam Victor](https://blog.openreplay.com/authors/sam-victor)


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](https://pub.dev/packages/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.

```dart
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`.

![-](https://blog.openreplay.com/images/adding-in-app-tours-to-flutter-apps/images/K43TlRK.png)

### 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.

```dart
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.

```dart
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 
```

![-](https://blog.openreplay.com/images/adding-in-app-tours-to-flutter-apps/images/j6igQXR.gif)


<h2>Session Replay for Developers</h2> 
<p><em>
Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data
</em></p>
<img alt="OpenReplay" width="768" height="400" src="https://blog.openreplay.com/assets/overview_Z17Qdyg.png" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em>





## 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.

```dart
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.

```dart
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.

``` dart
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.

```dart

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.

``` dart
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.

```dart
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.

![-](https://blog.openreplay.com/images/adding-in-app-tours-to-flutter-apps/images/Jsij1CS.gif)


## 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](https://github.com/iamVictorSam/tour_app) on GitHub.





[![newsletter](https://blog.openreplay.com/newsletter_Z1P0UIP.png)](https://newsletter.openreplay.com/)