Building iOS Phone application in Flutter

Konstantin Sofeikov
Flutter Community
Published in
12 min readJul 15, 2019
The final view of our application. For left to right: Keypad screen, Contacts screen with Search functionality, and the Recents screen.

I’ve just started learning Flutter SDK and what awesome stuff this SDK is! After completing a couple of courses on it, I decided to practice a little bit. I think that recreating some well-known apps would make it a good practice session. Since one of the fundamental phone functionalities is to make calls, I decided to take my iPhone and recreated its Phone application. On the screenshot above I’ve tried to recreate look and fill of the application. To keep this article relatively short, I will only go through the most interesting moments of the exercise.

I will assume that you have you setup ready, that is having your IDE and a device/simulator ready.

All code can be found here, at my GitHub: https://github.com/sofeikov/iOSPhoneFlutter

Since it has just one external dependency, deploying this code is an easy task. Before we start, I have to say: I am open for any kind of comments, feedback and suggestions! Be free to point out places where I could make something better or in a different, more reliable way!

OK, so first things first. We need some random data to display. For that, I went to one of many websites that generate random English names. I’ve requested 300 items and added them to data_bank.dart. Here is a small slice of that data:

A small slice of random data prepared for further experiments

These names will be source for Contacts list names. We also use this list to generate a random list of recent calls, both incoming and outgoing. For that we define the following class:

We keep some basic information there. A caller name, call source(skype, phone, WhatsUp, etc), call time and whether the call was missed or not. For defining call sources, we use the following enum:

Call source Enum.

So we can now go and generate a random list of calls. For that, we just make a loop and fill out those values with random values. Here we go:

At this point, we already have some data to work with. Let us now create the Scaffold on the application. There will be components of the UI that will be presented regardless of the screen we are in. These components are AppBar with a caption or Action Buttons(Edit, Clear, etc) and the bottom control bar. If you are unfamiliar with the bottom control bar, please head over to https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html to find out more. Meanwhile, let us consider how we will use it. BottomNavigationBar is a stateful widget. In has to maintain the state and we will be responsible for notifying this widget what it has to be in.

Ok so at this point we have a list of random calls. Some of them are missed, some of them are incoming, some are outgoing. Here a gif to see the functionality for the Recents screen will look like:

A demonstration of how the Recents calls interface will look like

Note, that in iOS interface Recents caption will also go away as you scroll down. Therefore we can see that the overall layout of the whole screen is quite a simple one. We will have a main column that will hold the AppBar with a caption and then the second element in the column will be ListView. For that will use this variable int _bottomBarIndex = 1; This variable holds the index of the currently selected tab. Whenever a new index is selected, we will update the state of this widget through setState function. So here is the code, that does nothing except for building the scaffold for the future interface:

class RecentCalls extends StatefulWidget {
@override
_RecentCallsState createState() => _RecentCallsState();
}

class _RecentCallsState extends State<RecentCalls> {
int _bottomBarIndex = 1;
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
selectedItemColor: Colors.blueAccent,
unselectedItemColor: Colors.grey,
backgroundColor: Colors.grey.shade200,
elevation: 0,
showUnselectedLabels: true,
onTap: (newInex) {
setState(() {
_bottomBarIndex = newInex;
});
},
currentIndex: _bottomBarIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.favorite),
title: Text("Favourites"),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_time),
title: Text("Recents"),
),
BottomNavigationBarItem(
icon: Icon(Icons.contacts),
title: Text("Contacts"),
),
BottomNavigationBarItem(
icon: Icon(Icons.keyboard),
title: Text("Keypad"),
),
BottomNavigationBarItem(
icon: Icon(Icons.voicemail),
title: Text("Voicemail"),
),
],
),
// Nothing too complicated, get bottom bar and the main page of the app
appBar: getAppbarFromBottomBarIndex(_bottomBarIndex),
body: getPageFromBottombar(_bottomBarIndex),
);
}
}

The variable _bottomBarIndex is responsible for notifying all other members of the interface that they need to be updated. Note that whenever this variable gets updated, two function will get called: getAppbarFromBottomBarIndex and getPageFromBottombar. So what are these functions? Well, as we can see, they are responsible for forming appBar and the body of the Scaffold.

In fact, the logic of these two functions is rather simple. They take a current bottomNavigationTab index as an input parameter and depending on it they return different widgets. Note, that out application does not use Navigation at all. Everything that is happening, is happening within the single route of the application. I will show the getAppbarFromBottomBarIndex function, the source code of the second one you can look up here:

Widget getAppbarFromBottomBarIndex(int index) {
/// Depending on the index of the bottom bar, we want to show different
/// states of the Appbar. Sometimes it is just header, but often it contains
/// some control elements. So functions contains the configuration logic for
/// the this.
/// 0 - Favourites
/// 1 - Recents
/// 2 - Contacts
/// 3 - Keypad
/// 4 - Voicemail

if (index == 0) {
} else if (index == 1) {
/// We need to show differnt controls depending on the state of the list.
/// If Edit was pressed, then we need to show two buttons: Clear and Done.
/// Otherwise we need to show just one button: edit.
return AppBar(
backgroundColor: kColorGreyShade200,
elevation: 0,
title: Container(
color: kColorGreyShade200,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Visibility(
// This allows to keep occupying space so that other controls in
// this row do not move left or right when widget becomes
// invisible
maintainSize: true,
maintainAnimation: true,
maintainState: true,
// This control is invisible if we are not editing the list
visible: _listState == ListState.EDITING,
child: InkWell(
child: Text("Clear", style: kAppleActionButtonTextStyle),
onTap: () {},
),
),
CupertinoSegmentedControl(
groupValue: _allOrMissedControlGroupValue,
onValueChanged: (key) {
// Again, we set our state depending on what mode we are in
if (key == kAllCalls) {
setState(() {
_showMissingOnly = false;
_allOrMissedControlGroupValue = kAllCalls;
});
} else {
setState(() {
_showMissingOnly = true;
_allOrMissedControlGroupValue = kMissedCalls;
});
}
},
children: {
kAllCalls: Padding(
child: Text(kAllCalls),
padding: EdgeInsets.symmetric(horizontal: 10),
),
kMissedCalls: Padding(
child: Text(kMissedCalls),
padding: EdgeInsets.symmetric(horizontal: 10),
),
},
),
SizedBox(
// Sizebox to occupy enough space. Done takes more pixels than
// Edit, therefore we need to reserve this space so that stuff
// of the left does not float around.
width: 40,
child: InkWell(
child: Text(editButtonText[_listState],
style: kAppleActionButtonTextStyle),
onTap: () {
setState(() {
if (_listState == ListState.VIEWING) {
_listState = ListState.EDITING;
} else {
_listState = ListState.VIEWING;
}
getListOfCalls(_showMissingOnly, _listState);
});
},
),
)
],
),
),
);
} else if (index == 2) {
// Contacts
return AppBar(
elevation: 0,
backgroundColor: kColorGreyShade200,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"Groups",
style: kAppleActionButtonTextStyle,
),
Icon(
CupertinoIcons.add,
color: Colors.blueAccent,
size: 36,
)
],
),
);
} else if (index == 3) {
// We have NO appbar for keypad
} else if (index == 4) {
// Voicemail section
return AppBar(
elevation: 0,
backgroundColor: kColorGreyShade200,
);
}
}

Basically, the code is quite straightforward, the only slightly tricky part is CupertinoSegmentedControl.

Let us now think how will create calls records in the list. We know that We can Edit the list, then call records change their state in terms of they start having additional icons on the left and lose one icon on the right. See the gif above to see this effect. Therefore, one path we can follow is to make phone call record a stateful widget and then update its state whenever the Edit button gets pressed.

However, I preferred the other way which is to keep phone call widgets stateless and just redraw them each time the Edit button gets pressed.

Before seeing the code of the whole widget, let us consider some minor moments first. Notice, that in the iOS app, the time of the call is shown depending on how far back a call appeared. If the call happened today, the app will show you the time of the day. If it happened this rolling week, the app will show you the name of the day. Otherwise, it is going to show you the date. For easilty achieving this functionality I propose one to use this package: https://pub.dev/packages/intl . This package is supported by the Dart team and allows easy data-time manipulations. Here is the function that achieves the desired effect:

I think the code is quite self-explanatory.

I was also wondering for a while what is the best approach to conditionally include/exclude a certain widget from a Row widget. From the internet search, it seems like the best way to achieve this is to write a function returning a widget. Inside this function, this logic is easily implementable. Here is the code. Note a couple of things. Firstly, note how we use ListState variable to define whether we want or not including the remove icon into a Row. Also note, that because string datetime description can be of variable length, we have to properly align the right-hand side of each list tile. We push the whole thing to the right so that Info icons are aligned equally and then allow string representation of the datetime to take the rest.

Building Contacts screen

This is actually the coolest part of this exercise since it involves implementing this quick -jump-to-a-letter functionality. See the gif below to get an idea of what we are trying to achieve.

Contacts screen demonstration. Notice how we are able to use navigation letters on the right-hand side of the UI to quickly jump to a letter. Also, at the end of the animation search functionality is demonstrated.

The full code implementing this functionality can be found here, direct link:

Alright, let us start from a high-level overview. Our main component here is ListView. Depending on whether anything is typed in the search fields, it will either display a list of contacts or a search result. Ok, so we define this high-level behaviour in the following way:

List<Widget> getContactListMaster() {
/// This is small facade for the drawing widgets.
/// If we are in the search state, we drawn a normal contact list
/// Otherwise we are drawing search results widgets
if (!_inSearchState) {
return getContactList();
}
return getSearchResult();
}

And this small function is used like this when we draw our ListView.

Expanded(
flex: 30,
child: ListView(
controller: _scrollController,
children: getContactListMaster(),
),
),

We will deal with the _scrollController a bit later.

Now let us deal with getSearchResult function first as it is much easier to do. Here is the listing of this function with some commentaries:

This all is very straightforward. We just go through all names we have in our database and check if each name contains substring that equals to the search term we have. We have non-empty search results, we just output them into ListView.

The more interesting stuff is hidden in building contact list itself. The thing is that if we do not want this quick-jump-to-letter functionality, then the task is trivial. However, we do want this functionality. Before getting into the code, let us consider, let us consider our general approach to the problem. So what we want is to jump quickly to a certain position. Unfortunately, the only way to achieve this at the moment is to tell how much you want to scroll down in pixels. You can not just “jump” there, you would have to get there through an animation.

There is an extensive discussion in Flutter’s GitHub in why and how to implement this feature. I believe that implementing this feature is hard for the following reason: ListView can contain Widgets inside itself. What is a widget? Well, pretty much anything. This can be something that is built upon some web data arrival or contacts of the following list tiles may depend on what has been shown before. That means you do not know how much pixels your content is going to take and hence there is no easy way to “jump” to a certain position.

Ok, having that in mind, what do we do? Well, as I said, to jump to a certain position we would have to know how pixels we want to skip. That means we have to know precisely how tall each list tile is. How can we know it? Well, we build it, right? Therefore we can control how tall each element is. Knowing how tall each element is in pixels, we can easily scroll down to the desired location. Look at the code below and the comments it contains. They make very clear what is going on:

Ok, after this code gets executed, we have a mapping “letter->jump height in pixels”. After that, we just need to create an event for each entry of the letters columns on the right. We do that by executing the following piece of code:

Note, how we use _scrollController to scroll to a certain location in pixels.

And this is pretty much it for the contact screen.

Building Keypad screen

There is nothing too exciting going on here. The only interesting thing we have to keep an eye on is that when the number you are typing it gets too long, the iOS native app first shrinks its font size and later starts showing a triple dot in front of the number, while effectively showing somewhat 15 last symbols of that number. It will surely be implemented as a stateful widget. Let us look at the functionality:

Functionality demoing. Note how the typed number gets trimmed when it gets too big

The full code of this screen is located here:

The code itself is quite long and for a reason. The reason for this is that we have a lot of actionable elements on this screen. If we sum it all up, we have 10 digits buttons, 2 buttons for * and # signs. Furthermore, we have a delete digit button. Thirteen buttons! However, logically the layout is quite simple. This UI state is achieved by the column with a few rows inside it. Note, that when we start typing a number, Add Number and Delete button becomes visible. This is achieved with the Visibility widget with the following code(code for hiding and showing the Delete Button is very similar):

Visibility(
/// if there is any number entered we should be able to add this numbers
/// to the contact list
child: Padding(
padding: const EdgeInsets.only(
bottom: 20,
),
child: Text(
"Add Number",
style: kAppleActionButtonTextStyle,
),
),
maintainState: true,
visible: typedNumber.length > 0,
maintainAnimation: true,
maintainSize: true,
),

Two points to note here. We use maintainState, maintainAnimation, maintainSize set to true. We do it because if we do not, the widgets around this text field will get displaced whenever the Text field changes its visibility. To avoid this we tell the SDK to maintain the size of the widget so that regardless of the visibility, the positions of the widget around stay untouched.

Secondly, notice that visible variable value depends on the state of the widget. Whenever typedNumber gets changed inside the setState function, visible parameter gets automatically updated.

Alright, so the next thing to get implemented is that automatically typed number truncation. Again, will flutter this can be achieved relatively easily, with just a few lines of code:

Padding(
padding:
const EdgeInsets.only(bottom: 10, left: 0, right: 0, top: 40),
child: SizedBox(
height: 50,
child: Text(
/// If number gets really long, we truncate it to show only the
/// last 15 symbols, and everything else gets replaced by ...
"${typedNumber.length > 15 ? '...' + typedNumber.substring(typedNumber.length - 15, typedNumber.length) : typedNumber}",
style: rebuildTextStyle(),
),
),
),

We just use a class state variable to get this view automatically updated for us. Whenever the typed number is changed, this ternary operator automatically recalculates the text field value. Notice also, that we use a function to return an appropriate TextStyle object:

class _KeypadState extends State<Keypad> {
String typedNumber = "";

TextStyle rebuildTextStyle() {
/// Return different text styles depending on the number of symbols in it
if (typedNumber.length <= 10) {
return TextStyle(
fontSize: 45,
fontWeight: FontWeight.w400,
);
} else if (typedNumber.length < 13) {
return TextStyle(
fontSize: 35,
fontWeight: FontWeight.w400,
);
} else {
return TextStyle(
fontSize: 30,
fontWeight: FontWeight.w400,
);
}
}
.
.
.
.
.
./// code goes on and on below, see github for full version

Building Voicemail screen

Literally the easiest part of the work. It is a stateless widget that just needs to be drawn. Since we are not implementing any action on it, it is going to be a stateless widget. See the image below:

Building this is a really-really straightforward exercise, so I will just redirect you to the GitHub sources. Here is the direct link to the file:

Building Favourites screen

We can not be bothered atm.

--

--