loading...

Building Twitter layout with Flutter

leoat12 profile image Leonardo Teteo ・8 min read

Flutter is the new shiny framework for mobile development by Google, it was first released last year and this month it reached the 1.0 version. I've known Flutter for some months now, but since it reached 1.0 I decided to take a deeper look at it. I've developed mobile applications before using native Android with Java and cross-platform with Xamarim and NativeScript, it was not anything serious, but it is an area I find interesting and some people can argue, it is the near future.

Presentations done, now let's go to the juicy details. In my opinion, the most remarkable feature of Flutter is how easy it is to understand its layouts mechanics. Basically, everything is a widget and you can put a widget anywhere you want. The widgets are very descriptive, have attributes that cover almost all common usages, it is effortless and simple to build a layout. Since everything is a widget, the framework is very permissive, dynamic and logical, you have no fear of doing something wrong, just do it, and with hot reload as fast a blink you can test everything easily.

My experience with Flutter is amateur level, I did the most basic tutorial on Flutter website, read some things about how Flutter handles layouts, principally this great article that summarizes everything you need to know about Flutter layout structure. I would never dare to try replicate Twitter layout in none of the aforementioned frameworks, but with Flutter I had enough confidence to do it because I had no fear of failure.

So we have something like this:

Twitter layout

To make it simpler, I made the layout taking in consideration only simple tweets, meaning only with text, no images, quotes, etc. Just to start. However, the rest will be present, the app bar at the top, the floating button to tweet and the bottom bar with tabs. Does it look too complex? Divide and conquer is the best approach for that!

First, let's see the app bar at the top. We can see that we have a circle avatar and a text written "Home". An app bar is a very common design pattern in Android apps and Flutter knows that,because of that Flutter has widgets to make it easy to build one. To do that we use the Scaffold widget that has a parameter called appBar that accepts an AppBar widget as follows:

return MaterialApp(
      title: "Twitter Layout",
      theme: new ThemeData(
        primaryColor: Color.fromRGBO(21, 32, 43, 1.0),
        accentColor: Colors.blueAccent[200],
      ),
      home: new Scaffold(
          appBar: new AppBar(
            title: Row(
              children: <Widget>[
                Container(
                  child: CircleAvatar(
                    child: new Text("L"),
                    radius: 15.0,
                  ),
                  margin: EdgeInsets.only(right: 30.0),
                ),
                new Text("Home")
              ],
            ),
            elevation: 4.0,
          ),
          body: _buildBody(context)
      ),
    );

As you can see, the Scaffold widget is wrapped by a MaterialApp widget that makes is it easy to build apps using Material Design, design directives for Android apps by Google, which the Twitter app clearly uses. The AppBar widgets has a title parameter that accepts any widget, although generally it is just a Text widget. Therefore, I created a Row widget that is responsible to place its children aligned horizontally, the way we want in this case. We add the children of the Row widget in a children parameter. In this case the widget has a Container and Text as children. The Container widget is a very convenient widget because it enables you to custom spacing, positioning and painting configurations to its child. In this case, I added a CircleAvatar widget as its child and added a margin of 30 pixels to the right of the circle to give separation from the "Home" text. The child of a CircleAvatar widget can be any widget, but we generally use text or an image, to make it simple I used a Text with the letter "L". The radius indicates the size of the circle. If you notice, the circle on the app bar is a little smaller than the ones on the tweets, so I had to configure that. The parameter on the AppBar widget indicates the elevation of the app bar, it is what gives that shadow below it. In the end it was not necessary to make an app bar, a circular avatar and all the spacing configuration from the ground up, Flutter makes it easy to achieve the result you want with the use of common patterns. The Scaffold widget also has a body parameter, where, in this case, all the app content resides. Let's get into it.

To organize the layout I made the body building logic on a _buildBody() method which is as follows:

Widget _buildBody(context){
    return new Container(
      child: new Column(
        children: <Widget>[
          new Flexible(
              child: Scaffold(
                body: new ListView.builder(
                  itemBuilder: (_, int index) => _tweets[index],
                  itemCount: _tweets.length,
                  reverse: false,
                ),
                floatingActionButton: FloatingActionButton(
                    onPressed: null, child: Icon(Icons.edit)),
              )),
          new Container(
            decoration:
            new BoxDecoration(color: Theme.of(context).cardColor),
            child: _buildTabsBar(context),
          ),
        ],
      ),
    );
  }

The _buildBody() return a Widget, which is, in this case, a Container. The container has a Column widget as child, which all the magic happens. In the image used as base above we can see that we have the list of tweets and right below it the tabs bar, besides the floating button. The children of the Column widget are placed the way they appear on the screen. First, we have a Flexible widget and, as the names suggests, it is flexible, meaning in this case that it will occupy the remaining of the space of the parent, this is where our list and floating button are. The Flexible widget child is a Scaffold widget. Nobody said that it can be used only was the main widget of a an app like we used before although it is very common that way. It is used here to facilitate the creation of the floating button since it has a floatingActionButton attribute, which takes a FloatingActionButton widget with an edit icon as child. It also has an onPressed attribute to configure the event, but since the goal here is just to make the layout, it is null for now. Only this will make the floating button appear exactly where we want it. As before, on the body parameter of he Scaffold widget, we have the main content, the list of tweets, that is implemented using ListView, which is kind of a specialized column that enables scrolling when the content does not fit entirely in the widget. It simply builds the list from a list tweets, which we will see later.

For now, let's focus on the other child of the Column, the container which has the method _buildTabsBar() as child. Let's see how the tabs are structured.

Widget _buildTabsBar(context) {
    return Container(
      height: 60,
      color: Color.fromRGBO(21, 32, 43, 1.0),
      child: new Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          Icon(Icons.home, color: Theme.of(context).accentColor),
          Icon(Icons.search, color: Colors.grey[100]),
          Icon(Icons.notifications_none, color: Colors.grey[100]),
          Icon(Icons.mail_outline, color: Colors.grey[100])
        ],
      ),
    );
  }

Making the bar is very simple and that's what makes it fantastic. It is just a Container with a Row as child that has the icons as children, that's it. I configure the height of the container and its color. Then, in the Row I used the mainAxisAlignment attribute to make the space be evenly distributed between the icons making look like the original layout. I still remember how it is a pain to center things using HTML and CSS, not to mention make the space even.

So, let's see the last part, the tweet. It looks like the part where there are more elements to take care of and it is right, but it is not that much more complex than the rest of the layout if you visualize it as a collection of rows and columns. To make the tweet reusable and easy to use in a ListView I created a Tweet class containing all the layout for the tweet:

class Tweet extends StatelessWidget {

  Tweet({this.user, this.userHandle, this.text});

  final String user;
  final String userHandle;
  final String text;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(10.0),
      decoration: BoxDecoration(
          color: Color.fromRGBO(21, 32, 43, 1.0),
          border: Border(bottom: BorderSide())),
      child: new Row(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          CircleAvatar(
            child: Text(user.substring(0, 1)),
          ),
          _tweetContent()
        ],
      ),
    );
  }

...

}

To make it simple, it is stateless and only gets the user, user handle and the tweet text to build the tweet. A full-fledged tweet would probably be statetul because of the counts for likes, retweets and answers and have much more parameters. The build() method returns a Container with the basic layout configuration, padding and a border at the bottom acting as a divider for the tweets, then it has a Row as child. The row contains the circle avatar of the user who tweeted and the tweet content, which is built through a method for organization sake. mainAxisAlignment and crossAxisAlignment set as start is very important because it makes the circle avatar to be placed at top-left corner as needed.

The _tweetContent() method returns the content of the tweet, of course. Below is the entire method since it is easier to connect the dots by seeing all elements, after that I will explain the code.

Widget _tweetContent(){
    return Flexible(
      child: Container(
        margin: EdgeInsets.only(left: 10.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            Row(
              children: <Widget>[
                Text(user,
                    style: TextStyle(
                        color: Colors.white,
                        fontWeight: FontWeight.bold)),
                Container(
                  margin: EdgeInsets.only(left: 5.0),
                  child: Text(userHandle + " · 30m",
                      style: TextStyle(color: Colors.grey[400])),
                )
              ],
            ),
            Container(
                margin: EdgeInsets.only(top: 5.0),
                child: Text(text, style: TextStyle(color: Colors.white))),
            Container(
              margin: EdgeInsets.only(top: 10.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: <Widget>[
                  Row(
                    children: <Widget>[
                      Icon(Icons.message, color: Colors.white),
                      Container(
                        margin: EdgeInsets.only(left: 3.0),
                        child: Text("15",
                            style: TextStyle(color: Colors.white)),
                      )
                    ],
                  ),
                  Row(
                    children: <Widget>[
                      Icon(Icons.repeat, color: Colors.white),
                      Container(
                        margin: EdgeInsets.only(left: 3.0),
                        child: Text("15",
                            style: TextStyle(color: Colors.white)),
                      )
                    ],
                  ),
                  Row(
                    children: <Widget>[
                      Icon(Icons.favorite_border, color: Colors.white),
                      Container(
                        margin: EdgeInsets.only(left: 3.0),
                        child: Text("15",
                            style: TextStyle(color: Colors.white)),
                      )
                    ],
                  ),
                  Icon(Icons.share, color: Colors.white)
                ],
              ),
            )
          ],
        ),
      ),
    );
  }

The parent element is a Flexible widget which allows the element to occupy the rest of the space available, the space on the row created on the build method above. Then we have a Container which allow to configure the margin necessary on the left of the content to separate it from the circle avatar. Finally, we have a Column, where the fun starts.

Again we have crossAxisAlignment and mainAxisAlignment as start to make the content to be positioned a the top-left corner. The first child of the column is a Row that contains the user name, its handle and the time of the tweet. To make the user handle be slightly separated from the user name we wrap the Text into a Container as we've done before. The second child of the column is a Container that wraps the actual text of the tweet with a margin at the top. Finally, we have another container with a Row as child to build the reply, retweet, like and share buttons. mainAxisAlignment is set as spaceBetween so to that all icons will have an equal space between them and will be evenly distributed through the width of the row. Very convenient configuration, positioning and spacing was always something very troublesome for me in other frameworks. Each icon is a Row itself containing the icon and the number of replies, RTs, etc beside it. Simple, isn't it?

So let's see the final result below:

Twitter layout made with Flutter

It is close enough except for the icons which are probably Twitter exclusive. The tweets are just a sample.

I hope this post will be of any use for people who are thinking about taking a look at Flutter and how to get started. For me it was love at the first sight when it comes to layout creation and unless it is limited about interactivity, where I still have to experiment, I believe that it will be popular very soon!

Posted on by:

leoat12 profile

Leonardo Teteo

@leoat12

Java Web Developer with a passion for Spring and cloud computing. Know a thing or two about AWS. Trying to learn NodeJS lately with the help of TypeScript.

Discussion

pic
Editor guide
 

Hey, this was an amazing write up and it's nice to see Flutter getting used more in Brazil, but you should take note to the fact that splitting widgets to methods is a performance antipattern. I will also update mine fixing this problem (and also adding BLoCs!) but yeah, we shouldn't really use manual widgets outside of the build method.

 

Great to know, but I was just following the documentation on this:

To minimize the visual confusion of deeply nested layout code, place some of the implementation in variables and functions.
Source

It is probably because it is a starter level tutorial and they didn't want to complicate things, maybe it is written somewhere in the more advanced documentation, but I will keep that in mind.

 

Sadly the documentation is very basic (for now, I hope) and doesn't cover most of the best practices, so most things are still being discovered by the community.

Well, we can say that it is the very best way to learn something, when it is still new and there are many details to discover. :)

 

Thanks for this tutorial. Building a clone app layout helps a lot in learning about the programming ecosystem. Learned a lot about flutter UI layouts and design while going through this article. Stepwise guidance with code and screenshots are easy to follow. These type of social app templates help a lot to develop own. There are a lot of flutter templates that support every functionality available in social apps. These templates help to learn as well as develop their own app with powerful features.

 

Hi,

I hope to get your consent to translate and shared with Chinese developers, I will indicate the source and author.

 

Sure, you can translate it. Share the link once you are finished although I will not understand anything. hahaha

 

hahaha, ok,Thanks !