Peter
Peter Software Developer | Technical Writer | Actively helping users with their questions on Stack Overflow. Occasionally I post here and on other platforms.

Creating a Timeline Component in Flutter

Creating a Timeline Component in Flutter
Follow me on

In this article, we will use the Stack widget and other Flutter widgets to create a timeline user interface.

Creating the Timeline UI Design

The following is the user interface:

timeline flutter

So first let’s create the app bar, before adding the code you need to create a new flutter project by executing:

1
flutter create timeline_widget

Creating The App Bar

First create a file called constants.dart which will contain the following:

1
2
3
4
5
6
7
import 'package:flutter/material.dart';

class Constants {
  static const kPurpleColor = Color(0xFFB97DFE);
  static const kRedColor = Color(0xFFFE4067);
  static const kGreenColor = Color(0xFFADE9E3);
}

we will use those colors in the timeline user interface. Next remove all the comments from the main.dart file and click format document. Then assign kPurpleColor to the primaryColor property in ThemeData:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Timeline',
      theme: ThemeData(
        primaryColor: Constants.kPurpleColor,
      ),
      home: TimelineComponent(title: 'Timeline'),
    );
  }
}

The TimelineComponent will extend the StatelessWidget, which means we need to override the build method. Now, inside the build method do the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
return Scaffold(
  drawer: Drawer(),
    appBar: AppBar(title: Text(this.title!), actions: [
    Padding(
        padding: const EdgeInsets.fromLTRB(0, 10, 10, 0),
        child: Stack(
        children: [
            CircleAvatar(
            backgroundColor: Colors.blue.shade200,
            child: const Text('PH'),
            ),
            new Positioned(
            right: 0,
            top: 0,
            child: new Container(
                padding: EdgeInsets.all(1),
                decoration: new BoxDecoration(
                color: Colors.red,
                borderRadius: BorderRadius.circular(6),
                ),
                constraints: BoxConstraints(
                minWidth: 12,
                minHeight: 12,
                ),
            ),
            )
        ],
        ),
    ),
    ]),

As you can see, we use the actions property which takes a list of widgets. Then we use the Stack widget which is used to overlap several children in a simple way. So, here first we have the CircleAvatar widget at the bottom and then we use a Container widget to create a red circle. Then we use the Positioned widget to position the red circle at the top right of the CircleAvatar.

Creating The Timeline

So now according to the image, the body property of the Scaffold will contain a Column widget. The upper part of the body will contain an image, some text on top of it, and a floating action button while the bottom part will contain the timeline ui.

Therefore, regarding the upper part we can do the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
body: Column(
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: [
    Stack(
      clipBehavior: Clip.none,
      children: [
        Container(
          height: size.height * 0.3,
          width: double.infinity,
          child: Image.asset("assets/images/background.jpeg",
              fit: BoxFit.fitWidth),
        ),
        Positioned(
          top: 40,
          left: 30,
          child: Row(children: <Widget>[
            Text("8",
                style: style.copyWith(fontSize: 70.0)),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                children: [
                  Text("Monday",
                      style: style.copyWith(fontSize: 25.0)),
                  Text("February 2015".toUpperCase(),
                      style: style.copyWith(fontSize: 12.0)),
                ],
              ),
            ),
          ]),
        ),
        Positioned(
          bottom: -20,
          right: 15,
          child: FloatingActionButton(
            onPressed: null,
            child: IconButton(
              icon: Icon(Icons.add, color: Colors.white),
              onPressed: null,
              iconSize: 40.0,
            ),
            backgroundColor: Colors.red,
          ),
        )
      ],
    ),

We use a Stack widget and inside of it we add the image. Then we use a Row and a Column and wrap them in a Positioned widget, to position the Text widget correctly. We also create a global TextStyle object:

1
TextStyle style =  TextStyle(color: Colors.white);

and then use the copyWith() method which creates a copy of this text style but with the given fields replaced with the new values.

Then we create a FloatingActionButton and position it correctly using the Postioned property and we use Clip.none to prevent clipping the floating action button.


Before creating the timeline ui, we need to create some sample data. Therefore create a class called Events:

1
2
3
4
5
6
7
8
class Events {
  final String time;
  final String eventName;
  final String description;

  Events({required this.time, required this.eventName, required this.description});

}

and create some sample data:

1
2
3
4
5
6
  final List<Events> listOfEvents = [
    Events(time: "5pm", eventName: "New Icon", description: "Mobile App"),
    Events(time: "3 - 4pm", eventName: "Design Stand Up", description: "Hangouts"),
    Events(time: "12pm", eventName: "Lunch Break", description: "Main Room"),
    Events(time: "9 - 11am", eventName: "Finish Home Screen", description: "Web App"),
  ];

Now regarding the timeline ui, we can do the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Flexible(
  child: ListView.builder(
      shrinkWrap: true,
      itemCount: listOfEvents.length,
      itemBuilder: (context, i) {
        return Stack(
          children: [
            Padding(
              padding: EdgeInsets.all(40),
              child: Row(
                children: [
                  SizedBox(width: size.width * 0.1),
                  SizedBox(child: Text(listOfEvents[i].time),
                    width: size.width * 0.2,
                  ),
                  SizedBox(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(listOfEvents[i].eventName),
                        Text(
                          listOfEvents[i].description,
                          style: TextStyle(
                              color: Colors.grey, fontSize: 12),
                        )
                      ],
                    ),
                  )
                ],
              ),
            ),
            Positioned(
              left: 50,
              child: new Container(
                height: size.height * 0.7,
                width: 1.0,
                color: Colors.grey.shade400,
              ),
            ),
            Positioned(
              bottom: 5,
              child: Padding(
                padding: const EdgeInsets.all(40.0),
                child: Container(
                  height: 20.0,
                  width: 20.0,
                  decoration: new BoxDecoration(
                    color: listOfColors[random.nextInt(3)],
                    borderRadius: BorderRadius.circular(20),
                  ),
                ),
              ),
            ),
          ],
        );
      }),
),

First, since we are using the listview inside a Column and since both widgets expands to the maximum size vertically therefore we wrap it with a Flexible widget to constraint the height of the listview. Then inside the itemBuilder return a Stack widget. The Stack widget will contain a Row widget, that will also contain the sample data.

Then create a Container widget with width: 1.0, this widget will be the straight line in the timeline, while the other Container containing the BorderRadius.circular(20) will be bullet points in the timeline.

Full Code

main.dart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import 'dart:math';

import 'package:flutter/material.dart';

import 'constants.dart';
import 'events.dart';

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

TextStyle style =  TextStyle(color: Colors.white);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Timeline',
      theme: ThemeData(
        primaryColor: Constants.kPurpleColor,
      ),
      home: TimelineComponent(title: 'Timeline'),
    );
  }
}

class TimelineComponent extends StatelessWidget {
  TimelineComponent({Key? key, this.title}) : super(key: key);

  final String? title;

  final List<Events> listOfEvents = [
    Events(time: "5pm", eventName: "New Icon", description: "Mobile App"),
    Events(time: "3 - 4pm", eventName: "Design Stand Up", description: "Hangouts"),
    Events(time: "12pm", eventName: "Lunch Break", description: "Main Room"),
    Events(time: "9 - 11am", eventName: "Finish Home Screen", description: "Web App"),
  ];

  final List<Color> listOfColors = [Constants.kPurpleColor,Constants.kGreenColor,Constants.kRedColor];

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;
    Random random = new Random();
    return Scaffold(
      drawer: Drawer(),
      appBar: AppBar(title: Text(this.title!), actions: [
        Padding(
          padding: const EdgeInsets.fromLTRB(0, 10, 10, 0),
          child: Stack(
            children: [
              CircleAvatar(
                backgroundColor: Colors.blue.shade200,
                child: const Text('PH'),
              ),
              new Positioned(
                right: 0,
                top: 0,
                child: new Container(
                  padding: EdgeInsets.all(1),
                  decoration: new BoxDecoration(
                    color: Colors.red,
                    borderRadius: BorderRadius.circular(6),
                  ),
                  constraints: BoxConstraints(
                    minWidth: 12,
                    minHeight: 12,
                  ),
                ),
              )
            ],
          ),
        ),
      ]),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Stack(
            clipBehavior: Clip.none,
            children: [
              Container(
                height: size.height * 0.3,
                width: double.infinity,
                child: Image.asset("assets/images/background.jpeg",
                    fit: BoxFit.fitWidth),
              ),
              Positioned(
                top: 40,
                left: 30,
                child: Row(children: <Widget>[
                  Text("8",
                      style: style.copyWith(fontSize: 70.0)),
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Column(
                      children: [
                        Text("Monday",
                            style: style.copyWith(fontSize: 25.0)),
                        Text("February 2015".toUpperCase(),
                            style: style.copyWith(fontSize: 12.0)),
                      ],
                    ),
                  ),
                ]),
              ),
              Positioned(
                bottom: -20,
                right: 15,
                child: FloatingActionButton(
                  onPressed: null,
                  child: IconButton(
                    icon: Icon(Icons.add, color: Colors.white),
                    onPressed: null,
                    iconSize: 40.0,
                  ),
                  backgroundColor: Colors.red,
                ),
              )
            ],
          ),
          Flexible(
            child: ListView.builder(
                shrinkWrap: true,
                itemCount: listOfEvents.length,
                itemBuilder: (context, i) {
                  return Stack(
                    children: [
                      Padding(
                        padding: EdgeInsets.all(40),
                        child: Row(
                          children: [
                            SizedBox(width: size.width * 0.1),
                            SizedBox(
                              child: Text(listOfEvents[i].time),
                              width: size.width * 0.2,
                            ),
                            SizedBox(
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  Text(listOfEvents[i].eventName),
                                  Text(
                                    listOfEvents[i].description,
                                    style: TextStyle(
                                        color: Colors.grey, fontSize: 12),
                                  )
                                ],
                              ),
                            )
                          ],
                        ),
                      ),
                      Positioned(
                        left: 50,
                        child: new Container(
                          height: size.height * 0.7,
                          width: 1.0,
                          color: Colors.grey.shade400,
                        ),
                      ),
                      Positioned(
                        bottom: 5,
                        child: Padding(
                          padding: const EdgeInsets.all(40.0),
                          child: Container(
                            height: 20.0,
                            width: 20.0,
                            decoration: new BoxDecoration(
                              color: listOfColors[random.nextInt(3)],
                              borderRadius: BorderRadius.circular(20),
                            ),
                          ),
                        ),
                      ),
                    ],
                  );
                }),
          ),
        ],
      ),
    );
  }
}

events.dart

1
2
3
4
5
6
7
class Events {
  final String time;
  final String eventName;
  final String description;

  Events({required this.time, required this.eventName, required this.description});
}

constants.dart

1
2
3
4
5
6
7
import 'package:flutter/material.dart';

class Constants {
  static const kPurpleColor = Color(0xFFB97DFE);
  static const kRedColor = Color(0xFFFE4067);
  static const kGreenColor = Color(0xFFADE9E3);
}
 

Become a Patron!