This document provides a comprehensive guide for implementing map download functionality in a Flutter map application using the Magic Lane SDK. The map download system includes browsing available maps, downloading content for offline use, managing download progress, and deleting downloaded maps.
- Overview
- Architecture
- API Token Importance
- Implementation
- Integration Points
- Testing and Validation
- Future Enhancements
The map download functionality consists of five main components:
- Map Downloads Button - A floating action button for accessing the map download interface
- Map Download Page - Main page containing available and downloaded maps tabs
- Map Download Controllers - Business logic for page state and individual download management
- Map Download Item UI - Individual map item display with download controls
- Content Store Integration - Integration with Magic Lane Content Store for map data
lib/map_downloads/
├── map_downloads_button.dart # FAB to open downloads page
├── map_download_page.dart # Main download page with tabs
├── map_download_item.dart # Individual map item widget
├── widgets/
│ ├── map_download_page_header.dart # Tab bar and error display
│ └── map_download_page_content.dart # Content for available/downloaded tabs
└── controllers/
├── map_download_page_controller.dart # Page-level state management
└── map_download_item_controller.dart # Individual item download logic
lib/utils/
└── utils.dart # Helper functions for downloads
The map download system integrates with:
- Magic Lane ContentStore - For retrieving available and local content
- Error Service - For user feedback and error handling
- Utils Service - For formatting and download restart helpers
- Localization Service - For internationalized strings
Important
To use the Magic Lane Maps SDK for Flutter, it is essential to provide a valid API token (sometimes called an app authorization token). The API token authenticates your application with Magic Lane services and enables access to map data, content downloads, and other SDK features. Without a valid token, the map download functionality will have a bigger chance of failing due to server priorities. The token should be securely managed and never exposed in public repositories. More about obtaining and managing API tokens can be found in the Get Started guide documentation.
The map downloads button provides easy access to the download management interface from the main map view.
import 'package:flutter/material.dart';
import '../map_downloads/map_download_page.dart';
import '../l10n/app_localizations.dart';
class MapDownloadsButton extends StatelessWidget {
const MapDownloadsButton({super.key});
@override
Widget build(BuildContext context) {
return FloatingActionButton(
heroTag: 'mapDownloads',
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
tooltip: AppLocalizations.of(context)!.downloadMaps,
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const MapDownloadPage()));
},
child: const Icon(Icons.map_outlined),
);
}
}Key Features:
- Hero Tag - Unique identifier to prevent conflicts with other FABs
- Navigation - Pushes to MapDownloadPage using MaterialPageRoute
- Localization - Uses localized tooltip text
- Consistent Styling - Matches app theme with deep purple background
Integration in MapOverlayManager:
The MapDownloadsButton is integrated into the main map UI via the _BottomControlsWidget in lib/widgets/map_overlay_manager.dart. This widget displays key controls at the bottom of the map when the user is not navigating, routing, selecting a landmark, or presenting a recorded route. The button is strategically placed at the top of the bottom controls column for quick access:
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [
const MapDownloadsButton(), // Integrated here for quick access
const SizedBox(height: 10),
Row(
children: [
const TransportModeButton(),
const SizedBox(width: 10),
const Expanded(child: SearchBarWidget()),
const SizedBox(width: 10),
const FollowPositionButton(),
],
),
],
),The main page for managing map downloads, featuring tabbed interface for available and downloaded content.
import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart';
import 'controllers/map_download_page_controller.dart';
import 'widgets/map_download_page_header.dart';
import 'widgets/map_download_page_content.dart';
// Page for managing map downloads
class MapDownloadPage extends StatefulWidget {
const MapDownloadPage({super.key});
@override
State<MapDownloadPage> createState() => _MapDownloadPageState();
}
class _MapDownloadPageState extends State<MapDownloadPage> {
late MapDownloadPageController _controller;
@override
void initState() {
super.initState();
_controller = MapDownloadPageController();
_initializeController();
}
Future<void> _initializeController() async {
await _controller.initialize(() {
if (mounted) setState(() {});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.mapDownloads),
backgroundColor: Colors.deepPurple[700],
foregroundColor: Colors.white,
elevation: 1,
leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.of(context).pop()),
),
body: DefaultTabController(
length: 2,
child: Column(
children: [
MapDownloadPageContent(
controller: _controller,
onStateChanged: () {
if (mounted) setState(() {});
},
),
SafeArea(child: MapDownloadPageTabs(controller: _controller)),
],
),
),
);
}
}Key Features:
- Controller Integration - Uses MapDownloadPageController for state management
- Tab Interface - DefaultTabController with 2 tabs for available/downloaded maps
- Safe Area - Accounts for device-specific UI elements
- State Management - Rebuilds UI when controller state changes
Handles the display of available and downloaded maps based on the selected tab.
import 'package:flutter/material.dart';
import 'package:magiclane_maps_flutter/magiclane_maps_flutter.dart';
import '../controllers/map_download_page_controller.dart';
import '../map_download_item.dart';
import '../../l10n/app_localizations.dart';
import '../../shared/widgets/loading_indicator.dart';
// Widget for the Map Download Page content, displaying available and downloaded maps.
class MapDownloadPageContent extends StatelessWidget {
final MapDownloadPageController controller;
final VoidCallback onStateChanged;
const MapDownloadPageContent({super.key, required this.controller, required this.onStateChanged});
@override
Widget build(BuildContext context) {
if (controller.isLoading && !controller.isInitialized && controller.availableMaps.isEmpty) {
return Expanded(child: LoadingIndicator.withMessage(message: AppLocalizations.of(context)!.loadingMaps));
}
return Expanded(child: TabBarView(children: [_buildAvailableMapsTab(context), _buildDownloadedMapsTab(context)]));
}
Widget _buildAvailableMapsTab(BuildContext context) {
return _buildMapList(
context: context,
maps: controller.availableMaps,
isDownloadedCheck: (mapItem) => controller.isMapDownloaded(mapItem),
emptyStateWidget: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.cloud_download, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.noMapsAvailable,
style: const TextStyle(fontSize: 18, color: Colors.grey),
),
const SizedBox(height: 8),
Text(AppLocalizations.of(context)!.checkInternetConnection, style: const TextStyle(color: Colors.grey)),
],
),
),
);
}
Widget _buildDownloadedMapsTab(BuildContext context) {
return _buildMapList(
context: context,
maps: controller.localMaps,
isDownloadedCheck: (_) => true,
verticalPadding: 4.0,
emptyStateWidget: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.offline_pin, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.noDownloadedMaps,
style: const TextStyle(fontSize: 18, color: Colors.grey),
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context)!.downloadMapsFromAvailableTab,
style: const TextStyle(color: Colors.grey),
),
],
),
),
);
}
/// Creates a reusable map list with refresh capability
Widget _buildMapList({
required BuildContext context,
required List<ContentStoreItem> maps,
required bool Function(ContentStoreItem) isDownloadedCheck,
required Widget emptyStateWidget,
double verticalPadding = 2.0,
}) {
if (maps.isEmpty) {
return emptyStateWidget;
}
return RefreshIndicator(
onRefresh: controller.refreshMaps,
child: ListView.separated(
padding: const EdgeInsets.all(8),
itemCount: maps.length,
separatorBuilder: (context, index) => Padding(padding: EdgeInsets.symmetric(vertical: verticalPadding)),
itemBuilder: (context, index) {
final mapItem = maps[index];
final isDownloaded = isDownloadedCheck(mapItem);
return MapDownloadItem(
mapItem: mapItem,
isDownloaded: isDownloaded,
onStatusChanged: () {
controller.onMapStatusChanged();
onStateChanged();
},
);
},
),
);
}
}Manages the tab bar and error display for the map download page (lib/map_downloads/widgets/map_download_page_header.dart).
class MapDownloadPageTabs extends StatelessWidget {
final MapDownloadPageController controller;
const MapDownloadPageTabs({super.key, required this.controller});
@override
Widget build(BuildContext context) {
if (controller.error != null) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: Colors.red.shade50,
child: Row(
children: [
Icon(Icons.error, color: Colors.red.shade700),
const SizedBox(width: 8),
Expanded(
child: Text(controller.error!, style: TextStyle(color: Colors.red.shade700)),
),
],
),
);
}
return TabBar(
labelColor: Colors.deepPurple[700],
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.deepPurple[700],
dividerHeight: 0.0,
tabs: [
Tab(
icon: const Icon(Icons.download),
text: '${AppLocalizations.of(context)!.available} (${controller.availableMaps.length})',
),
Tab(
icon: const Icon(Icons.offline_pin),
text: '${AppLocalizations.of(context)!.downloaded} (${controller.localMaps.length})',
),
],
);
}
}Manages the overall state of the download page, including loading available maps and tracking local content.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:magiclane_maps_flutter/magiclane_maps_flutter.dart';
// Controller for managing map downloads and state.
class MapDownloadPageController {
List<ContentStoreItem> _availableMaps = [];
List<ContentStoreItem> _localMaps = [];
bool _isLoading = false;
bool _isInitialized = false;
String? _error;
VoidCallback? _stateChangeCallback;
// Getters
List<ContentStoreItem> get availableMaps => _availableMaps;
List<ContentStoreItem> get localMaps => _localMaps;
bool get isLoading => _isLoading;
bool get isInitialized => _isInitialized;
String? get error => _error;
Future<void> initialize(VoidCallback onStateChanged) async {
_stateChangeCallback = onStateChanged;
_setLoading(true);
_setError(null);
// Allow downloads on cellular networks for content service
SdkSettings.setAllowOffboardServiceOnExtraChargedNetwork(ServiceGroupType.contentService, true);
// Load local maps first
_loadLocalMaps();
// Then load available maps from store
await _loadAvailableOnlineMaps();
_isInitialized = true;
_notifyStateChanged();
}
void _loadLocalMaps() {
final localMaps = ContentStore.getLocalContentList(ContentType.roadMap);
_localMaps = localMaps;
_notifyStateChanged();
}
Future<void> _loadAvailableOnlineMaps() async {
final completer = Completer<List<ContentStoreItem>?>();
// Get available maps from the online store
ContentStore.asyncGetStoreContentList(ContentType.roadMap, (err, items, isCached) {
if (err == GemError.success) {
completer.complete(items);
} else {
// Failed to load online maps
completer.complete(null);
}
});
final maps = await completer.future;
_availableMaps = maps ?? [];
_notifyStateChanged();
}
Future<void> refreshMaps() async {
_loadLocalMaps();
await _loadAvailableOnlineMaps();
}
void onMapStatusChanged() {
// Refresh the maps list when a download completes or is deleted
_loadLocalMaps();
}
bool isMapDownloaded(ContentStoreItem mapItem) {
return _localMaps.any((local) => local.id == mapItem.id);
}
void _setLoading(bool loading) {
_isLoading = loading;
_notifyStateChanged();
}
void _setError(String? error) {
_error = error;
_notifyStateChanged();
}
void _notifyStateChanged() {
_stateChangeCallback?.call();
}
}Key Features:
- Content Store Integration - Uses Magic Lane SDK to fetch local and online maps
- Cellular Network Support - Enables downloads on charged networks
- State Management - Tracks loading, error, and initialization states
- Refresh Capability - Supports pull-to-refresh functionality
Handles the download lifecycle for individual map items, including progress tracking and state management.
import 'package:flutter/foundation.dart';
import 'package:magiclane_maps_flutter/magiclane_maps_flutter.dart';
/// Controller that encapsulates the business logic for a single map download item.
class MapDownloadItemController extends ChangeNotifier {
ContentStoreItem mapItem;
final VoidCallback? onStatusChanged;
bool isDownloading = false;
bool isPaused = false;
int downloadProgress = 0;
bool _isDisposed = false;
MapDownloadItemController({required this.mapItem, this.onStatusChanged}) {
// Initialize local state from the provided item
updateStateFromItem();
}
void init() {
// Resume download if needed and subscribe to progress updates
Utils.restartDownloadIfNecessary(
mapItem,
(err) {},
onProgress: (progress) {
downloadProgress = progress;
if (!_isDisposed) notifyListeners();
},
);
updateStateFromItem();
}
void updateMapItem(ContentStoreItem newItem) {
mapItem = newItem;
updateStateFromItem();
}
void updateStateFromItem() {
downloadProgress = mapItem.downloadProgress;
isDownloading = Utils.getIsDownloadingOrWaiting(mapItem);
isPaused = mapItem.status == ContentStoreItemStatus.paused;
if (!_isDisposed) notifyListeners();
}
void startDownload({bool allowChargedNetworks = true}) {
isDownloading = true;
isPaused = false;
if (!_isDisposed) notifyListeners();
mapItem.asyncDownload(
(GemError err) {
if (err == GemError.success) {
isDownloading = false;
downloadProgress = 100;
ContentStore.refresh();
if (!_isDisposed) notifyListeners();
if (!_isDisposed) onStatusChanged?.call();
} else {
isDownloading = false;
if (!_isDisposed) notifyListeners();
if (err != GemError.suspended) {
// Higher-level UI should present errors; still log via Utils if available
}
}
},
onProgress: (int progress) {
downloadProgress = progress;
if (!_isDisposed) notifyListeners();
},
allowChargedNetworks: allowChargedNetworks,
);
}
GemError pauseDownload() {
final result = mapItem.pauseDownload();
if (result == GemError.success) {
isPaused = true;
isDownloading = false;
if (!_isDisposed) notifyListeners();
}
return result;
}
void resumeDownload() {
// Simply start download again which triggers the async download flow
startDownload();
}
GemError cancelDownload() {
final result = mapItem.cancelDownload();
if (result == GemError.success) {
isDownloading = false;
isPaused = false;
downloadProgress = 0;
if (!_isDisposed) notifyListeners();
if (!_isDisposed) onStatusChanged?.call();
}
return result;
}
GemError performDelete() {
final result = mapItem.deleteContent();
if (result == GemError.success) {
if (!_isDisposed) onStatusChanged?.call();
}
return result;
}
@override
void dispose() {
_isDisposed = true;
super.dispose();
}
}Key Features:
- Download Lifecycle Management - Handles start, pause, resume, cancel, and delete operations
- Progress Tracking - Real-time progress updates through callbacks
- State Synchronization - Maintains consistency between UI and SDK state
- Memory Safety - Proper disposal and disposed state checking
Individual map item display with contextual actions based on download state.
import 'package:flutter/material.dart';
import 'package:magiclane_maps_flutter/magiclane_maps_flutter.dart';
import '../services/error_service.dart';
import '../l10n/app_localizations.dart';
import '../shared/widgets/loading_indicator.dart';
import 'controllers/map_download_item_controller.dart';
// Widget representing a single map download item with its status and actions.
class MapDownloadItem extends StatefulWidget {
final ContentStoreItem mapItem;
final bool isDownloaded;
final VoidCallback onStatusChanged;
const MapDownloadItem({super.key, required this.mapItem, required this.isDownloaded, required this.onStatusChanged});
@override
State<MapDownloadItem> createState() => _MapDownloadItemState();
}
class _MapDownloadItemState extends State<MapDownloadItem> {
late final MapDownloadItemController _controller;
late ContentStoreItem _mapItem;
@override
void initState() {
super.initState();
_mapItem = widget.mapItem;
_controller = MapDownloadItemController(mapItem: _mapItem, onStatusChanged: widget.onStatusChanged);
_controller.init();
_controller.addListener(_onControllerChanged);
}
@override
void didUpdateWidget(MapDownloadItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.mapItem.id != widget.mapItem.id) {
_mapItem = widget.mapItem;
_controller.updateMapItem(_mapItem);
}
}
@override
void dispose() {
_controller.removeListener(_onControllerChanged);
_controller.dispose();
super.dispose();
}
void _onControllerChanged() {
if (mounted) setState(() {});
}
// Builds the trailing widget based on the current download status (if it has been downloaded).
Widget _buildTrailingWidget() {
if (widget.isDownloaded && _mapItem.isCompleted) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check_circle, color: Colors.green, size: 20),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
// Confirm delete
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteMap),
content: Text(AppLocalizations.of(context)!.deleteMapConfirmation(_mapItem.name)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
final result = _controller.performDelete();
if (result == GemError.success) {
ErrorService.showSuccess('${_mapItem.name} deleted successfully');
} else {
ErrorService.handleGemError(result, context: 'Map deletion');
}
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text(AppLocalizations.of(context)!.delete),
),
],
);
},
);
},
tooltip: AppLocalizations.of(context)!.delete,
),
],
);
}
if (_controller.isDownloading) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
LoadingIndicator.progress(
progress: _controller.downloadProgress / 100.0,
size: 24,
strokeWidth: 2,
backgroundColor: Colors.grey[300],
color: Colors.deepPurple[700],
),
const SizedBox(width: 8),
Text('${_controller.downloadProgress}%', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.pause),
onPressed: () => _controller.pauseDownload(),
tooltip: AppLocalizations.of(context)!.pauseTooltip,
),
],
);
}
if (_controller.isPaused) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.pause_circle, color: Colors.orange),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () => _controller.resumeDownload(),
tooltip: AppLocalizations.of(context)!.resumeTooltip,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => _controller.cancelDownload(),
tooltip: AppLocalizations.of(context)!.cancelTooltip,
),
],
);
}
// Not downloaded, show download button
return IconButton(
icon: const Icon(Icons.download),
onPressed: () => _controller.startDownload(),
tooltip: AppLocalizations.of(context)!.downloadTooltip,
);
}
@override
Widget build(BuildContext context) {
final countries = _mapItem.countryCodes.isNotEmpty
? _mapItem.countryCodes.join(', ')
: AppLocalizations.of(context)!.unknown;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.deepPurple[100],
child: Utils.getCountryFlagImage(_mapItem) != null
? Image.memory(Utils.getCountryFlagImage(_mapItem)!, gaplessPlayback: true)
: Icon(Icons.map, color: Colors.deepPurple[700]),
),
title: Text(
_mapItem.name,
style: const TextStyle(fontWeight: FontWeight.w600),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.sizeLabel(Utils.formatFileSize(_mapItem.totalSize)),
style: const TextStyle(fontSize: 13),
),
if (countries != AppLocalizations.of(context)!.unknown)
Text(
AppLocalizations.of(context)!.countriesLabel(countries),
style: const TextStyle(fontSize: 12, color: Colors.grey),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (_mapItem.isUpdatable)
Container(
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(color: Colors.orange[100], borderRadius: BorderRadius.circular(12)),
child: Text(
AppLocalizations.of(context)!.updateAvailable,
style: TextStyle(fontSize: 10, color: Colors.orange[800], fontWeight: FontWeight.bold),
),
),
],
),
trailing: _buildTrailingWidget(),
onTap: widget.isDownloaded ? null : () => _controller.startDownload(),
),
);
}
}The Utils class provides helper functions for formatting file sizes, managing downloads, and retrieving country flag images (/lib/utils/utils.dart).
class Utils {
Utils._();
static bool getIsDownloadingOrWaiting(ContentStoreItem contentItem) => [
ContentStoreItemStatus.downloadQueued,
ContentStoreItemStatus.downloadRunning,
ContentStoreItemStatus.downloadWaitingNetwork,
ContentStoreItemStatus.downloadWaitingFreeNetwork,
ContentStoreItemStatus.downloadWaitingNetwork,
].contains(contentItem.status);
// Method that returns the image of the country associated with the road map item
static Uint8List? getCountryFlagImage(ContentStoreItem contentItem) {
Img? img = MapDetails.getCountryFlagImg(contentItem.countryCodes[0]);
if (img == null) return null;
if (!img.isValid) return null;
return img.getRenderableImageBytes(size: Size(100, 100));
}
static void restartDownloadIfNecessary(
ContentStoreItem contentItem,
void Function(GemError err) onCompleteCallback, {
void Function(int progress)? onProgress,
}) {
//If the map is downloading pause and start downloading again
//so the progress indicator updates value from callback
if (getIsDownloadingOrWaiting(contentItem)) {
_pauseAndRestartDownload(contentItem, onCompleteCallback, onProgress: onProgress);
}
}
static void _pauseAndRestartDownload(
ContentStoreItem contentItem,
void Function(GemError err) onCompleteCallback, {
void Function(int progress)? onProgress,
}) {
final errCode = contentItem.pauseDownload(
onComplete: (err) {
if (err == GemError.success) {
// Download the map.
contentItem.asyncDownload(onCompleteCallback, onProgress: onProgress, allowChargedNetworks: true);
} else {
print("Download pause for item ${contentItem.id} failed with code $err");
}
},
);
if (errCode != GemError.success) {
print("Download pause for item ${contentItem.id} failed with code $errCode");
}
}
static String formatFileSize(int sizeInBytes) {
if (sizeInBytes < 1024) {
return '$sizeInBytes B';
} else if (sizeInBytes < 1024 * 1024) {
return '${(sizeInBytes / 1024).toStringAsFixed(1)} KB';
} else if (sizeInBytes < 1024 * 1024 * 1024) {
return '${(sizeInBytes / (1024 * 1024)).toStringAsFixed(1)} MB';
} else {
return '${(sizeInBytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
}
}Key Features:
- State-Based UI - Different trailing widgets based on download state
- Progress Visualization - Circular progress indicator with percentage
- Confirmation Dialogs - User confirmation for destructive actions
- Country Flags - Visual representation using country flag images
- Update Indicators - Shows when map updates are available
The map download system integrates with the Magic Lane Content Store:
// Get local content synchronously
final localMaps = ContentStore.getLocalContentList(ContentType.roadMap);
// Get online content asynchronously
ContentStore.asyncGetStoreContentList(ContentType.roadMap, (err, items, isCached) {
if (err == GemError.success) {
// Handle successful response
}
});
// Configure network settings
SdkSettings.setAllowOffboardServiceOnExtraChargedNetwork(ServiceGroupType.contentService, true);Individual content items support full download lifecycle management:
// Start download
mapItem.asyncDownload(
(GemError err) { /* completion callback */ },
onProgress: (int progress) { /* progress callback */ },
allowChargedNetworks: true,
);
// Control download
final pauseResult = mapItem.pauseDownload();
final cancelResult = mapItem.cancelDownload();
final deleteResult = mapItem.deleteContent();-
Page Navigation
- Verify FAB opens download page
- Confirm tabs switch between available and downloaded content
-
Content Loading
- Test initial load of available maps
- Verify local maps display correctly
- Test pull-to-refresh functionality
-
Download Workflow
- Start download and verify progress updates
- Test pause/resume functionality
- Verify cancel operation resets state
- Confirm successful completion moves item to downloaded tab
-
Error Handling
- Test network connectivity issues
- Verify error messages display appropriately
- Test recovery from failed downloads
-
Delete Operations
- Confirm deletion requires user confirmation
- Verify successful deletion removes item from downloaded tab
- Network Interruption - Downloads should handle network changes gracefully
- App Backgrounding - Download state should persist across app lifecycle
- Storage Limitations - Consider device storage constraints
- Concurrent Downloads - Multiple simultaneous downloads should work correctly
- Download Scheduling - Queue downloads for optimal timing
- Storage Management - Automatic cleanup of old or unused maps
- Bandwidth Control - User-configurable download speed limits
- Offline Sync - Synchronize downloads across devices
