WhatsApp Clone with Flutter in a week

Sharing what I’ve learned, and the DX

Richard Ng
8 min readMar 11, 2019

This development journey is shared through 2 articles. This is part one. Source code is on Github, branch part-1. You may find article Part 2 here.

Flutter has been out for quite a while (starting from May 2017, stable release on Dec 2018), and the team has been very active (both technical-wise and promo-wise). As a frontend developer who is always interested in trying out cross-platform stuff, I give Flutter a go.

Hacking through some tutorials here and there, I decided to play around with it through a small project. I want to know what would it be like if I were to use Flutter to build an app with production-level design requirement, not a mere example app. Also, to do it in step-by-step manner, I decided to focus on the putting up the layout first, before diving into the app’s state management such as using BLOC pattern, inheritedModel, etc. Lastly, I want this project to be worth sharing, so eventually I picked an app to clone for which I believe there would be a large audience with shared experience — WhatsApp.

Enough prologue, let’s get started.

Firstly, getting the colours right

Properties such as primaryColor, primaryIconTheme, textTheme are fine, they are easy to understand. How about indicatorColor and a bunch of other ? How should I know what to add / ignore ? Maybe I’m not well-versed with Material Design, I add the rest by trial and error. Say like for indicatorColor, I added it when I have to adjust the “selected tab bottom border color”.

Second, grabbing the low-hanging fruit — TabView

With Flutter, it’s just a breeze. The Scaffold( ) widget from Flutter has properties like appBar, body, etc to help us quickly putting up a basic layout.

Other than ChatList( ), all classes above are provided by Flutter
The list of contacts we normally see in the “landing page”

Flutter has a wide range of frequently seen / used app “widgets” (ListView, TabView, TopAppBar, to name a few) that you can just use out of the box. And here is our progress:

With only a few lines of code, we are already up to this

(Okay… WhatsApp in IOS indeed is in another style. If interested, Flutter’s Cupertino widgets could be used to make a separate set of layouts. As I’m an Android user so I just made the Android layout only at the moment… )

Next, creating ChatRoom( )

Making a chat app, so naturally, the next thing should be the chat room.

Laying out the top AppBar, messages in the body, bottom with text input + send button… all are fine, just using a combination of Row and Column, and then within each Row / Column, wrapping some children with Expanded( flex: x, ) to control how much space each child can occupy, should give you a fairly okay layout. Just think in terms of flex-box layout in CSS.

Laying out our chat room with Flutter’s Column, Expanded, etc.

Official tutorial even illustrates a way of animating “message push” when user finished typing and hit the “send” button. Up to this point we already scaffolded the chat room layout.

Connecting ChatRoom( ) to ChatList( )

When user tap on one of the ListTiles inside the CHATS tab ListView, user is directed to the chat room view by pushing the ChatRoom view on top of the “navigation stack”. With Flutter, simply calling the Navigator.push method inside ListTile’s onTap property will do the job. A working Back button (←) will automatically be added as the leading icon at the top AppBar.

Navigation in Flutter is just neat

Moving away from Material Design…

The challenge really comes in when you want to do custom stuff (i.e. not strictly follow Material Design and therefore we cannot find pre-made stuff out of the box), they are:

  • Top AppBar
  • Message widget
  • Rounded text input field

We will do the adjustment one by one, starting from the AppBar.

Customisation of Widget — AppBar

Material Design AppBar only has one leading icon. In WhatsApp we have both the Back button (←) and leading CircleAvatar co-existing.

The problem here is that the auto-inserted Back button is competing the space with the CircleAvatar which we will want to place it in the AppBar’s “leading” property. Surely we can set the AppBar’s “automaticallyImplyLeading” property to false to reclaim the space for our avatar, but then the Back button, and the user’s ability to tap it to return to the chat list page, are removed. To solve this, we will do a little bit trick on the “title” property.

The idea is that we will leave the AppBar’s “leading” property empty, allowing Flutter to insert the Back button whenever necessary. To place our CircleAvatar, as the title property accepts a widget (which is usually Text), we can give it a parent widget with 2 children, one being the title text content and another being the avatar. Let’s try using a Row to wrap a CircleAvatar and a Text.

Are we done yet ? Kinda… but if we are too follow the design requirement better, we will see that we’ll have the avatar and the title to be closer to the Back button.

The Real WhatsApp (left) vs Our Clone (right)
Debug border paint of AppBar

Thinking in CSS, we could give negative margin to the parent row in order to overflow its children into the Title widget’s left padding (i.e. visually eliminating area around red circle). Unfortunately negative margin is not an option in Flutter. To overflow some children outside the parent’s border while remaining visible, we can use Stack to replace Row as the parent widget, and wrap the overflowing children with Positioned.

A few points to note for the above snippet:

  • SizedBox with infinity width is to mimic a Row with maximised width
  • “leftOffset” is a negative value, causing the children to “overflow into” the parent’s left padding
  • A Stack widget puts its children by default at its top-left-handed corner, one on top of another.

With the last point in mind, we can lay out our children by working out the maths for its top/right/bottom/left values. In CSS terms, it is a non-static positioned parent with absolute-positioned children. And here’s the result of our adjustment:

a Stack widget allows Positioned-children to overflow

And we are now much closer to the design requirement ✌️

Real WhatsApp AppBar (left) vs Our Clone’s AppBar (right)

Customisation of Widget — Message Bar

Before
After ( btw the mic/send button can be adjusted through checking the state with textController, but we’re not going to talk about it here )

We’ve got rounded (nearly half-circle) ends, with one leading icon and two trailing icons adjacent to the text field.

We want to put 3 icons near text input field — one at the start and two at end. Also, we want the ends to be rounded. Rounding corners of a widget can simply done by using Flutter’s ClipRRect, supplying it with our desired radius. To make our rounded ends, we can use a larger radius so that the arches of the corners will merge.

Putting this roundedContainer and a send button inside a Row, we’ve completed our inputBar.

Debug border print of input bar

And now our layout is pretty close with the real one !

Customisation of Widget — Message Thread

Usually I’ve got no reply after the blue tick… anyway

I intentionally leave this part at the end as this is the hardest one. Instead of a rectangle, or a rectangle with a small triangle attached to its top-right-handed corner as its “beak”, the message widget in WhatsApp is “a rounded rectangle, with a rounded beak.”

Skimming through Flutter’s documentation, I cannot find any out of the box for this, so I decided to make my own. We will need the ability to custom-cut a rectangle, so we will want to use ClipPath. It allows us to cut a rectangle by specifying the cutting border with coordinates or even equations. Just think of placing the rectangle on a coordinated grid paper, with the top-left-handed corner being (0, 0).

I started with the top-left-peaked version (i.e. mirror reflect of the above one) of the message thread.

Cutting out a message thread from a rectangle

Using the blueprint above, we can craft out our custom message thread. The code to execute this is here. What it does is to guide a point to move from the top-left-handed corner, through an anti-clockwise round-trip, all the way back to top-left-handed corner. The guiding points can be calculated using high school geometry 😐 …yea taking some time to figure out all of them, but it’s worth doing. Here’s the resulting message thread:

The math works 😎

The rest of the job:

  • set max width for each thread ( 80vw i guess ? )
  • text soft wrap
  • “flex start/end” for incoming /outgoing messages
  • different colours for incoming /outgoing messages

Shouldn’t be too hard to accomplish, we can use BoxContraint for max width, set softwrap true for Text, adjust mainAxisAlignment in Row, set color property of Container based on condition.

What we’ve done so far…

  • Scaffolded a MaterialApp with custom Theme colours
  • Built the TabView, and the ListView in the “CHAT” tab
  • Built the ChatRoom( ), and connect it with ChatList( )
  • Customised AppBar( ) using Stack
  • Customised MessageBar( ) using ClipRRect
  • Customised MessageThread( ) using ClipPath

How I feel…

The good things…

  • Out-of-the-box widgets are just so handy and cool
  • Animations are smooth, esp. if you’re developing in a real device
  • Short build time, hot reload, debug paint, and error message with high clarity, improve DX quite a lot
  • Unlike react native and npm, diagnosing and upgrading Flutter are nearly painless (basically u just do flutter doctor -v and flutter upgrade which will work)

The bad things…

  • Too many widgets for you to learn and really understand how they work because you can mix and match them like a champ
  • “Widget wrapping hell” is common, you wrap a widget with another widget which is wrapped by another… although you can mitigate this by refactoring your code, but still…

What’s next

We will add camera to our app, animate the top app bar, and also add floating action button (that circle button on the bottom-right-handed corner in case you don’t know) which changes with tab change. Hope you enjoy reading the article so far, see you in Part 2.

--

--