Build a Celebrity Jet Tracking Dashboard in 380 lines with Flutter Markup Language — Part 1: UI and Overview

Isaac Olajos
13 min readJun 23, 2023

--

Part 1: Overview, Layout and UI

In this article we go over Part 1 of 2 of how to build a fully functional celebrity jet tracker in under 400 Lines using Flutter Markup Language, OpenSky and a few other REST API’s.

To build the dashboard, we will be using Flutter Markup Language Version 2.0.1, which allows us to create an application from a single source that will run seamlessly on any platform!

Let’s track Elon, Trump, and a few others! A full list of tail and ICAO numbers, as well as the data needed to power the dashboard will be linked in Part 2. You can check out a constantly updated version of the dashboard at jet.fml.dev!

note: all jet tracking is being done using publicly available data and tail numbers.

Features and Overview

We are going to build the following dashboard in Flutter Markup Language:

The completed jet tracking dashboard

Build Time and Expectations:

  • Run on macOS, Windows desktop, Linux, web and mobile web browsers, and as an installed application on iOS and Android from a single template/source.
  • Less than 400 lines of markup.
  • Responsive with live data.
  • No custom API required.

Dashboard Features:

  • Select from a list of users and their jets.
  • Display a list of flights from the selected jet using open API’s.
  • Select specific flights to display the data on that flight.
  • Aggregate, transform, and display flight data available from multiple open API’s to create new data and visualizations.
  • Display relevant data in a functional UI.
  • Chart relevant data for visual reference.
  • Display past and current locations on a map.
  • Display location and flight information on each marker on the map.
  • All data is up to date and live.

Flutter Markup Language Prerequisites

The quickest, minimum requirements to create a fully functional FML app without any file server knowledge is:

If you choose to do this, you can see how to connect to a locally hosted template here, under Offline Applications. The application will still be able to make network calls, but will not be accessible outside of the installed host device.

Our recommended setup to run on all platforms is:

Once you have the environment up and running, you can open a copy of the wiki for reference and begin building the dashboard!

Dashboard Layout

To start, lets create the overall layout of the dashboard without data. We will be doing this part in under 300 lines of FML. We can see its main up of a main upper header, and 3 lower sections which all contain their own subsections.

Starting with the outer sections, we will use the decoration border=”all” and radius=”10” to produce rounded boxes, with a margin=”5” to give the boxes some space:

We see the basic layout structure from the wireframe
<!--We use row as our starting layout-->
<FML layout="row">
<!--The header spans the upper section, this can also be a BOX-->
<HEADER border="all">
</HEADER>
<!--We choose to use two columns in an FML layout="row" to display the left and right halves-->
<COL>
<!--Two boxes divide the halve into quadrants-->
<BOX border="all" radius="10" margin="5">
</BOX>
<BOX border="all" radius="10" margin="5">
</BOX>
</COL>
<COL>
<!--A single box takes up the remaining space-->
<BOX border="all" radius="10" margin="5">
</BOX>
</COL>
</FML>

Now lets isolate the quadrant sizing. Starting with the overall layout, we see that the left half takes up 1/3 and the right takes up 2/3, which is the same respectively for the topleft and bottomleft. For this we simply add a flex factor to the two boxes we want to take more space with flex=””:

We use flex=”” to assign a portion of the area
<HEADER border="all">
</HEADER>
<COL>
<!--Flex is 1 by default, so we do not need to specify a flex of 1-->
<BOX ...>
</BOX>
<!--We add flex of 2 to take up 2/3 vertically-->
<BOX flex="2" ...>
</BOX>
</COL>
<!--We add flex of 2 to take up 2/3 horizontally-->
<COL flex="2">
<BOX ...>
</BOX>
</COL>

note: ...is used for readability of the article based code blocks, readers should assume attributes and widgets added in previous steps are masked using ... if not shown.

Building the Header and User Selection.

In the HEADER we see a title and an image, with a list at the end of the header. Adding the widgets as follows:

The basic layout for the header of the application
<!--The header takes a vertical alignment, and a layout type of row-->
<HEADER border="all" valign="center" layout="row">
<!--Add an image with a margin for spacing, height, and the URL to the image either fully qualified or local-->
<IMG url="resources/images/fml.png" margin="20" height="100"/>
<!--Text as the title with a style-->
<TEXT value="FMLJet" style="h2"/>
<!--A horizontal scrolling, reversed, list is created with draggability in web-->
<LIST id="userselect" reverse="true" direction="horizontal" draggable="true" flex="1">
<!--A dummy list item is created with a size as it has no contents-->
<ITEM id="userlistitem" width="100" height="100" color="blue"/>
</LIST>
</HEADER>

Now looking at the item, we want to add a radius, bordercolor, and an image child:

Creating a list item for the header
<!--List item inherits from box, and therefore has its decoration attributes-->
<!--Fixing the width and height ensures our list items are always the same size-->
<ITEM radius="50" border="all" borderwidth="3" bordercolor="purple" elevation="5" shadowcolor="purple" margin="8" shadowx="0" shadowy="0">
<!--We add an image with a height of 100% of its parent, so the width of the image is not fitted-->
<IMG url="resources/images/doge.png" height="100%"/>
</ITEM>

Now we want to add some responsiveness to ensure we can see when the item is selected vs. not selected, while also ensuring the first item is selected by default:

<!--We denote evals with "=" and return selected values based on the list items selected binding-->
<!--In this case, we want to make the item larger, and add a shadow when selected-->
<ITEM ... width="={this.selected} ? 100 : 70" height="={this.selected} ? 100 : 70" elevation="={this.selected} ? 5 : 0" selected="={this.index} == 0" bordercolor="={this.selected} ? '#c300d0' : 'grey'">
<!--{this. refers to a widgets self, otherwise an id can be given-->
<IMG .../>
</ITEM>

Building The Upper Left Flight Selection List

Next we can move on to the upper left quadrant with the same approach as the header. A TEXT widget and LIST is all that’s needed:

Creating a list and item for plane data
<COL>
<BOX ...>
<!--We add the title as a text widget-->
<TXT value="20 Day Flights" style="h5" margin="10"/>
<!--A list as before with an ID for binding. The list is vertical and top to bottom scrolling by default-->
<LIST id="planelist" flex="1">
<!--And an item with a bordercolor-->
<ITEM border="all" bordercolor="#c300d0"/>
</LIST>
</BOX>
<BOX .../>
</COL>

The ITEM becomes slightly more complex with it’s layout, using the selectable logic from above to make the ITEM responsive:

<!--We implement the same selectable logic as above-->
<ITEM pad="8" id="listitem" layout="row" halign="between" bordercolor="={this.selected} ? '#c300d0' : null" color="={this.selected} ? '#7b008322' : 'black'" border="all" selected="={this.index} == 0">
<ROW>
<!--A non expanding SBOX allows the inner widgets to determine size, while laying out differently than the parent-->
<SBOX expand="false">
<!--We add our placeholder text-->
<TXT value="DPTR" style="h6"/>
<TXT value="h:mm dd/MM"/>
</SBOX>
<!--An icon widget from the material widget library-->
<ICON icon="arrow_right"/>
<!--A repeat of the above layout with arrival values-->
<SBOX expand="false">
<TXT value="ARRV" style="h6"/>
<TXT value="h:mm dd/MM"/>
</SBOX>
</ROW>
<!--A final placeholder for the flight status and duration-->
<COL halign="end" valign="center">
<TXT value="Arrived'" color="green"/>
<TXT value="Duration: 1h 10m"/>
</COL>
</ITEM>

Building The Lower Left Current and Total Flight Trends

Now we can move on to the lower left quadrant. For this quadrant, we want to ensure each section is individually scrollable when the screen becomes to small. This section is going to take up the majority of the template.

We see the expected layout and the coded layout for the bottomleft quadrant

Starting with its sub layout, we need to create two scrollable sections divided by boxes. The upper section to display selected flight statistics:

<BOX flex="2" border="all" radius="10" margin="5">
<BOX pad="8" flex="5">
<!--The first section is scrollable independant of text-->
<TXT value="Recent Flight Statistics" style="h5" margin="0,10,10,10"/>
<SCROLL>
<!--As usual, we divide the section up using our layout widgets-->
<!--Due to the scroller being infinately high, the boxes will allow their children
to size them in the scrollers direction rather than growing-->
<BOX color="black" pad="10" margin="5" radius="5">
</BOX>
<ROW>
<BOX color="black" pad="10" margin="5" radius="5">
</BOX>
<BOX color="black" pad="10" margin="5" radius="5">
</BOX>
</ROW>
<ROW>
<BOX color="black" pad="10" margin="5" radius="5">
</BOX>
<BOX color="black" pad="10" margin="5" radius="5">
</BOX>
</ROW>
</SCROLL>
</BOX>

</BOX>

And the lower section to display total flight statistics:

<BOX border="all" radius="10" margin="5">
...
<BOX pad="8" flex="4">
<TXT value="20 Day Flight Statistics" style="h5" margin="0,10,10,10"/>
<SCROLL>
<!--A 4x4 display that can be given a hard sized in height of 120 to be seen-->
<ROW>
<BOX color="black" pad="10" margin="5">
</BOX>
<BOX color="black" pad="10" margin="5">
</BOX>
</ROW>
<ROW>
<BOX color="black" pad="10" margin="5" radius="5">
</BOX>
<BOX color="black" pad="10" margin="5" radius="5">
</BOX>
</ROW>
</SCROLL>
</BOX>
</BOX>

Within these boxes, we only have three distinct blocks that we can use to repeat each element, first is the arrival and departure airport names:

Creating the placeholders to match the wireframe for the airport name
<SCROLL>
<BOX ...>
<TXT value="Departure Airport:"/>
<TXT value="DEPARTURE AIRPORT NAME" style="h5" margin="5"/>
<!--An 11 high box with a margin will create a 1 high box (height - margin = decorationheight)-->
<BOX height="11" color="white" margin="5"/>
<TXT value="Arrival Airport:"/>
<TXT value="ARRIVAL AIRPORT NAME" style="h5" margin="5"/>
</BOX>
...

Next, the trend for the current flight; replacing the text with the correct placeholder names:

Creating the placeholders for the selected flight stats
<ROW>
<BOX ...>
<TXT value="Distance Flown:"/>
<ROW valign="center">
<TXT value="22 km" style="h5" margin="5"/>
<ROW halign="end">
<COL halign="end">
<TXT value="234 km" size="12"/>
<TXT value="333 km" size="12" color="red"/>
</COL>
<ICON icon="trending_down" color="red" size="30"/>
</ROW>
</ROW>
</BOX>
...

Finally, the trend for all flights, repeating the items in the same fashion:

Creating the placeholders for the total flight stats
<ROW>
<BOX ...>
<TEXT value="Flight Duration" margin="0,0,10,0"/>
<ROW halign="between">
<COL valign="between" halign="center">
<TXT value="min" size="12"/>
<TXT value="0h 20m" color="#8B8000" style="h5"/>
</COL>
<COL valign="between" halign="center">
<TXT value="total" size="12"/>
<TXT value="5h 4m" style="h4" bold="true"/>
</COL>
<COL valign="between" halign="center">
<TXT value="max" size="12"/>
<TXT value="6h 3m" color="#027148" style="h5"/>
</COL>
</ROW>
</BOX>
...

Building The Top Right Jet Selection List and Plane Info Panel

Lastly, we move to the right. The top right quadrant contains a simple horizontal list like we built for the users, with info text to its left similar to the bottom left trend panes:

We see the list item for the jets
<SBOX border="all" radius="10" layout="col" pad="12" margin="8">
<ROW valign="center">
<COL expand="false" margin="10,20" valign="around">
<TXT value="Name:"/>
<TXT value="First Last" size="20"/>
<TXT value="Occupation:"/>
<TXT value="Users Occupation" size="25"/>
</COL>
<BOX height="100" margin="10" color="black" radius="10">
<LIST id="taildata" direction="horizontal" >
<ITEM pad="10" id="planeitem" layout="col" center="true" selected="={this.index} == 0" color="={this.selected} ? '#7b008322' : 'black'" bordercolor="={this.selected} ? '#c300d0' : null" border="all" margin="5" radius="10">
<ICON icon="airplanemode_active" size="34" color="={planeitem.selected} ? 'white' : 'grey'"/>
<TEXT value="AXAXA" color="={planeitem.selected} ? 'white' : 'grey'" size="10"/>
</ITEM>
</LIST>
</BOX>
</ROW>
</SBOX>

And a display panel with an image, with a repeated column of data allowing us to reuse its structure:

The info pane for the jets with the jet image
<SBOX ...>
...
<ROW layout="row" valign="center" color="black" radius="5">
<ROW id="boxheight" layout="row" color="#7b008366" halign="between" pad="10">
<SBOX pad="0,5" flex="1">
<TXT value="Price"/>
<TXT value="$ 1.4M" style="h5" margin="5"/>
<TXT value="Cost/Hr:"/>
<TXT value="$ 5000" style="h5" margin="5"/>
<TXT value="Fuel:"/>
<TXT value="500 gal/hr" style="h5" margin="5"/>
<TXT value="Yeary Additional:"/>
<TXT value="$ 100K" style="h5" margin="5"/>
</SBOX>
...
</ROW>
<BOX width="20" height="1"/>
<!--Giving the image a max height allows it not to oversize-->
<IMG url="resources/images/DOGEPLANE.jpg" maxheight="400" maxwidth="400"/>
</ROW>
</SBOX>

Building The Bottom Right Map and Layout

The lower quadrant contains charts on the left, and a map on the right, starting with the layout, we can use MAP directly rather than a placeholder:

Creating a map with a space for charts
...
<ROW flex="5">
<BOX border="all" radius="10" flex="2" pad="12" margin="8" color="black">
<!--This will house the three charts-->
</BOX>
<BOX flex="3">
<TXT value="Location Data" style="h5" margin="10"/>
<!--A map will display a blank, interactable, map when not given markers-->
<MAP zoom="5" flex="1" showall="true" margin="8" radius="10" border="all" >
</MAP>
</BOX>
</ROW>
</COL>

We can add a marker to display data within the map:

Adding a marker to the map
<MAP ...>
<MARKER latitude="0" longitude="0">
<!--A marker can be a combination of any widgets, we use
a simple icon-->
<ICON icon="location_on_outlined" color="black"/>
</MARKER>
</MAP>

Building The Bottom Right Charts

Lastly for the layout, we build the charts, which will display as an icon until data is added:

The output when charts are added without data

A line chart:

...
<BOX ...>
<TXT value="Flight Duration" style="h5" margin="10"/>
<!-- CHARTS take in axis to determine the chart scale
and series to plot the points-->
<CHART flex="1">
<XAXIS type="category" title="Date" />
<YAXIS type="numeric" title="Duration(h)" />
<SERIES type="bar" color="lightblue" />
</CHART>

A bar chart:

...
<TXT value="Arrival and Departure Time Average" style="h5" margin="10"/>
<CHART flex="1">
<XAXIS type="category" title="Hour" />
<YAXIS type="numeric" title="Flights" />
<SERIES type="bar" color="lightblue" />
</CHART>

and two pie charts:

...     
<TXT value="Flights Per Day Of Week" style="h5" margin="10"/>
<ROW>
<!--A pie chart only has an X axis-->
<CHART type="pie" showlegend="false">
<XAXIS title="Departures" />
<SERIES name="Departure Day" x="monday" color="purple"/>
</CHART>
<CHART type="pie" showlegend="false">
<XAXIS title="Arrivals" />
<SERIES name="Arrival Day" color="purple"/>
</CHART>
</ROW>
</BOX>

That’s all for the most difficult part: The Layout and UI! We have managed to build our entire dashboard’s UI in 271 lines. As you can tell, this takes up the majority of the line count and bulk of the project.

Bonus: Creating A Mobile Friendly UI

For a bonus round, we are going to use the built in databinding to create a mobile friendly layout within our template. You can do this either as the same template, or as a separate one. In our case we will do it combined to display more features of FML, and do it as the step after the web design. Normally, we would advice Mobile First Design be practiced.

First, we will determine the width at which our layout begins to look poor and cut off items, we can do this via the full dash to get a better sense of the layout, We can add a temporary {SYSTEM.screenwidth} binding to the title page:

Cut off items in the bottom left need to be handled

As we can see, 1748 is the first section we start to lose readability, mainly on the bottom left quadrant within the boxes.

Ensuring items arent cut off

To remedy this, we create a VAR to denote when the screen is too small:

<VAR id="isSmall" value="={SYSTEM.screenwidth} &lt; 1748"/>

And then force these items to wrap within their rows:

<TXT value="20 Day Flight Statistics" style="h5" margin="0,10,10,10"/>
<SCROLL>
<ROW wrap="={isSmall}">
...
</ROW>
<ROW wrap="={isSmall}">
</ROW>
</SCROLL>

Next we can find the next smallest threshold, which seems to be 1255. The largest problem here is the jet image, as well as the Recent flight stats, so with this we will do the same.

Creating a smaller screen view

First the VAR:

<VAR id="isSmaller" value="={SYSTEM.screenwidth} &lt; 1255"/>

With this we set the jet image visibility:

<IMG visible="=!{isSmaller}" .../>

And Wrap the recent flight stats rows:

<ROW  wrap="={isSmaller}">
...
</ROW>
<ROW wrap="={isSmaller}">
...
</ROW>

Finally, we will determine the mobile view, which in this case we will use anything less than 940.

Creating a mobile response

Again creating a VAR:

<VAR id="isMobile" value="={SYSTEM.screenwidth} &lt; 940"/>

We force the jet info panel to wrap:

<ROW id="boxheight" layout="row" color="#7b008366" halign="between" pad="10" wrap="={isMobile}">

Hide the header text

<TEXT value="FMLJet" visible="={isMobile}" style="h2"/>

Wrap a box and a scroller around the two columns to set the correct layout direction, as well as assigning them min and max heights:

<SCROLL>
<BOX layout="={isMobile} ? 'col' : 'row'" minheight="1000">
</BOX>
</SCROLL>

And finally, do the same for the bottom right, changing the ROW to a BOX so we can choose layout.

<BOX layout="={isMobile} ? 'col' : 'row'" flex="5">

Ideally, for mobile you would have a separate view where each box is a PAGE in the PAGER widget.

Conclusion of Part 1

Up to this point, we have the following UI that responds to screen size changes:

The final layout after completing Part 1

With that we have completed the overall layout for the dashboard, with the majority of its functionality built in prior to the data being added! Layout, by far, is the most time consuming section. We will see that adding, transforming, manipulating, and driving UI based on datasources in Part 2 is much easier and quicker! Remember, for any assistance, suggestions, or feedback:

Thanks for reading! For part 2 of 2, check out the next article: Connecting Data and Functions.

--

--

Isaac Olajos

Health and Technology. Flutter Markup Language developer, Integrated Movement Training owner.