Fourth post in the series of Flutter and Google Maps.
In the Third post we have implemented conversion of coordinates to human-readable address using Google Maps API.
In this post, I will show you how to show a static map in Flutter Google Maps.
Table of contents
Open Table of contents
Desired Outcome
We want to show user a static map image on the Home screen.
Prerequisites
Google Maps Static map
is a simple image that shows a map with markers, lines, and shapes.
It requires an API key to access the Google Maps API.
Before implementing your solution, spend time reading the Google Maps Static API documentation.
To summarize approach: you will send a network request to https://maps.googleapis.com/maps/api/staticmap
with various parameters
to get the desired map image.
The parameter list is big and provides you with a lot of flexibility to customize the map image.
In this post, we will show a simple map with a marker at a specific location with a zoom level, also we will set maptype
as roadmap
.
Once we are done with this example, I encourage you to play with other parameters and see how it affects the map image.
Home Screen preparation
Let’s start by preparing the Home screen to show the static map.
Follow code snippet below to update the home.dart
file.
129 collapsed lines
import 'dart:convert';
import 'package:flutter/foundation.dart';import 'package:flutter/material.dart';import 'package:flutter_dotenv/flutter_dotenv.dart';import 'package:google_maps_project/models/user_location.dart';import 'package:google_maps_project/screens/map.dart';import 'package:http/http.dart' as http;import 'package:location/location.dart';
class HomeScreen extends StatefulWidget { const HomeScreen({super.key});
@override State<HomeScreen> createState() { return _HomeScreenState(); }}
class _HomeScreenState extends State<HomeScreen> { var _isLoading = false; final String _apiKey = dotenv.get('GOOGLE_MAPS_API_KEY'); UserLocation _userLocation = const UserLocation(latitude: 37.422, longitude: -122.084); var _isLoadingAddress = false; var _parsedAddress = 'Unknown Address';
@override void initState() { super.initState(); _getCurrentLocation(); }
void _getCurrentLocation() async { setState(() { _isLoading = true; });
Location location = Location();
bool serviceEnabled; PermissionStatus permissionGranted; LocationData locationData;
serviceEnabled = await location.serviceEnabled();
if (!serviceEnabled) { serviceEnabled = await location.requestService(); if (!serviceEnabled) { setState(() { _isLoading = false; }); return; } }
permissionGranted = await location.hasPermission();
if (permissionGranted == PermissionStatus.denied) { permissionGranted = await location.requestPermission(); if (permissionGranted != PermissionStatus.granted) { setState(() { _isLoading = false; }); return; } }
locationData = await location.getLocation();
final lat = locationData.latitude; final lng = locationData.longitude;
if (lat == null || lng == null) { setState(() { _isLoading = false; }); return; }
if (kDebugMode) { print('Using Users current location'); }
_userLocation = UserLocation(latitude: lat, longitude: lng); setState(() { _isLoading = false; _isLoadingAddress = true; });
_fetchAddress(_userLocation.latitude, _userLocation.longitude); }
Future<void> _fetchAddress(double latitude, double longitude) async { final url = Uri.parse( 'https://maps.googleapis.com/maps/api/geocode/json?latlng=$latitude,$longitude&key=$_apiKey', );
final response = await http.get(url); final resData = json.decode(response.body); final address = resData['results'][0]['formatted_address'];
setState(() { _parsedAddress = address; _isLoadingAddress = false; }); }
void _showInteractiveMap() async { UserLocation? selectedLocation = await Navigator.of(context).push<UserLocation>( MaterialPageRoute( builder: (ctx) => MapScreen( location: _userLocation, ), ), );
if (selectedLocation != null) { setState(() { _userLocation = selectedLocation; _isLoadingAddress = true; }); }
_fetchAddress(_userLocation.latitude, _userLocation.longitude); }
@override Widget build(BuildContext context) {12 collapsed lines
Widget addressValue = const CircularProgressIndicator();
if (!_isLoadingAddress) { addressValue = Text( _parsedAddress, style: TextStyle( color: Theme.of(context).colorScheme.onBackground, fontSize: 24, ), ); }
Widget content = Column( mainAxisAlignment: MainAxisAlignment.center, children: [8 collapsed lines
Text( 'Your current location is:', style: TextStyle(color: Theme.of(context).colorScheme.onBackground), ), addressValue, const SizedBox( height: 16, ),
Container( height: 200, width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( width: 2, color: Theme.of(context).colorScheme.primary.withOpacity(0.6), ), ), alignment: Alignment.center,
child: const Text('Soon'), ), const SizedBox( height: 16, ),14 collapsed lines
Center( child: TextButton.icon( icon: const Icon(Icons.map), label: const Text('Show Map'), onPressed: _showInteractiveMap, ), ), ], );
if (_isLoading) { content = const Center(child: CircularProgressIndicator()); }
return Scaffold( appBar: AppBar( title: const Text('Google Maps Case'), ),
body: SingleChildScrollView( padding: const EdgeInsets.all(24), child: content, ),3 collapsed lines
); }}
After updating the home.dart
file, you should see a simple container with a border on the Home screen right above the Show Map
button.
Add Static Map Image
Now, let’s add the static map image to the Home screen to replace our placeholder.
128 collapsed lines
import 'dart:convert';
import 'package:flutter/foundation.dart';import 'package:flutter/material.dart';import 'package:flutter_dotenv/flutter_dotenv.dart';import 'package:google_maps_project/models/user_location.dart';import 'package:google_maps_project/screens/map.dart';import 'package:http/http.dart' as http;import 'package:location/location.dart';
class HomeScreen extends StatefulWidget { const HomeScreen({super.key});
@override State<HomeScreen> createState() { return _HomeScreenState(); }}
class _HomeScreenState extends State<HomeScreen> { var _isLoading = false; final String _apiKey = dotenv.get('GOOGLE_MAPS_API_KEY'); UserLocation _userLocation = const UserLocation(latitude: 37.422, longitude: -122.084); var _isLoadingAddress = false; var _parsedAddress = 'Unknown Address';
@override void initState() { super.initState(); _getCurrentLocation(); }
void _getCurrentLocation() async { setState(() { _isLoading = true; });
Location location = Location();
bool serviceEnabled; PermissionStatus permissionGranted; LocationData locationData;
serviceEnabled = await location.serviceEnabled();
if (!serviceEnabled) { serviceEnabled = await location.requestService(); if (!serviceEnabled) { setState(() { _isLoading = false; }); return; } }
permissionGranted = await location.hasPermission();
if (permissionGranted == PermissionStatus.denied) { permissionGranted = await location.requestPermission(); if (permissionGranted != PermissionStatus.granted) { setState(() { _isLoading = false; }); return; } }
locationData = await location.getLocation();
final lat = locationData.latitude; final lng = locationData.longitude;
if (lat == null || lng == null) { setState(() { _isLoading = false; }); return; }
if (kDebugMode) { print('Using Users current location'); }
_userLocation = UserLocation(latitude: lat, longitude: lng); setState(() { _isLoading = false; _isLoadingAddress = true; });
_fetchAddress(_userLocation.latitude, _userLocation.longitude); }
Future<void> _fetchAddress(double latitude, double longitude) async { final url = Uri.parse( 'https://maps.googleapis.com/maps/api/geocode/json?latlng=$latitude,$longitude&key=$_apiKey', );
final response = await http.get(url); final resData = json.decode(response.body); final address = resData['results'][0]['formatted_address'];
setState(() { _parsedAddress = address; _isLoadingAddress = false; }); }
void _showInteractiveMap() async { UserLocation? selectedLocation = await Navigator.of(context).push<UserLocation>( MaterialPageRoute( builder: (ctx) => MapScreen( location: _userLocation, ), ), );
if (selectedLocation != null) { setState(() { _userLocation = selectedLocation; _isLoadingAddress = true; }); }
_fetchAddress(_userLocation.latitude, _userLocation.longitude); }
String _createStaticMapUrl() { double lat = _userLocation.latitude; double lng = _userLocation.longitude;
return 'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng&zoom=16&size=600x300&maptype=roadmap&markers=color:red%7Clabel:1%7C$lat,$lng&key=$_apiKey'; }
@override Widget build(BuildContext context) {23 collapsed lines
Widget addressValue = const CircularProgressIndicator();
if (!_isLoadingAddress) { addressValue = Text( _parsedAddress, style: TextStyle( color: Theme.of(context).colorScheme.onBackground, fontSize: 24, ), ); }
Widget content = Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Your current location is:', style: TextStyle(color: Theme.of(context).colorScheme.onBackground), ), addressValue, const SizedBox( height: 16, ),
Container( height: 200, width: double.infinity,7 collapsed lines
decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( width: 2, color: Theme.of(context).colorScheme.primary.withOpacity(0.6), ), ), alignment: Alignment.center,
clipBehavior: Clip.antiAlias,
child: Image.network(
_createStaticMapUrl(), fit: BoxFit.cover, width: double.infinity, height: double.infinity,
loadingBuilder: (ctx, child, loadingProgress) { if (loadingProgress == null) { return child; }
return const Center( child: CircularProgressIndicator(), ); }, ), ),28 collapsed lines
const SizedBox( height: 16, ), Center( child: TextButton.icon( icon: const Icon(Icons.map), label: const Text('Show Map'), onPressed: _showInteractiveMap, ), ), ], );
if (_isLoading) { content = const Center(child: CircularProgressIndicator()); }
return Scaffold( appBar: AppBar( title: const Text('Google Maps Case'), ), body: SingleChildScrollView( padding: const EdgeInsets.all(24), child: content, ), ); }}
Save your changes and you will see two updates in action: the static map image and the loading animation while the image is loading.
A different way - Journey
If you don’t want to use Google Maps API, you can use the alternative solution - Journey.
Journey is a simple and easy-to-use Flutter widget that provides flexible configurable static maps.
Note: this package is
work in progress
and may have some limitations and can be changed in the future.
First, you need to request API key from Journey here: Journey - Request Access.
Follow the instructions to install the package and use it in your Flutter project.
It’s basic, install a package:
1flutter pub add static_map
Add Journey’s API key to your .env
file: JOURNEY_MAPS_API_KEY=YOUR_API_KEY
.
Update main.dart
file to load the API key from the .env
file and initialize the Journey’s StaticMap
widget.
6 collapsed lines
import 'package:flutter/material.dart';import 'package:flutter_dotenv/flutter_dotenv.dart';import 'package:google_maps_project/screens/home.dart';import 'package:google_maps_project/theme.dart';import 'package:static_map/static_map.dart';
void main() async { WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: ".env");
StaticMap.initialize(apiKey: dotenv.get('JOURNEY_MAPS_API_KEY'));
runApp(const MyApp());}14 collapsed lines
class MyApp extends StatelessWidget { const MyApp({super.key});
// This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: defaultTheme, home: const HomeScreen(), ); }}
And now let’s move to update our home.dart
file to use Journey’s StaticMapImage
widget.
We will reuse Container
we built for Google Maps example and replace Image.network
with StaticMapImage
.
Check Basic Usage documentation for more details.
191 collapsed lines
1import 'dart:convert';2
3import 'package:flutter/foundation.dart';4import 'package:flutter/material.dart';5import 'package:flutter_dotenv/flutter_dotenv.dart';6import 'package:google_maps_project/models/user_location.dart';7import 'package:google_maps_project/screens/map.dart';8import 'package:http/http.dart' as http;9import 'package:location/location.dart';10import 'package:static_map/static_map.dart';11
12class HomeScreen extends StatefulWidget {13 const HomeScreen({super.key});14
15 @override16 State<HomeScreen> createState() {17 return _HomeScreenState();18 }19}20
21class _HomeScreenState extends State<HomeScreen> {22 var _isLoading = false;23 final String _apiKey = dotenv.get('GOOGLE_MAPS_API_KEY');24 UserLocation _userLocation =25 const UserLocation(latitude: 37.422, longitude: -122.084);26 var _isLoadingAddress = false;27 var _parsedAddress = 'Unknown Address';28
29 @override30 void initState() {31 super.initState();32 _getCurrentLocation();33 }34
35 void _getCurrentLocation() async {36 setState(() {37 _isLoading = true;38 });39
40 Location location = Location();41
42 bool serviceEnabled;43 PermissionStatus permissionGranted;44 LocationData locationData;45
46 serviceEnabled = await location.serviceEnabled();47
48 if (!serviceEnabled) {49 serviceEnabled = await location.requestService();50 if (!serviceEnabled) {51 setState(() {52 _isLoading = false;53 });54 return;55 }56 }57
58 permissionGranted = await location.hasPermission();59
60 if (permissionGranted == PermissionStatus.denied) {61 permissionGranted = await location.requestPermission();62 if (permissionGranted != PermissionStatus.granted) {63 setState(() {64 _isLoading = false;65 });66 return;67 }68 }69
70 locationData = await location.getLocation();71
72 final lat = locationData.latitude;73 final lng = locationData.longitude;74
75 if (lat == null || lng == null) {76 setState(() {77 _isLoading = false;78 });79 return;80 }81
82 if (kDebugMode) {83 print('Using Users current location');84 }85
86 _userLocation = UserLocation(latitude: lat, longitude: lng);87 setState(() {88 _isLoading = false;89 _isLoadingAddress = true;90 });91
92 _fetchAddress(_userLocation.latitude, _userLocation.longitude);93 }94
95 Future<void> _fetchAddress(double latitude, double longitude) async {96 final url = Uri.parse(97 'https://maps.googleapis.com/maps/api/geocode/json?latlng=$latitude,$longitude&key=$_apiKey',98 );99
100 final response = await http.get(url);101 final resData = json.decode(response.body);102 final address = resData['results'][0]['formatted_address'];103
104 setState(() {105 _parsedAddress = address;106 _isLoadingAddress = false;107 });108 }109
110 void _showInteractiveMap() async {111 UserLocation? selectedLocation =112 await Navigator.of(context).push<UserLocation>(113 MaterialPageRoute(114 builder: (ctx) => MapScreen(115 location: _userLocation,116 ),117 ),118 );119
120 if (selectedLocation != null) {121 setState(() {122 _userLocation = selectedLocation;123 _isLoadingAddress = true;124 });125 }126
127 _fetchAddress(_userLocation.latitude, _userLocation.longitude);128 }129
130 String _createStaticMapUrl() {131 double lat = _userLocation.latitude;132 double lng = _userLocation.longitude;133
134 return 'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng&zoom=16&size=600x300&maptype=roadmap&markers=color:red%7Clabel:1%7C$lat,$lng&key=$_apiKey';135 }136
137 @override138 Widget build(BuildContext context) {139 Widget addressValue = const CircularProgressIndicator();140
141 if (!_isLoadingAddress) {142 addressValue = Text(143 _parsedAddress,144 style: TextStyle(145 color: Theme.of(context).colorScheme.onBackground,146 fontSize: 24,147 ),148 );149 }150
151 Widget content = Column(152 mainAxisAlignment: MainAxisAlignment.center,153 children: [154 Text(155 'Your current location is:',156 style: TextStyle(color: Theme.of(context).colorScheme.onBackground),157 ),158 addressValue,159 const SizedBox(160 height: 16,161 ),162 Container(163 height: 200,164 width: double.infinity,165 decoration: BoxDecoration(166 borderRadius: BorderRadius.circular(12),167 border: Border.all(168 width: 2,169 color: Theme.of(context).colorScheme.primary.withOpacity(0.6),170 ),171 ),172 alignment: Alignment.center,173 clipBehavior: Clip.antiAlias,174 child: Image.network(175 _createStaticMapUrl(),176 fit: BoxFit.cover,177 width: double.infinity,178 height: double.infinity,179 loadingBuilder: (ctx, child, loadingProgress) {180 if (loadingProgress == null) {181 return child;182 }183
184 return const Center(185 child: CircularProgressIndicator(),186 );187 },188 ),189 ),190 const SizedBox(191 height: 16,192 ),193 Container(11 collapsed lines
194 height: 200,195 width: double.infinity,196 decoration: BoxDecoration(197 borderRadius: BorderRadius.circular(12),198 border: Border.all(199 width: 2,200 color: Theme.of(context).colorScheme.primary.withOpacity(0.6),201 ),202 ),203 alignment: Alignment.center,204 clipBehavior: Clip.antiAlias,205
206 child: StaticMapImage(207
208 imageOptions: const ImageOptions(209 fit: BoxFit.cover,210 ),211
212 loadingBuilder: (ctx, child, loadingProgress) {213 if (loadingProgress == null) {214 return child;215 }216
217 return const Center(218 child: CircularProgressIndicator(),219 );220 },221
222 options: StaticMapOptions(223 center: StaticMapLatLng(224 _userLocation.latitude,225 _userLocation.longitude,226 ),227 zoom: 16,228 width: 600,229 height: 300,230 scale: 2,231 overlays: [232
233 StaticMapMarker(234 point: StaticMapLatLng(235 _userLocation.latitude,236 _userLocation.longitude,237 ),238 color: Colors.red,239 size: 8,240 label: 'A',241 labelSize: 14,242 labelColor: Colors.black,243 )244 ],245 ),246 ),247 ),28 collapsed lines
248 const SizedBox(249 height: 16,250 ),251 Center(252 child: TextButton.icon(253 icon: const Icon(Icons.map),254 label: const Text('Show Map'),255 onPressed: _showInteractiveMap,256 ),257 ),258 ],259 );260
261 if (_isLoading) {262 content = const Center(child: CircularProgressIndicator());263 }264
265 return Scaffold(266 appBar: AppBar(267 title: const Text('Google Maps Case'),268 ),269 body: SingleChildScrollView(270 padding: const EdgeInsets.all(24),271 child: content,272 ),273 );274 }275}
Save, refresh app, and you should see another static map image added below Google Maps Static Map image, but this one is generated by Journey’s StaticMapImage
widget.
Conclusion
In this post, we have learned how to show a static map in Flutter using Google Maps API.
We have also introduced an alternative solution - Journey, which provides a simple and easy-to-use Flutter widget for static maps.
I encourage you to play with different parameters and see how it affects the map image.
Thank you from reading and treat yourself with a good piece of cake 🍰