Maps is a open-source, clean-architecture Flutter-based app designed to research nearby places by a specific category.
- Interactive 3D map screen built with Mapbox.
- Automatic current-location detection on app start.
- Category-based place search (Restaurants, Cafés, Hotels, Petrol Stations, ATMs, GYMs, Pharmacies).
- Yelp API integration to fetch nearby businesses by selected category.
- Overpass / OpenStreetMap integration to fetch nearby building polygons for those businesses.
- Building highlighting on the map for matched results, plus tap-to-select behavior.
- Business details bottom sheet on tap (name, image, address, rating, phone, category).
- Category picker bottom sheet with persisted selection (HydratedBloc keeps selected category across launches).
maps/
├── android/ # Android platform files
├── ios/ # iOS platform files
├── lib/
│ ├── backbone/ # DI
│ ├── data/ # Data layer
│ ├── domain/ # Business logic
│ └── presentation/ # UI layer
├── environment/.env # Environment variables
├── pubspec.yaml # Dependencies
└── README.md # Introduction
- Responsibility: Handles all UI and user interaction logic.
- Components:
- Widgets/Pages: Flutter widgets that form the screens of the app. They are responsible for rendering the UI based on the current state.
- BLoC (Business Logic Component): Manages the state of a feature. It listens to events from the UI (e.g., button clicks) and emits new states in response. It communicates with the Domain layer via Use Cases.
- Responsibility: Contains the core business logic of the application. This layer is completely independent of the UI and data sources.
- Components:
- Entities: Business objects that represent the core data structures of the app (e.g.,
BusinessResponse). - Gateways (Abstract): Defines the contracts (interfaces) for the Data layer to implement. This decouples the domain logic from the specific data source implementations.
- Use Cases: Encapsulates a single, specific business rule. They orchestrate the flow of data between the Presentation and Data layers by using repository contracts.
- Entities: Business objects that represent the core data structures of the app (e.g.,
- Responsibility: Responsible for retrieving data from various sources (e.g., remote API, local database).
- Components:
- Models: Data Transfer Objects (DTOs) that are specific to a data source (e.g.,
BusinessResponseDtofor the Yelp API). They include logic for serialization/deserialization (fromJson/toJson). - Gateways (Implementation): Implements the repository contracts defined in the Domain layer.
- Models: Data Transfer Objects (DTOs) that are specific to a data source (e.g.,
When a building is pressable, its fill color changes to yellow to indicate that it can be selected.
Future<void> _highlightBusinessesOnMap({
required List<Map<String, dynamic>> features,
}) async {
if (_mapboxMap == null || !_styleLoaded) return;
// clear previous highlights
await _clearAllHighlights();
// add new highlights
for (final feature in features) {
final props = feature['properties'] as Map<String, dynamic>;
final id = props['id'];
await _setFeatureState(id: id.toString(), shouldHighlight: true);
_highlightedFeatureIds.add(id.toString());
}
}
Future<void> _clearAllHighlights() async {
if (_mapboxMap == null || !_styleLoaded) return;
try {
if (_lastFeatureId != null) {
await _mapboxMap!.setFeatureState(
AppConstants.sourceId,
AppConstants.sourceLayerId,
_lastFeatureId!,
jsonEncode({'selected': false}),
);
_lastFeatureId = null;
}
if (_highlightedFeatureIds.isNotEmpty) {
for (final id in _highlightedFeatureIds) {
await _setFeatureState(id: id, shouldHighlight: false);
}
_highlightedFeatureIds.clear();
}
} catch (_) {
// ignore if the user taps too fast during map style reload
}
}
Future<void> _setFeatureState({
required String id,
required bool shouldHighlight,
}) async {
await _mapboxMap?.setFeatureState(
AppConstants.sourceId,
AppConstants.sourceLayerId,
id,
jsonEncode({'categoryHighlight': shouldHighlight}),
);
}Fetching building geometry from Overpass (OpenStreetMap) for a list of businesses, and returning it as GeoJSON-like features.
class OverpassGatewayImpl implements OverpassGateway {
@override
Future<List<Map<String, dynamic>>> getFeatures({
required List<BusinessDto> businesses,
}) async {
// Initialize a string buffer to build the Overpass API query
final buffer = StringBuffer();
buffer.write('[out:json];(');
// Loop through each business and add queries for nearby buildings
for (final b in businesses) {
final lat = b.coordinates.latitude;
final lon = b.coordinates.longitude;
// Query ways (building polygons) within 50 meters of the business
buffer.write('way(around:30,$lat,$lon)["building"];');
// Query relations (complex polygons like multipolygon) within 50 meters
buffer.write('relation(around:30,$lat,$lon)["building"];');
}
buffer.write(');(._;>;);out;');
// Build the Overpass API URL with the encoded query
final url =
'https://overpass-api.de/api/interpreter?data=${Uri.encodeComponent(buffer.toString())}';
final response = await http.get(Uri.parse(url));
// Check if the response is successful
if (response.statusCode != 200) {
debugPrint('Overpass error.');
return [];
}
// Decode the JSON response from Overpass API
final data = jsonDecode(response.body) as Map<String, dynamic>;
// Separate elements into nodes, ways, and relations
final nodes = <int, List<double>>{};
final ways = <int, List<int>>{};
final relations = <int, List<int>>{};
for (final Map<String, dynamic> el in data['elements']) {
if (el['type'] == 'node') {
// Store node coordinates (longitude, latitude)
nodes[el['id']] = [el['lon'], el['lat']];
} else if (el['type'] == 'way') {
// Store way nodes (list of node IDs)
ways[el['id']] = List<int>.from(el['nodes']);
} else if (el['type'] == 'relation') {
// For relations, store references to way IDs
final members = el['members'] as List;
final wayIds = members
.where((m) => m['type'] == 'way')
.map<int>((m) => m['ref'] as int)
.toList();
relations[el['id']] = wayIds;
}
}
final features = <Map<String, dynamic>>[];
// Process way-buildings into GeoJSON polygons
for (final entry in ways.entries) {
final coords = entry.value.map((id) => nodes[id]!).toList();
if (coords.isEmpty) continue;
// Close the polygon if first and last points are not the same
if (coords.first[0] != coords.last[0] ||
coords.first[1] != coords.last[1]) {
coords.add(coords.first);
}
// Add the polygon as a GeoJSON Feature
features.add({
'type': 'Feature',
'geometry': {
'type': 'Polygon',
'coordinates': [coords],
},
'properties': {'id': entry.key},
});
}
// Process multipolygon (relations)
for (final entry in relations.entries) {
final polygons = <List<List<double>>>[];
for (final wayId in entry.value) {
if (!ways.containsKey(wayId)) continue;
final coords = ways[wayId]!.map((id) => nodes[id]!).toList();
if (coords.isEmpty) continue;
// Close the polygon
if (coords.first[0] != coords.last[0] ||
coords.first[1] != coords.last[1]) {
coords.add(coords.first);
}
polygons.add(coords);
}
if (polygons.isNotEmpty) {
// Add the multipolygon as a GeoJSON Feature
features.add({
'type': 'Feature',
'geometry': {
'type': 'MultiPolygon',
'coordinates': [polygons],
},
'properties': {'id': entry.key},
});
}
}
return features;
}
}- Framework: Flutter
- Language: Dart
- Architecture: Clean Architecture
- State management: Flutter BLoC
- Dependency Injection: Get It
- REST API Communication: Flutter Http
- Screens Adaptivity: Flutter Screenutil
- Location Services: Flutter Geolocator
- Maps Interaction: Flutter Mapbox
Before running this project, make sure you have:
- Flutter SDK (3.9.0 or higher)
- Dart SDK (included with Flutter)
- Android Studio / Xcode (for mobile development)
- YELP API Key (get one from Yelp)
- Mapbox API Key (get one from Mapbox)
-
Clone the repository
git clone https://github.com/yourusername/flutter_maps_app.git cd maps -
Install dependencies
flutter pub get
-
Set up environment variables
Create a
environment/.envfile in the root directory and adjust with your API KEY:MAPBOX_ACCESS_TOKEN=your_mapbox_api_key_here YELP_API_KEY=your_yelp_api_key_here MAP_STYLE=your_custom_or_default_map_style_path_here
-
Run the application
flutter run
- Minimum SDK: 21
- Target SDK: Latest
- Permissions: Location
- iOS 14.0+
- Permissions: Location
- Category selection: Open the category bottom sheet and select any
- Building selection: Once the highlighted buildings loaded with yellow color, select any building to see its basic information
