Creating a Timeline Component in Flutter
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:
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);
}