Build an onboarding experience for your mobile apps with Flutter - Part 2: Build the carousel

Introduction

Introduction

Hi folks! I guess you have been looking for this second part right ? 😏 Well, here we are. This is the second part of the tutorial series. If you are here it means you have completed the first part and already have a beautiful working app. We’re going to give more control to our app users by providing slides’ carousel indicators, and allowing our users to switch to a particular screen by clicking on the corresponding carousel indicator.

Prerequisites

To follow along with the tutorial, you should have the following:

Now this is the code for the dots section. Copy and paste it after the MyHomePageState class.

1//..lib/main.dart
2    
3    class Dots extends StatelessWidget {
4      final IndexController controller;
5      final int slideIndex;
6      final int numberOfDots;
7      
8      Dots({this.controller, this.slideIndex, this.numberOfDots});
9      
10      Widget _activeSlide(int index) {
11        return GestureDetector(
12          onTap: () {
13            print('Tapped');
14            // controller.move(index);
15          },
16          child: new Container(
17            child: Padding(
18              padding: EdgeInsets.only(left: 8.0, right: 8.0),
19              child: Container(
20                width: 20.0,
21                height: 20.0,
22                decoration: BoxDecoration(
23                  color: Colors.orangeAccent.withOpacity(.3),
24                  borderRadius: BorderRadius.circular(50.0),
25                ),
26              ),
27            ),
28          ),
29        );
30      }
31      
32      Widget _inactiveSlide(int index) {
33        return GestureDetector(
34          onTap: () {
35            controller.move(index);
36          },
37          child: new Container(
38            child: Padding(
39              padding: EdgeInsets.only(left: 5.0, right: 5.0),
40              child: Container(
41                width: 14.0,
42                height: 14.0,
43                decoration: BoxDecoration(
44                    color: Colors.grey.withOpacity(0.7),
45                    borderRadius: BorderRadius.circular(50.0)),
46              ),
47            ),
48          ),
49        );
50      }
51      
52      List<Widget> _generateDots() {
53        List<Widget> dots = [];
54        for (int i = 0; i < numberOfDots; i++) {
55          dots.add(i == slideIndex ? _activeSlide(i) : _inactiveSlide(i));
56        }
57        return dots;
58      }
59      
60      @override
61      Widget build(BuildContext context) {
62        return Center(
63            child: Row(
64          mainAxisAlignment: MainAxisAlignment.center,
65          children: _generateDots(),
66        ));
67      }
68    }

The Dots class constructor takes an instance of the index controller, the current slide index and the number of carousel indicators to build. We’ll use these variables to control the look and behavior of the carousel indicators.

Next, we have to build indicators for active and inactive slides. So based on the status of the slide the proper icon will be rendered. Therefore we have _activeSlide and _inactiveSlide widgets.

Basically there are composed of a container widget, which has a border radius as its decoration and a color. Both of these widgets have almost same properties. The sole difference is the opacity of the color. Also we wrapped them inside a GestureDetector widget which should enable us to listen to various types of gestures triggered on it (tap, doubleTap, longPress,tapUp, etc). Each of these gestures have a corresponding listener to react accordingly:

1onTap: () {
2            controller.move(index);
3      },

The code above tells the slide controller to move the slides to the page corresponding to the indicator tapped/clicked. Thus, we can control our slides by simply clicking on the dots we provided.

After this step, we need to generate the indicators. The _generateDots function returns a list of indicators based on the numberOfDots provided in the constructor inside the loop. If the current iteration number is equal to the current slide index, we add an _activeWidget to the list, if not we add an _inactiveWidget.

Finally, inside the build method we render and return our indicators inside a centered row so they can be aligned horizontally:

1return Center(
2            child: Row(
3          mainAxisAlignment: MainAxisAlignment.center,
4          children: _generateDots(),
5        ));

Now, we need to add the indicators to our slides. Just paste the following piece of code into the transformerPageView after the last SizedBox widget.

1new ParallaxContainer(
2                          position: info.position,
3                          translationFactor: 500.0,
4                          child: Dots(
5                            controller: controller,
6                            slideIndex: _slideIndex,
7                            numberOfDots: images.length,
8                          ),
9                        )

With all the parts completed, you should have the following :

1import 'package:flutter/material.dart';
2    import 'package:transformer_page_view/transformer_page_view.dart';
3    
4    void main() => runApp(MyApp());
5    class MyApp extends StatelessWidget {
6      // This widget is the root of your application.
7      @override
8      Widget build(BuildContext context) {
9        return MaterialApp(
10          debugShowCheckedModeBanner: false,
11          title: 'Flutter Demo',
12          theme: ThemeData(
13            // This is the theme of your application.
14            //
15            // Try running your application with "flutter run". You'll see the
16            // application has a blue toolbar. Then, without quitting the app, try
17            // changing the primarySwatch below to Colors.green and then invoke
18            // "hot reload" (press "r" in the console where you ran "flutter run",
19            // or simply save your changes to "hot reload" in a Flutter IDE).
20            // Notice that the counter didn't reset back to zero; the application
21            // is not restarted.
22            primarySwatch: Colors.blue,
23          ),
24          home: MyHomePage(title: 'Flutter Demo Home Page'),
25        );
26      }
27    }
28    class MyHomePage extends StatefulWidget {
29      final String title;
30      MyHomePage({this.title});
31      @override
32      MyHomePageState createState() {
33        return new MyHomePageState();
34      }
35    }
36    
37    class MyHomePageState extends State<MyHomePage> {
38      int _slideIndex = 0;
39      final GlobalKey<ScaffoldState> _key = new GlobalKey<ScaffoldState>();
40      final List<String> images = [
41        "assets/slide_1.png",
42        "assets/slide_2.png",
43        "assets/slide_3.png",
44        "assets/slide_4.png"
45      ];
46      
47      List<Color> colors = [Colors.orange];
48      final List<String> text0 = [
49        "Welcome in your app",
50        "Enjoy teaching...",
51        "Showcase your skills",
52        "Friendship is great"
53      ];
54      
55      final List<String> text1 = [
56        "App for food lovers, satisfy your taste",
57        "Find best meals in your area, simply",
58        "Have fun while eating your relatives and more",
59        "Meet new friends from all over the world"
60      ];
61      
62      final IndexController controller = IndexController();
63      @override
64      Widget build(BuildContext context) {
65        TransformerPageView transformerPageView = TransformerPageView(
66            pageSnapping: true,
67            onPageChanged: (index) {
68              setState(() {
69                this._slideIndex = index;
70              });
71            },
72            loop: false,
73            controller: controller,
74            transformer: new PageTransformerBuilder(
75                builder: (Widget child, TransformInfo info) {
76              return new Material(
77                color: Colors.white,
78                elevation: 8.0,
79                textStyle: new TextStyle(color: Colors.white),
80                borderRadius: new BorderRadius.circular(12.0),
81                child: new Container(
82                  alignment: Alignment.center,
83                  color: Colors.white,
84                  child: Padding(
85                    padding: const EdgeInsets.all(18.0),
86                    child: Column(
87                      mainAxisAlignment: MainAxisAlignment.center,
88                      crossAxisAlignment: CrossAxisAlignment.center,
89                      children: <Widget>[
90                        new ParallaxContainer(
91                          child: new Text(
92                            text0[info.index],
93                            style: new TextStyle(
94                                color: Colors.blueGrey,
95                                fontSize: 34.0,
96                                fontFamily: 'Quicksand',
97                                fontWeight: FontWeight.bold),
98                          ),
99                          position: info.position,
100                          opacityFactor: .8,
101                          translationFactor: 400.0,
102                        ),
103                        SizedBox(
104                          height: 45.0,
105                        ),
106                        new ParallaxContainer(
107                          child: new Image.asset(
108                            images[info.index],
109                            fit: BoxFit.contain,
110                            height: 350,
111                          ),
112                          position: info.position,
113                          translationFactor: 400.0,
114                        ),
115                        SizedBox(
116                          height: 45.0,
117                        ),
118                        new ParallaxContainer(
119                          child: new Text(
120                            text1[info.index],
121                            textAlign: TextAlign.center,
122                            style: new TextStyle(
123                                color: Colors.blueGrey,
124                                fontSize: 28.0,
125                                fontFamily: 'Quicksand',
126                                fontWeight: FontWeight.bold),
127                          ),
128                          position: info.position,
129                          translationFactor: 300.0,
130                        ),
131                        SizedBox(
132                          height: 55.0,
133                        ),
134                        new ParallaxContainer(
135                          position: info.position,
136                          translationFactor: 500.0,
137                          child: Dots(
138                            controller: controller,
139                            slideIndex: _slideIndex,
140                            numberOfDots: images.length,
141                          ),
142                        )
143                      ],
144                    ),
145                  ),
146                ),
147              );
148            }),
149            itemCount: 4);
150            
151        return Scaffold(
152          backgroundColor: Colors.white,
153          body: transformerPageView,
154        );
155      }
156    }
157    class Dots extends StatelessWidget {
158    
159      final IndexController controller;
160      final int slideIndex;
161      final int numberOfDots;
162      Dots({this.controller, this.slideIndex, this.numberOfDots});
163      
164      List<Widget> _generateDots() {
165        List<Widget> dots = [];
166        for (int i = 0; i < numberOfDots; i++) {
167          dots.add(i == slideIndex ? _activeSlide(i) : _inactiveSlide(i));
168        }
169        return dots;
170      }
171      
172      Widget _activeSlide(int index) {
173        return GestureDetector(
174          onTap: () {
175            print('Tapped');
176          },
177          child: new Container(
178            child: Padding(
179              padding: EdgeInsets.only(left: 8.0, right: 8.0),
180              child: Container(
181                width: 20.0,
182                height: 20.0,
183                decoration: BoxDecoration(
184                  color: Colors.orangeAccent.withOpacity(.3),
185                  borderRadius: BorderRadius.circular(50.0),
186                ),
187              ),
188            ),
189          ),
190        );
191      }
192      
193      Widget _inactiveSlide(int index) {
194        return GestureDetector(
195          onTap: () {
196            controller.move(index);
197          },
198          child: new Container(
199            child: Padding(
200              padding: EdgeInsets.only(left: 5.0, right: 5.0),
201              child: Container(
202                width: 14.0,
203                height: 14.0,
204                decoration: BoxDecoration(
205                    color: Colors.grey.withOpacity(0.7),
206                    borderRadius: BorderRadius.circular(50.0)),
207              ),
208            ),
209          ),
210        );
211      }
212      
213      @override
214      Widget build(BuildContext context) {
215        return Center(
216            child: Row(
217          mainAxisAlignment: MainAxisAlignment.center,
218          children: _generateDots(),
219        ));
220      }
221    }

The code is pretty concise for the nice work we’ve achieved. Flutter helps us build this awesome app with no hassle and with a minimum effort 😄 .

We are done with this tutorial. Run your app with this command in your terminal: flutter run and see the magic happen 🙃

flutter-onboarding-part-2-1

Conclusion

This is the end of the tutorial series. I do hope it has been useful to you, and you can apply the knowledge acquired to build beautiful apps 😌 . The source code for this part is available here; feel free to fork it and modify it as per your needs.