Map download¶
Setup¶
Prerequisites¶
Build and run¶
Go to the map_download
directory within the Flutter examples directory, which is the name of this example project.
Note - the gem_kit
directory containing the Maps SDK for Flutter
should be in the plugins
directory of the example, e.g.
example_pathname/plugins/gem_kit
- see the environment setup guide above.
Run: flutter pub get
Configure the native parts:
First, verify that the ANDROID_SDK_ROOT
environment variable
is set to the root path of your android SDK.
In android/build.gradle
add the maven
block as shown,
within the allprojects
block, for both debug and release builds:
1 allprojects {
2 repositories {
3 google()
4 mavenCentral()
5 maven {
6 url "${rootDir}/../plugins/gem_kit/android/build"
7 }
8 }
9 }
in android/app/build.gradle
within the android
block, in the defaultConfig
block,
the android SDK version minSdk
must be set as shown below.
Additionally, for release builds, in android/app/build.gradle
,
within the android
block, add the buildTypes
block as shown:
Replace example_pathname
with the actual project pathname
1android {
2 defaultConfig {
3 applicationId "com.magiclane.gem_kit.examples.example_pathname"
4 minSdk 21
5 targetSdk flutter.targetSdk
6 versionCode flutterVersionCode.toInteger()
7 versionName flutterVersionName
8 }
9 buildTypes {
10 release {
11 minifyEnabled false
12 shrinkResources false
13
14 // TODO: Add your own signing config for the release build.
15 // Signing with the debug keys for now, so `flutter run --release` works.
16 signingConfig signingConfigs.debug
17 }
18 }
19}
Then run the project:
flutter run --debug
orflutter run --release
How it works¶
Map Initialization¶
1import 'package:gem_kit/core.dart';
2import 'package:gem_kit/map.dart';
3import 'maps_page.dart';
4import 'package:flutter/material.dart';
5
6Future<void> main() async {
7 const projectApiToken = String.fromEnvironment('GEM_TOKEN');
8 await GemKit.initialize(appAuthorization: projectApiToken);
9 runApp(const MyApp());
10}
11
12class MyApp extends StatelessWidget {
13 const MyApp({super.key});
14
15 @override
16 Widget build(BuildContext context) {
17 return const MaterialApp(
18 debugShowCheckedModeBanner: false,
19 title: 'Map Download',
20 home: MyHomePage(),
21 );
22 }
23}
The dart
material package is imported, as well as the required gem_kit
packages. The map is embedded in a widget that serves as the root of the application.
Map Display and UI Components¶
1class MyHomePage extends StatefulWidget {
2 const MyHomePage({super.key});
3
4 @override
5 State<MyHomePage> createState() => _MyHomePageState();
6}
7
8class _MyHomePageState extends State<MyHomePage> {
9 @override
10 void dispose() {
11 GemKit.release();
12 super.dispose();
13 }
14
15 @override
16 Widget build(BuildContext context) {
17 return Scaffold(
18 appBar: AppBar(
19 backgroundColor: Colors.deepPurple[900],
20 title: const Text(
21 'Map Download',
22 style: TextStyle(color: Colors.white),
23 ),
24 actions: [
25 IconButton(
26 onPressed: () => _onMapButtonTap(context),
27 icon: const Icon(
28 Icons.map_outlined,
29 color: Colors.white,
30 ),
31 ),
32 ],
33 ),
34 body: GemMap(onMapCreated: _onMapCreated),
35 );
36 }
37
38 void _onMapCreated(GemMapController controller) async {
39 SdkSettings.setAllowOffboardServiceOnExtraChargedNetwork(
40 ServiceGroupType.contentService, true);
41 }
42
43 void _onMapButtonTap(BuildContext context) async {
44 Navigator.of(context).push(MaterialPageRoute<dynamic>(
45 builder: (context) => const MapsPage(),
46 ));
47 }
48}
This code defines the main app structure and how it handles the map’s initialization and the navigation to the map download page.
Maps page¶
1import 'package:gem_kit/content_store.dart';
2import 'package:gem_kit/core.dart';
3import 'maps_item.dart';
4import 'package:flutter/material.dart';
5import 'dart:async';
6
7class MapsPage extends StatefulWidget {
8 const MapsPage({super.key});
9
10 @override
11 State<MapsPage> createState() => _MapsPageState();
12}
13
14class _MapsPageState extends State<MapsPage> {
15 List<ContentStoreItem> mapsList = [];
16
17 @override
18 Widget build(BuildContext context) {
19 return Scaffold(
20 appBar: AppBar(
21 automaticallyImplyLeading: true,
22 foregroundColor: Colors.white,
23 title: const Text(
24 "Maps List",
25 style: TextStyle(color: Colors.white),
26 ),
27 backgroundColor: Colors.deepPurple[900],
28 ),
29 body: FutureBuilder<List<ContentStoreItem>>(
30 future: _getMaps(),
31 builder: (context, snapshot) {
32 if (!snapshot.hasData || snapshot.data == null) {
33 return const Center(
34 child: CircularProgressIndicator(),
35 );
36 }
37 return Scrollbar(
38 child: ListView.separated(
39 padding: EdgeInsets.zero,
40 itemCount: snapshot.data!.length,
41 separatorBuilder: (context, index) => const Divider(
42 indent: 50,
43 height: 0,
44 ),
45 itemBuilder: (context, index) {
46 final map = snapshot.data!.elementAt(index);
47 return MapsItem(map: map);
48 },
49 ),
50 );
51 }),
52 );
53 }
54
55 Future<List<ContentStoreItem>> _getMaps() async {
56 Completer<List<ContentStoreItem>> mapsList =
57 Completer<List<ContentStoreItem>>();
58 ContentStore.asyncGetStoreContentList(ContentType.roadMap,
59 (err, items, isCached) {
60 if (err == GemError.success && items != null) {
61 mapsList.complete(items);
62 }
63 });
64 return mapsList.future;
65 }
66}
The MapsPage
widget fetches the list of maps available for download and displays them in a scrollable list.
Map Item¶
1import 'dart:typed_data';
2import 'package:gem_kit/content_store.dart';
3import 'package:gem_kit/core.dart';
4import 'package:gem_kit/map.dart';
5import 'package:flutter_slidable/flutter_slidable.dart';
6import 'package:flutter/material.dart';
7import 'dart:async';
8
9class MapsItem extends StatefulWidget {
10 final ContentStoreItem map;
11
12 const MapsItem({super.key, required this.map});
13
14 @override
15 State<MapsItem> createState() => _MapsItemState();
16}
17
18class _MapsItemState extends State<MapsItem> {
19 late bool _isDownloaded;
20
21 double _downloadProgress = 0;
22
23 @override
24 void initState() {
25 super.initState();
26 _isDownloaded = widget.map.isCompleted;
27 _downloadProgress = widget.map.downloadProgress.toDouble();
28
29 if (_isDownloadingOrWaiting()) {
30 final errCode = widget.map.pauseDownload();
31 if (errCode != GemError.success) {
32 print(
33 "Download pause for item ${widget.map.id} failed with code $errCode");
34 return;
35 }
36
37 Future<dynamic>.delayed(const Duration(milliseconds: 500))
38 .then((value) => _downloadMap());
39 }
40 }
41
42 bool _isDownloadingOrWaiting() {
43 final status = widget.map.status;
44 return [
45 ContentStoreItemStatus.downloadQueued,
46 ContentStoreItemStatus.downloadRunning,
47 ContentStoreItemStatus.downloadWaiting,
48 ContentStoreItemStatus.downloadWaitingFreeNetwork,
49 ContentStoreItemStatus.downloadWaitingNetwork,
50 ].contains(status);
51 }
52
53 @override
54 Widget build(BuildContext context) {
55 return Slidable(
56 enabled: _isDownloaded,
57 endActionPane: ActionPane(
58 motion: const ScrollMotion(),
59 extentRatio: 0.25,
60 children: [
61 SlidableAction(
62 onPressed: (context) => _deleteMap(widget.map),
63 backgroundColor: Colors.red,
64 foregroundColor: Colors.white,
65 padding: EdgeInsets.zero,
66 icon: Icons.delete,
67 )
68 ]),
69 child: ListTile(
70 onTap: _onTileTap,
71 leading: Container(
72 padding: const EdgeInsets.all(8),
73 width: 50,
74 child: Image.memory(_getMapImage(widget.map)),
75 ),
76 title: Text(
77 widget.map.name,
78 style: const TextStyle(
79 color: Colors.black, fontSize: 16, fontWeight: FontWeight.w600),
80 ),
81 subtitle: Text(
82 "${(widget.map.totalSize / (1024.0 * 1024.0)).toStringAsFixed(2)} MB",
83 style: const TextStyle(
84 color: Colors.black,
85 fontSize: 16,
86 ),
87 ),
88 trailing: SizedBox.square(
89 dimension: 50,
90 child: Builder(
91 builder: (context) {
92 if (_isDownloaded == true) {
93 return const Icon(
94 Icons.check_circle_rounded,
95 color: Colors.green,
96 );
97 } else {
98 return Stack(
99 alignment: Alignment.center,
100 children: [
101 CircularProgressIndicator(
102 value: _downloadProgress,
103 color: Colors.deepPurple[900],
104 ),
105 GestureDetector(
106 onTap: () => _onProgressTap(),
107 child: Text(
108 "${(_downloadProgress * 100).toStringAsFixed(0)}%",
109 style: TextStyle(
110 color: Colors.deepPurple[900],
111 fontWeight: FontWeight.bold),
112 ),
113 )
114 ],
115 );
116 }
117 },
118 )),
119 ),
120 );
121 }
122
123 Future<void> _onTileTap() async {
124 if (_isDownloaded) {
125 final errCode = await widget.map.makeVisibleOnMap();
126 if (!mounted) return;
127 if (errCode != GemError.success) {
128 await showDialog<void>(
129 context: context,
130 builder: (BuildContext context) {
131 return AlertDialog(
132 title: const Text('Error'),
133 content: const Text('Could not make map visible.'),
134 actions: <Widget>[
135 TextButton(
136 child: const Text('OK'),
137 onPressed: () {
138 Navigator.of(context).pop();
139 },
140 ),
141 ],
142 );
143 },
144 );
145 } else {
146 Navigator.of(context).pop();
147 }
148 } else {
149 await _downloadMap();
150 }
151 }
152
153 Uint8List _getMapImage(ContentStoreItem map) {
154 final image = map.images[0];
155 return Uint8List.fromList(image);
156 }
157
158 Future<void> _downloadMap() async {
159 final errCode = widget.map.resumeDownload((errCode, progress) async {
160 if (errCode != GemError.success) {
161 print(
162 "Download failed for item ${widget.map.id} with code $errCode");
163 }
164
165 if (progress != null) {
166 setState(() {
167 _downloadProgress = progress.toDouble();
168 });
169 }
170
171 if (_downloadProgress == 1.0) {
172 setState(() {
173 _isDownloaded = true;
174 });
175 }
176 });
177 if (errCode != GemError.success) {
178 print("Download failed for item ${widget.map.id} with code $errCode");
179 }
180 }
181
182 void _deleteMap(ContentStoreItem map) async {
183 final result = await showDialog<bool>(
184 context: context,
185 builder: (context) => AlertDialog(
186 title: const Text('Confirm Delete'),
187 content: const Text('Are you sure you want to delete this map?'),
188 actions: [
189 TextButton(
190 onPressed: () => Navigator.pop(context, false),
191 child: const Text('Cancel')),
192 TextButton(
193 onPressed: () => Navigator.pop(context, true),
194 child: const Text('Delete'))
195 ],
196 ));
197
198 if (result != true) {
199 return;
200 }
201
202 final errCode = map.deleteContent();
203 if (errCode != GemError.success) {
204 print("Delete map ${map.id} failed with code $errCode");
205 }
206 setState(() {
207 _isDownloaded = false;
208 _downloadProgress = 0.0;
209 });
210 }
211
212 void _onProgressTap() async {
213 if (_isDownloadingOrWaiting()) {
214 final errCode = widget.map.pauseDownload();
215 if (errCode != GemError.success) {
216 print(
217 "Download pause for item ${widget.map.id} failed with code $errCode");
218 return;
219 }
220 setState(() {
221 _downloadProgress = widget.map.downloadProgress.toDouble();
222 });
223 } else {
224 await _downloadMap();
225 }
226 }
227}
The MapsItem
widget represents each map item in the list, allowing users to download or delete maps and see the download progress. The widget also allows pausing and resuming downloads.