This story is also available as a YouTube video. Watch it here.
A couple of days ago, I had the idea of building an app that would show the translation of an inputted word over each country in their respective languages. I wanted to be able to see how languages developed and changed geographically.
I had never built mobile apps or manipulated maps before, but I wanted to give it a try anyway.
Being able to compile my code to iOS, Android, and Web without relying on multiple code bases made me decide to build it with Flutter. Flutter also has a bunch of libraries available, so I knew it would be easy to get started.
Flutter Map is an open-source package for Flutter that provides a highly customizable map widget. With Flutter Map, developers can create interactive maps in their Flutter applications, including features like markers, polygons, polylines, and tile layers.
You can see that I highlighted a couple of words above. This was when my problems started. I thought that learning Flutter would be my biggest challenge. However, learning how maps actually work was a bit more tricky for me. But before I explain why, let's build an app with the sole purpose of displaying a map with Flutter Map.
Step 1: Displaying a map
This is the first screen I implemented. A simple map that I could play around with. As I zoom in & out and move around, the map would automatically adjusts and show more or less information according to how much zoom I apply.
Displaying a map is as easy as implementing the following widget:
return Scaffold(
body: Stack(
children: [
FlutterMap(
options: MapOptions(
center: LatLng(51.509364, -0.128928),
zoom: 3.2,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.example.app',
),
],
),
],
),
);
This will result in the following screen:
Pretty cool, isn't it? I had just built my first app, but I had no clue how I did it.
So let's take a step back and analyze the code I had just copied and pasted from Flutter Map's documentation.
Understanding the basics of Flutter (Skip this part if you're familiar with it)
You can see that the widget we built is composed of a Scaffold whose body is a Stack and whose child is a FlutterMap.
return Scaffold(
body: Stack(
children: [
FlutterMap(
[...]
),
],
),
);
Everything in Flutter is a widget, and widgets are just tiny chunks of UI that you can combine to make a complete app. Building an app in Flutter is like building a Lego set - piece by piece.
If we had multiple children inside our Stack, they would've been displayed on top of each other, as in an actual stack. If, instead of a Stack widget, we had a Row or Column, their children would be displayed as their names suggest. You just need to play around with the widgets to get them to be displayed as you expect in your app.
In fact, for this simple application, we didn't even need the Stack and Scaffold widget on the back of the FlutterMap widget. If we had returned just the FlutterMap widget, the result would've been the same.
return FlutterMap(
[...]
);
Back to the FlutterMap
The FlutterMap widget is provided by the Flutter Map library. You can see that it's composed of options and children.
Through the options, we're telling this widget that we want this map to be initialized centered in London (51.509364, -0.128928) with a zoom of 3.2 applied.
Our children are the layers of our map. For this example, we only need one layer, which is a Tile Layer, responsible for fetching the map images from Open Street Maps.
Tile Server URL
If you pay attention to our TileLayer, you will see that one of the parameters is the urlTemplate.
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.example.app',
),
This kind of URL is called a "tile server URL". It is a standard format used by many map tile providers to deliver map tiles to mapping software. In the URL, the {z}, {x}, and {y} placeholders are used to specify the zoom level, the X-coordinate, and the Y-coordinate of the tile, respectively. When a map is displayed, the mapping software will make a request to the tile server URL for each tile needed to display the current view.
Here are two tiles that when combined display Portugal entirely:
Our map is actually generated based on multiple static images placed side by side. As we move around and zoom in & out, our app will fetch new images from the Tile Server URL to fill our screen with the map.
Different Tile Server URL Providers
Learning about Tile Server URL led me to my next problem. Open Street Maps displays too much information as you zoom in. Political borders, country names, roads, rivers… All of this information would make my translations hard to see. I needed to get rid of them. But since the images are static, I could not use the tile server provided by OpenStreetMaps. I would have to find a different one.
With a few Google searches, I was able to find multiple map providers, a few of them paid, others free.
I wanted to find something simple. Just the world map with the border of the countries. The closest I got to what I wanted was a Tile Server provided by Stamen Design with the name "Toner". They offer six different flavors, and one of them is only the background. No labels. Close to what I needed.
The "background" one was perfect for me, so I got the Tile Server URL:
https://stamen-tiles.a.ssl.fastly.net/toner-background/{z}/{x}/{y}.png
And by replacing the urlTemplate in my widget, I could immediately see the results:
FlutterMap(
options: MapOptions(
center: LatLng(51.509364, -0.128928),
zoom: 3.2,
),
children: [
TileLayer(
urlTemplate: 'https://stamen-tiles.a.ssl.fastly.net/toner-background/{z}/{x}/{y}.png',
userAgentPackageName: 'com.example.app',
),
],
);
Step 2: Placing a Label on the Map
My idea is to build an app in which I can input a word and see the translation of this word over each country in their respective languages. Now that I have a map showing up, I had to find a way to display labels over it on specific coordinates.
Map Layers
Let's look back at a previous paragraph:
[In Flutter Map] Our children are the layers of our map. For this example, we only need one layer, which is a Tile Layer, responsible for fetching the map images from Open Street Maps.
Our map is composed of layers on top of each other. For the previous example, we only required one layer: TileLayer. However, the Flutter Map library provides other types of layers, such as MarkerLayer, PolygonLayer, PolylineLayer, CircleLayer, and AttributionLayer.
PolygonLayer
The polygon layer is used for displaying a polygon over our map. Let's take a look:
PolygonLayer(
polygonCulling: false,
polygons: [
Polygon(
points: [
LatLng(36.95, -9.5),
LatLng(42.25, -9.5),
LatLng(42.25, -6.2),
LatLng(36.95, -6.2),
],
color: Colors.blue.withOpacity(0.5),
borderStrokeWidth: 2,
borderColor: Colors.blue,
isFilled: true
),
],
)
The example above is creating a PolygonLayer widget, which is used to display one or more polygons on the map.
The PolygonLayer widget takes a list of Polygon objects as its polygons parameter. In this case, there is only one polygon defined in the list.
The Polygon class is used to define a polygon on the map, and it takes several parameters to define the shape, location, and appearance of the polygon.
The polygonCulling parameter is a boolean value that specifies whether to cull (remove) polygons that are entirely outside the viewable area of the map. Setting it to false means that all polygons will be rendered regardless of whether they are outside the viewable area of the map.
In this example, the Polygon is defined with a points parameter, which is a list of LatLng objects that define the vertices of the polygon. The color parameter specifies the fill color of the polygon. In this case, we're placing a square over Portugal. Let's see how it looks like:
PolylineLayer
PolylineLayer(
polylines: [
Polyline(
points: [
LatLng(38.73, -9.14), // Lisbon, Portugal
LatLng(51.50, -0.12), // London, United Kingdom
LatLng(52.37, 4.90), // Amsterdam, Netherlands
],
color: Colors.blue,
strokeWidth: 2,
),
],
)
The example above creates a PolylineLayer widget, which is used to display one or more polylines on the map.
The PolylineLayer widget takes a list of Polyline objects as its polylines parameter. In this case, there is only one polyline defined in the list.
The Polyline class is used to define a polyline on the map, and it takes several parameters to define the shape, location, and appearance of the polyline.
In this example, the Polyline is defined with a points parameter, which is a list of LatLng objects that define the vertices of the polyline. The color parameter specifies the color of the polyline.
Therefore, we're drawing a line connecting the capitals of Portugal, England, and the Netherlands.
CircleLayer
CircleLayer(
circles: [
CircleMarker(
point: LatLng(52.2677, 5.1689), // center of 't Gooi
radius: 5000,
useRadiusInMeter: true,
color: Colors.red.withOpacity(0.3),
borderColor: Colors.red.withOpacity(0.7),
borderStrokeWidth: 2,
)
],
)
The example above creates a CircleLayer widget, which is used to display one or more circles on the map.
The CircleLayer widget takes a list of CircleMarker objects as its circles parameter. In this case, there is only one circle defined in the list.
The CircleMaker class is used to define a circle on the map, and it takes several parameters to define the shape, location, and appearance of the circle.
In this example, the CircleMarker is defined with a point parameter, which specifies the location of the marker on the map as a LatLng object. The color parameter specifies the color of the circle and the radius parameter the size of the circle.
We're drawing a circle over the 't Gooi area in the Netherlands with a radius of 5km.
MarkerLayer
The marker layer is the most simple one. We use it for displaying a widget on a specific coordinate. Let's take a look:
MarkerLayer(
markers: [
Marker(
point: LatLng(51.509364, -0.128928),
width: 80,
height: 80,
builder: (context) => FlutterLogo(),
),
],
)
In the example above we create a MarkerLayer widget, which is used to display one or more markers on the map.
The MarkerLayer widget takes a list of Marker objects as its markers parameter. In this case, there is only one marker defined in the list.
The Marker class is used to define a marker on the map, and it takes several parameters to define the location, size, and appearance of the marker.
In this example, the Marker is defined with a point parameter, which specifies the location of the marker on the map as a LatLng object. The width and height parameters specify the size of the marker in pixels, and the builder parameter takes a function that returns a Widget to define the appearance of the marker.
In this example, the builder function is using the FlutterLogo widget to display a Flutter logo at the location specified by the point parameter, which is London.
Placing Words on the Map
As I said before, my objective is to place words on the map, and the MarkerLayer is the perfect solution for it.
Before we create our markers, let's create our text widgets. Starting with the style:
TextStyle getDefaultTextStyle() {
return const TextStyle(
fontSize: 12,
backgroundColor: Colors.black,
color: Colors.white,
);
}
This method will return a TextStyle object defining the font size, the background color, and the color of the font. It'll be used by:
Container buildTextWidget(String word) {
return Container(
alignment: Alignment.center,
child: Text(
word,
textAlign: TextAlign.center,
style: getDefaultTextStyle()
)
);
}
Our Text widget receives a word as its text and is placed within a Container widget.
Marker buildMarker(LatLng coordinates, String word) {
return Marker(
point: coordinates,
width: 100,
height: 12,
builder: (context) => buildTextWidget(word)
);
}
The buildTextWidget function is called within our buildMarker function to create the Markers that will be the children of our MarkerLayer widget. They will be created with the coordinates where they'll be placed and the word they'll display.
MarkerLayer(
markers: [
buildMarker(LatLng(39.3999, -8.2245), "Amor"), // Portugal
buildMarker(LatLng(55.3781, -3.4360), "Love"), // England
buildMarker(LatLng(46.2276, 2.2137), "Aimer"), // France
buildMarker(LatLng(52.1326, 5.2913), "Liefde"), // Netherlands
buildMarker(LatLng(51.1657, 10.4515), "Liebe"), // Germany
],
)
And finally:
Conclusion
In this story, we learned the basics of how maps are rendered and a few possibilities when it comes to Tile Servers.
Besides that, we also learned how our map is sliced in layers and how to take advantage of them to place markers, polygons, polylines, and circles on specific coordinates.
To wrap up, we learned through a practical example how to display text labels over specific countries on our map.
What's next?
My journey with Flutter is just getting started. This is just the surface of the first app I've been building.
In my next stories, I'll show how I was able to allow the user to input a word and translate this word into several languages so that it could be displayed on the map.
Besides that, I'll also show how to manipulate your map in more advanced ways, such as increasing and decreasing the size of the labels as we zoom in & out and how to implement a map controller to use buttons instead of gestures to control our map.
Stay tuned!
Top comments (0)