Flutter Challenge Twitter

Flutter Challenge Twitter

三月 01, 2019

Flutter Challenges will attempt to recreate a particular app UI or design in
Flutter.

This challenge will attempt the home screen of the Twitter Android app.Note
that the focus will be on the UI rather than actually fetching data from a
backend server.

Understanding the app structure



The Twitter app has 4 main pages controlled by a Bottom Navigation Bar.

They are:

  1. Home(Displays tweets in your feed)
  2. Search(Search people,organisations,etc)
  3. Notifications(Notifications and mentions)
  4. Messages(Private messages)

The BottomNavigationBar has for tabs to go to each of these pages.

In our app,we’ll have four different pages and we’ll just change the pages
when an item on the BottomNavigationBar is tapped.

Setting up the project

After creating a Flutter project(I’ve named it twitter_ui),clear out the default
code in the project until you’re left with this:

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
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Challenge Twitter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(),
);
}
}

The HomePage will have a Scaffold that will hold our main
BottomNavigationBar and whatever page is currently active.

Getting Started

Because the Bottom Navigation Bar is main widget used to navigate,let’s
try that first.

Here’s how the BottomNavigationBar looks like:



Because we don’t have the exact icons used in the app,we’ll go with the Font Flutter
Awesome Package.
Add the dependency in the pubspec.yaml and add

1
import 'package:font_awesome_flutter/font_awesome_flutter.dart';

to the main.dart.

The BottomNavigationBar code comes down to:

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
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
title: Text(''),
icon: Icon(FontAwesomeIcons.home,
color: selectedPageIndex == 0 ? Colors.blue : Colors.blueGrey),
),
BottomNavigationBarItem(
title: Text(''),
icon: Icon(FontAwesomeIcons.search,
color: selectedPageIndex == 1 ? Colors.blue : Colors.blueGrey),
),
BottomNavigationBarItem(
title: Text(''),
icon: Icon(FontAwesomeIcons.bell,
color: selectedPageIndex == 2 ? Colors.blue : Colors.blueGrey),
),
BottomNavigationBarItem(
title: Text(''),
icon: Icon(FontAwesomeIcons.envelope,
color: selectedPageIndex == 3 ? Colors.blue : Colors.blueGrey),
),
],
onTap: (index) {
setState(() {
selectedPageIndex = index;
});
},
currentIndex: selectedPageIndex,
),

Add this to the HomePage.

Notice when we set the color of the icons,we check if the icon is selected and
then assign colors.In the twitter app,selected icon is blue and let’s set
unselected ones to blueGrey.

We define a integer called selectedPageIndex which stores the index of the
page selected.In the onTop function,we set the variable to the new index.It is
wrapped in a setState() call as we need a refresh of the page to re-render the AppBar.

Here is our Bottom Navigation Bar:

Setting Up The Pages

Let’s set up the four basic pages which will be displayed when the respective icons are clicked.

We set up four pages (in different files) like this:

For the user feed(home) page:

1
2
3
4
5
6
7
8
9
10
11
12
import 'package:flutter/material.dart';

class UserFeedPage extends StatefulWidget {
_UserFeedPageState createState() => _UserFeedPageState();
}

class _UserFeedPageState extends State<UserFeedPage> {
@override
Widget build(BuildContext context) {
return Container();
}
}

Similarly we set up the Search,Notification and Messages Page.

Again in the base page,we import all these pages and set up a list of these
pages.

1
2
3
4
5
6
var pages = [
UserFeedPage(),
SearchPage(),
NotificationPage(),
MessagesPage(),
];

And in the Scaffold,we set

1
body:pages[selectPageIndex],

which sets the body to display the page.

So till now,this is the MyHomePage base widget:

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
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twitter_ui/messages_page.dart';
import 'package:twitter_ui/notification_page.dart';
import 'package:twitter_ui/search_page.dart';
import 'package:twitter_ui/user_feed_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Challenge Twitter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
var selectedPageIndex = 0;
var pages = [
UserFeedPage(),
SearchPage(),
NotificationPage(),
MessagesPage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(''),
),
body: pages[selectedPageIndex],
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
title: Text(''),
icon: Icon(
FontAwesomeIcons.home,
color: selectedPageIndex == 0 ? Colors.blue : Colors.blueGrey,
)),
BottomNavigationBarItem(
title: Text(''),
icon: Icon(
FontAwesomeIcons.search,
color: selectedPageIndex == 1 ? Colors.blue : Colors.blueGrey,
)),
BottomNavigationBarItem(
title: Text(''),
icon: Icon(
FontAwesomeIcons.bell,
color: selectedPageIndex == 2 ? Colors.blue : Colors.blueGrey,
)),
BottomNavigationBarItem(
title: Text(''),
icon: Icon(
FontAwesomeIcons.envelope,
color: selectedPageIndex == 3 ? Colors.blue : Colors.blueGrey,
)),
],
currentIndex: selectedPageIndex,
onTap: (index) {
setState(() {
selectedPageIndex = index;
});
},
),
);
}
}

Now,we’ll recreate the pages themselves.

Creating the User Feed Page



There are two elements in the page:The AppBar and the list of tweets.

First let’s make the AppBar,It has a user profile picture and a black title with a white background.

1
2
3
4
5
6
7
8
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(
'Home',
style: TextStyle(color: Colors.black),
),
leading: Icon(Icons.account_circle, color: Colors.grey, size: 35.0),
),

We’ll use an icon instead of a profile picture.



Now,we need to create the list of tweets.For this we use a ListView.builder().

Let’s take a look at the list item.



First,we’ll have a column with a row and a divider you see at the bottom.

Inside the row,we’ll have the user icon and another column.

The column will hold a row for tweet information,a text with the tweet itself,
an image and another row for actions to take on tweet(like,comment,etc.).

For brevity,we’ll exclude the image for now,but adding it is as simple as
adding an image in a row.

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
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';

class UserFeedPage extends StatefulWidget {
_UserFeedPageState createState() => _UserFeedPageState();
}

class _UserFeedPageState extends State<UserFeedPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(
'Home',
style: TextStyle(color: Colors.black),
),
leading: Icon(Icons.account_circle, color: Colors.grey, size: 35.0),
),
body: ListView.builder(
itemCount: 4,
itemBuilder: (context, position) {
TweetItemModel tweet = TweetHelper.getTweet(position);
return Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.account_circle,
size: 60.0,
color: Colors.grey,
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Container(
child: RichText(
text: TextSpan(children: [
TextSpan(
text: tweet.username,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18.0,
color: Colors.black)),
TextSpan(
text: ' ' + tweet.twitterHandle,
style: TextStyle(
fontSize: 16.0,
color: Colors.grey)),
TextSpan(
text: '${tweet.time}',
style: TextStyle(
fontSize: 16.0,
color: Colors.grey)),
]),
overflow: TextOverflow.ellipsis,
),
),
flex: 5,
),
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(right: 4.0),
child: Icon(Icons.expand_more,
color: Colors.grey),
),
)
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(tweet.tweet,
style: TextStyle(fontSize: 18.0)),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Icon(FontAwesomeIcons.comment,
color: Colors.grey),
Icon(FontAwesomeIcons.retweet,
color: Colors.grey),
Icon(FontAwesomeIcons.heart,
color: Colors.grey),
Icon(FontAwesomeIcons.shareAlt,
color: Colors.grey),
],
),
)
],
),
)
],
),
),
Divider(),
],
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(FontAwesomeIcons.featherAlt),
),
);
}
}

class TweetItemModel {
String tweet;
String username;
String time;
String twitterHandle;

TweetItemModel(this.tweet, this.username, this.time, this.twitterHandle);
}

class TweetHelper {
static var tweets = [
TweetItemModel(
"Inviting computer science students to register for the latest event in computer technology.",
"Google Devs",
"3m",
"@GoogleDevsIndia"),
TweetItemModel(
"Developing a large, complex app that needs a microservice architecture? @crichardson. Read on to learn how to decompose an application into services ",
"Java",
"5m",
"@java"),
TweetItemModel(
"The Samsung Galaxy S9 is in the record books now, but it's not likely that Samsung will be celebrating this particular milestone. https://www.androidauthority.com/samsung-galaxy-s9-history-887809/ … #samsung #samsunggalaxys9 by: C. Scott Brown",
"Android Authority",
"30m",
"@AndroidAuth"),
TweetItemModel(
"Inviting computer science students to register for the latest event in computer technology.",
"Google Devs India",
"3m",
"@GoogleDevsIndia"),
];

static TweetItemModel getTweet(int position) {
return tweets[position];
}
}



This is the recreated page for the Twitter user feed.The fact that recreating
andy UI is fast and easy in Flutter is a testament to its development speed and customisability at the same time.These are two things that rarely go together.

The main.dart code is follow:

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
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twitter_ui/messages_page.dart';
import 'package:twitter_ui/notification_page.dart';
import 'package:twitter_ui/search_page.dart';
import 'package:twitter_ui/user_feed_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Challenge Twitter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
var selectedPageIndex = 0;
var pages = [
UserFeedPage(),
SearchPage(),
NotificationPage(),
MessagesPage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: pages[selectedPageIndex],
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
title: Text(''),
icon: Icon(
FontAwesomeIcons.home,
color: selectedPageIndex == 0 ? Colors.blue : Colors.blueGrey,
)),
BottomNavigationBarItem(
title: Text(''),
icon: Icon(
FontAwesomeIcons.search,
color: selectedPageIndex == 1 ? Colors.blue : Colors.blueGrey,
)),
BottomNavigationBarItem(
title: Text(''),
icon: Icon(
FontAwesomeIcons.bell,
color: selectedPageIndex == 2 ? Colors.blue : Colors.blueGrey,
)),
BottomNavigationBarItem(
title: Text(''),
icon: Icon(
FontAwesomeIcons.envelope,
color: selectedPageIndex == 3 ? Colors.blue : Colors.blueGrey,
)),
],
currentIndex: selectedPageIndex,
onTap: (index) {
setState(() {
selectedPageIndex = index;
});
},
),
);
}
}