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:
:lineno-start: 1
allprojects {
repositories {
google()
mavenCentral()
maven {
url "${rootDir}/../plugins/gem_kit/android/build"
}
}
}
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
:lineno-start: 1
android {
defaultConfig {
applicationId "com.magiclane.gem_kit.examples.example_pathname"
minSdk 21
targetSdk flutter.targetSdk
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
minifyEnabled false
shrinkResources false
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
Then run the project:
flutter run --debug
orflutter run --release
App entry and initialization¶
const projectApiToken = String.fromEnvironment('GEM_TOKEN');
void main() {
runApp(const MyApp());
}
This code initializes the projectApiToken with the required authorization token and launches the app.
How it works¶
Map Display and UI Components¶
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void dispose() {
GemKit.release();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple[900],
title: const Text(
'Map Download',
style: TextStyle(color: Colors.white),
),
actions: [
IconButton(
onPressed: () => _onMapButtonTap(context),
icon: const Icon(
Icons.map_outlined,
color: Colors.white,
),
),
],
),
body: GemMap(onMapCreated: _onMapCreated, appAuthorization: projectApiToken),
);
}
void _onMapCreated(GemMapController controller) async {
SdkSettings.setAllowOffboardServiceOnExtraChargedNetwork(
ServiceGroupType.contentService, true);
}
void _onMapButtonTap(BuildContext context) async {
Navigator.of(context).push(MaterialPageRoute<dynamic>(
builder: (context) => const MapsPage(),
));
}
}
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¶
import 'package:gem_kit/content_store.dart';
import 'package:gem_kit/core.dart';
import 'maps_item.dart';
import 'package:flutter/material.dart';
import 'dart:async';
class MapsPage extends StatefulWidget {
const MapsPage({super.key});
@override
State<MapsPage> createState() => _MapsPageState();
}
class _MapsPageState extends State<MapsPage> {
List<ContentStoreItem> mapsList = [];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: true,
foregroundColor: Colors.white,
title: const Text(
"Maps List",
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.deepPurple[900],
),
body: FutureBuilder<List<ContentStoreItem>>(
future: _getMaps(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Scrollbar(
child: ListView.separated(
padding: EdgeInsets.zero,
itemCount: snapshot.data!.length,
separatorBuilder: (context, index) => const Divider(
indent: 50,
height: 0,
),
itemBuilder: (context, index) {
final map = snapshot.data!.elementAt(index);
return MapsItem(map: map);
},
),
);
}),
);
}
Future<List<ContentStoreItem>> _getMaps() async {
Completer<List<ContentStoreItem>> mapsList =
Completer<List<ContentStoreItem>>();
ContentStore.asyncGetStoreContentList(ContentType.roadMap,
(err, items, isCached) {
if (err == GemError.success && items != null) {
mapsList.complete(items);
}
});
return mapsList.future;
}
}
The MapsPage
widget fetches the list of maps available for download and displays them in a scrollable list.
Map Item¶
import 'dart:typed_data';
import 'package:gem_kit/content_store.dart';
import 'package:gem_kit/core.dart';
import 'package:gem_kit/map.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter/material.dart';
import 'dart:async';
class MapsItem extends StatefulWidget {
final ContentStoreItem map;
const MapsItem({super.key, required this.map});
@override
State<MapsItem> createState() => _MapsItemState();
}
class _MapsItemState extends State<MapsItem> {
late bool _isDownloaded;
double _downloadProgress = 0;
@override
void initState() {
super.initState();
_isDownloaded = widget.map.isCompleted;
_downloadProgress = widget.map.downloadProgress.toDouble();
if (_isDownloadingOrWaiting()) {
final errCode = widget.map.pauseDownload();
if (errCode != GemError.success) {
print(
"Download pause for item ${widget.map.id} failed with code $errCode");
return;
}
Future<dynamic>.delayed(const Duration(milliseconds: 500))
.then((value) => _downloadMap());
}
}
bool _isDownloadingOrWaiting() {
final status = widget.map.status;
return [
ContentStoreItemStatus.downloadQueued,
ContentStoreItemStatus.downloadRunning,
ContentStoreItemStatus.downloadWaiting,
ContentStoreItemStatus.downloadWaitingFreeNetwork,
ContentStoreItemStatus.downloadWaitingNetwork,
].contains(status);
}
@override
Widget build(BuildContext context) {
return Slidable(
enabled: _isDownloaded,
endActionPane: ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.25,
children: [
SlidableAction(
onPressed: (context) => _deleteMap(widget.map),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: EdgeInsets.zero,
icon: Icons.delete,
)
]),
child: ListTile(
onTap: _onTileTap,
leading: Container(
padding: const EdgeInsets.all(8),
width: 50,
child: Image.memory(_getMapImage(widget.map)),
),
title: Text(
widget.map.name,
style: const TextStyle(
color: Colors.black, fontSize: 16, fontWeight: FontWeight.w600),
),
subtitle: Text(
"${(widget.map.totalSize / (1024.0 * 1024.0)).toStringAsFixed(2)} MB",
style: const TextStyle(
color: Colors.black,
fontSize: 16,
),
),
trailing: SizedBox.square(
dimension: 50,
child: Builder(
builder: (context) {
if (_isDownloaded == true) {
return const Icon(
Icons.check_circle_rounded,
color: Colors.green,
);
} else {
return Stack(
alignment: Alignment.center,
children: [
CircularProgressIndicator(
value: _downloadProgress,
color: Colors.deepPurple[900],
),
GestureDetector(
onTap: () => _onProgressTap(),
child: Text(
"${(_downloadProgress * 100).toStringAsFixed(0)}%",
style: TextStyle(
color: Colors.deepPurple[900],
fontWeight: FontWeight.bold),
),
)
],
);
}
},
)),
),
);
}
Future<void> _onTileTap() async {
if (_isDownloaded) {
final errCode = await widget.map.makeVisibleOnMap();
if (!mounted) return;
if (errCode != GemError.success) {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Error'),
content: const Text('Could not make map visible.'),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
} else {
Navigator.of(context).pop();
}
} else {
await _downloadMap();
}
}
Uint8List _getMapImage(ContentStoreItem map) {
final image = map.images[0];
return Uint8List.fromList(image);
}
Future<void> _downloadMap() async {
final errCode = widget.map.resumeDownload((errCode, progress) async {
if (errCode != GemError.success) {
print(
"Download failed for item ${widget.map.id} with code $errCode");
}
if (progress != null) {
setState(() {
_downloadProgress = progress.toDouble();
});
}
if (_downloadProgress == 1.0) {
setState(() {
_isDownloaded = true;
});
}
});
if (errCode != GemError.success) {
print("Download failed for item ${widget.map.id} with code $errCode");
}
}
void _deleteMap(ContentStoreItem map) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirm Delete'),
content: const Text('Are you sure you want to delete this map?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete'))
],
));
if (result != true) {
return;
}
final errCode = map.deleteContent();
if (errCode != GemError.success) {
print("Delete map ${map.id} failed with code $errCode");
}
setState(() {
_isDownloaded = false;
_downloadProgress = 0.0;
});
}
void _onProgressTap() async {
if (_isDownloadingOrWaiting()) {
final errCode = widget.map.pauseDownload();
if (errCode != GemError.success) {
print(
"Download pause for item ${widget.map.id} failed with code $errCode");
return;
}
setState(() {
_downloadProgress = widget.map.downloadProgress.toDouble();
});
} else {
await _downloadMap();
}
}
}
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.