import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:provider/provider.dart'; /// Data model for the user's mission and simulated location. class MissionData extends ChangeNotifier { final double _totalDistanceKm; double _currentDistanceKm; final double _userLatitude; final double _userLongitude; /// Initializes the mission with a total distance and a simulated user location. /// /// The initial current distance walked is 0.0. MissionData({ required double totalDistanceKm, required double userLatitude, required double userLongitude, double initialDistanceKm = 0.0, }) : _totalDistanceKm = totalDistanceKm, _userLatitude = userLatitude, _userLongitude = userLongitude, _currentDistanceKm = initialDistanceKm { if (_totalDistanceKm <= 0) { throw ArgumentError('Total distance must be positive.'); } if (_currentDistanceKm < 0) { throw ArgumentError('Initial distance cannot be negative.'); } if (_currentDistanceKm > _totalDistanceKm) { _currentDistanceKm = _totalDistanceKm; // Cap initial distance at total } } /// The total distance required for the mission in kilometers. double get totalDistanceKm => _totalDistanceKm; /// The current distance walked by the user in kilometers. double get currentDistanceKm => _currentDistanceKm; /// The simulated geographical latitude of the user. double get userLatitude => _userLatitude; /// The simulated geographical longitude of the user. double get userLongitude => _userLongitude; /// The remaining distance to complete the mission in kilometers. double get remainingDistanceKm => (_totalDistanceKm - _currentDistanceKm).clamp(0.0, _totalDistanceKm); /// The progress of the mission as a percentage (0.0 to 1.0). double get progressPercentage => _currentDistanceKm / _totalDistanceKm.clamp(1.0, double.infinity); /// Adds a specified [distance] in kilometers to the current distance walked. /// /// The distance added must be positive. The current distance will not exceed /// the total mission distance. void addDistance(double distance) { if (distance <= 0) { throw ArgumentError('Distance to add must be positive.'); } _currentDistanceKm = (_currentDistanceKm + distance).clamp(0.0, _totalDistanceKm); notifyListeners(); } } /// The main application widget for displaying location and mission. class LocationMissionApp extends StatelessWidget { const LocationMissionApp({super.key}); @override Widget build(BuildContext context) { return ChangeNotifierProvider<MissionData>( create: (context) => MissionData( totalDistanceKm: 100.0, // Mission: walk 100 kilometers userLatitude: 51.5, // Simulated London latitude userLongitude: -0.09, // Simulated London longitude ), builder: (context, child) { return MaterialApp( title: 'Mission Tracker', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const MissionScreen(), ); }, ); } } /// Displays the map with simulated user location and mission progress. class MissionScreen extends StatefulWidget { const MissionScreen({super.key}); @override State<MissionScreen> createState() => _MissionScreenState(); } class _MissionScreenState extends State<MissionScreen> { final TextEditingController _distanceInputController = TextEditingController(); final _formKey = GlobalKey<FormState>(); @override void dispose() { _distanceInputController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Mission Progress'), ), body: Consumer<MissionData>( builder: (context, missionData, child) { return Column( children: <Widget>[ Expanded( flex: 2, child: Card( margin: const EdgeInsets.all(8.0), clipBehavior: Clip.antiAlias, child: Stack( children: [ FlutterMap( options: MapOptions( initialCenter: LatLng(missionData.userLatitude, missionData.userLongitude), initialZoom: 13.0, interactionOptions: const InteractionOptions( flags: InteractiveFlag.all & ~InteractiveFlag.rotate, ), ), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'com.example.app', ), MarkerLayer( markers: [ Marker( point: LatLng(missionData.userLatitude, missionData.userLongitude), width: 80, height: 80, child: const Icon( Icons.location_on, color: Colors.red, size: 40.0, ), ), ], ), ], ), Positioned( top: 8, left: 8, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(4), ), child: const Text( 'Simulated Location', style: TextStyle(color: Colors.white, fontSize: 12), ), ), ), ], ), ), ), Expanded( flex: 1, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ Text( 'Mission: Walk ${missionData.totalDistanceKm.toStringAsFixed(0)} km', style: Theme.of(context).textTheme.headlineSmall, textAlign: TextAlign.center, ), const SizedBox(height: 8.0), Text( 'Progress: ${missionData.currentDistanceKm.toStringAsFixed(1)} km of ${missionData.totalDistanceKm.toStringAsFixed(0)} km', style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.center, ), const SizedBox(height: 4.0), LinearProgressIndicator( value: missionData.progressPercentage, minHeight: 10, backgroundColor: Colors.grey[300], valueColor: const AlwaysStoppedAnimation<Color>(Colors.green), ), const SizedBox(height: 8.0), Text( 'Remaining: ${missionData.remainingDistanceKm.toStringAsFixed(1)} km', style: Theme.of(context).textTheme.titleSmall, textAlign: TextAlign.center, ), const Spacer(), Form( key: _formKey, child: Row( children: [ Expanded( child: TextFormField( controller: _distanceInputController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( labelText: 'Add distance (km)', border: const OutlineInputBorder(), suffixText: 'km', errorStyle: TextStyle( color: Theme.of(context).colorScheme.error, fontSize: 10), ), validator: (String? value) { if (value == null || value.isEmpty) { return 'Please enter a distance'; } final double? distance = double.tryParse(value); if (distance == null || distance <= 0) { return 'Enter a positive number'; } return null; }, ), ), const SizedBox(width: 8.0), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { final double distanceToAdd = double.parse(_distanceInputController.text); missionData.addDistance(distanceToAdd); _distanceInputController.clear(); FocusScope.of(context).unfocus(); // Dismiss keyboard } }, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 16), ), child: const Text('Add Walk'), ), ], ), ), ], ), ), ), ], ); }, ), ); } } void main() { runApp(const LocationMissionApp()); }
Preview:
downloadDownload PNG
downloadDownload JPEG
downloadDownload SVG
Tip: You can change the style, width & colours of the snippet with the inspect tool before clicking Download!
Click to optimize width for Twitter