Enhance Your Google Maps with Dynamic Themes Using Riverpod


Google maps
Since its launch in 2005, Google Maps has transformed from just a basic online mapping tool to a central part of modern mobile apps, used in everything from ride-sharing to travel guides and healthcare platforms. While Google Maps provides powerful default visuals, its generic theme may not always align with your app's branding or user experience goals. This leads to our topic of how to improve the user’s experience by adopting different map themes via the Google Maps Styling Wizard. Below is a working sample:
Project Setup
P.S. I assume this article's reader is familiar with the Flutter framework. 😅
There is a public repository for the full implementation on GitHub to follow along.
Installing 3rd-party plugins
The first step is creating a new Flutter project. Run “flutter create <DIRECTORY>” in the command line,
flutter create <DIRECTORY>
and add the necessary dependencies. Below is a list of the 3rd-party plugins used in this project:
Shared_Preferences: “Wraps platform-specific persistent storage for simple data (NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc)”
Flutter_Riverpod: “A reactive caching and data-binding framework.”
Geolocator: “A Flutter geolocation plugin that provides easy access to platform-specific location services (FusedLocationProviderClient or, if unavailable, the LocationManager on Android and CLLocationManager on iOS).”
Google_Maps_Flutter: “A Flutter plugin that provides a Google Maps widget.”
Flutter_expandable_fab: “A speed dial FAB (Floating Action Button) that can animate the display and hiding of multiple action buttons.”
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.6.1
flutter_expandable_fab: ^2.4.1
geolocator: ^13.0.4 # for location access
shared_preferences: ^2.5.3
…
Add Google Maps Dependency
To install Flutter Google Maps in the project, you should start by reading the documentation on pub: Google Maps Flutter
Add the
google_maps_flutter
dependency on your “pubspec.yaml” file: dependencies:dependencies: flutter: sdk: flutter google_maps_flutter: ^2.12.1
Run flutter pub get
…
Get an API key:
Go to the Google Cloud Console.
Create or select a project.
Enable the Maps SDK for Android and Maps SDK for iOS.
Create API credentials.
Copy the API key.
Configure Android
Open
android/app/src/main/AndroidManifest.xml
Add the API key inside the
<application>
tag:
<application
android:label="Map Themes"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
// ADD API KEY AS SEEN BELOW.....
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="${google.maps.api.key}"/>
Configure iOS
Open
ios/Runner/AppDelegate.swift
.Add the following import:
import Flutter
import UIKit
// --------- IMPORT GOOGLE MAPS LIBRARY
import GoogleMaps
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
if let path = Bundle.main.path(forResource: "Info", ofType: "plist"),
let dict = NSDictionary(contentsOfFile: path),
// ------ ADD GOOGLE MAPS KEY HERE AS SEEN BELOW .........
let apiKey = dict["GOOGLE_MAPS_API_KEY"] as? String {
GMSServices.provideAPIKey(apiKey)
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Open
ios/Runner/Info.plist
Add the following key inside the
</dict>
tag:
...
<key>GOOGLE_MAPS_API_KEY</key>
<string>${API_KEY}</string> // insert your key here
</dict>
...
</plist>
Map Configurations
abstract class MapRepository {
GoogleMapController setMapController(GoogleMapController controller);
GoogleMapController? getMapController();
(String, Stream<Position>) getLocationStream();
Future<(String, Position?)> getCurrentLocation();
Future<(String, bool)> locationPermissionStatus();
}
This repository helps maintain clean code by centralizing and abstracting all location-related logic using Google Maps in Flutter, promoting separation of concerns and reusability.
In my implementation, the MapRepository
handles the following responsibilities using the geolocator
plugin:
getLocationStream(): This method returns a continuous stream of location updates; I use it to update the user’s location on the map as they move.
getCurrentLocation(): This method retrieves the user’s current GPS location once; I used it to get the coordinates (latitude and Longitude) of the user on the initState of the
map_view
locationPermissionStatus(): Before accessing location data, your app must ensure that the user has granted the necessary permissions. This method checks the current permission status and optionally prompts the user if needed.
P.S.: Because this is a demo project, I combined the location methods above with the map methods and called it MapRepository
. In a larger project, it could be separated into MapRepository
and LocationRepository
- Other map methods:
setMapController(GoogleMapController controller): This method is used to store a reference to the
GoogleMapController
when the map is first created. I save this controller within the repository so it can interact with the map without directly accessing the UI layer.getMapController(): I use this method to retrieve the stored
GoogleMapController
instance. This allows other classes or services to access the map controller safely and consistently.
P.S.: See the project map_repository.dart
file for MapRepository implementation.
Creating the Map Provider to access the locationStream() method
Utilizing the Riverpod StreamProvider
API, I access the locationStream
from MapRepository via ref
// -------- IMPORT THE CLASSES
import 'package:dynamic_map_themes/repository/map_repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';
final mapViewProvider = StreamProvider<Position>(
(ref) {
var locationStream = ref.read(mapRepositoryProvider).getLocationStream().$2;
return locationStream.handleError((error) {
// Handle the error here
print('Error: $error');
});
},
);
…
Creating the map view
import 'package:dynamic_map_themes/presentation/widgets/floating_themes_btn.dart';
import 'package:dynamic_map_themes/provider/map_styles_provider.dart';
import 'package:dynamic_map_themes/provider/map_view_provider.dart';
import 'package:dynamic_map_themes/repository/map_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class MapView extends ConsumerStatefulWidget {
const MapView({
super.key,
});
@override
ConsumerState<MapView> createState() => _MapViewState();
}
class _MapViewState extends ConsumerState<MapView> {
@override
void initState() {
ref.read(mapRepositoryProvider).getCurrentLocation();
super.initState();
}
@override
Widget build(BuildContext context) {
final mapStyles = ref.watch(mapStyleProvider).mapStyle;
final mapView = ref.watch(mapViewProvider);
return Scaffold(
key: UniqueKey(),
floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: FloatingThemeBtnsWidget(),
body: mapView.when(
data: (data) {
return GoogleMap(
initialCameraPosition: CameraPosition(
target: LatLng(data.latitude, data.longitude),
zoom: 13,
),
style: mapStyles.isEmpty == true ? null : mapStyles,
myLocationButtonEnabled: false,
mapType: MapType.normal,
onMapCreated: (controller) {
ref.read(mapRepositoryProvider).setMapController(controller);
},
myLocationEnabled: true,
);
},
error: (error, stackTrace) {
return const Center(
child: Text(
'Please enable your location, and try again!',
style: TextStyle(color: Colors.red),
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
),
);
}
}
…
In the
initState()
, we can create the map view by using a stateful widget, which gives us access to the initState, a lifecycle method that is called when a state object is created. It is called only once, and it’s best for initializing our getCurrentLocation() method, which requests permission and retrieves the user's location. This is why we usedref.read
; it is used to access the current value of a provider without listening to it for changes.In the
build()
method, we are permitted to use the Riverpodref.watch
method that listens to changes in providers. ref.watch(mapStyleProvider).mapStyle listens to the current map style, while theref.watch(mapViewProvider) listens to the live stream of the user's location.
The
Google Map Widget
dynamically updates:
initialCameraPosition: uses the current coordinates from the
mapViewProvider
.style: uses the dynamic map style (
null
if not set).onMapCreated: stores the controller in the repository for later use.
mapType: Google Maps provides this enum that contains four values (hybrid, satellite, normal, terrain). For this project, we would be using the
normal
type to access the themes JSON.The
ExpandableFab
renders floating buttons (defined inFloatingThemeBtnsWidget
) to switch between themes dynamically.
See the lib/presentation/widgets/floating_themes_btn.dart
in the project repository for the implementation of the custom floating action button used. You can also find the plugin on pub: flutter_expandable_fab
Creating the Map Styles
We download the map styles JSON we need for the project from Google's Styling Wizard at mapstyle.withgoogle by following these steps:
Adjust the map features, colors, and elements to match your preference using the available tools.
Once satisfied with your customizations, click on the "Finish" button.
Copy the JSON and paste it inside our Flutter project assets folder
Create a Provider class to manage the map styles; here, we utilize the Notifier class from the Riverpod plugin to manage the state
Creating the Map Styles Provider
import 'package:dynamic_map_themes/core/constants/asset_images.dart';
import 'package:dynamic_map_themes/core/enums/map_enums.dart';
import 'package:dynamic_map_themes/utils/shared_pref_helper.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final mapStyleProvider = AsyncNotifierProvider<MapStyleAsyncNotifier, MapStylesState>(
() => MapStyleAsyncNotifier(),
);
class MapStyleAsyncNotifier extends AsyncNotifier<MapStylesState> {
@override
Future<MapStylesState> build() async {
final style = await _getSelectedMapStyle();
return MapStylesState(mapStyle: style);
}
Future<String> _getSelectedMapStyle() async {
final savedMapStyleName = SharedPreferencesHelper.getString(key: SharedKeys.mapStyle.name);
final mapStyleEnum = MapStyles.styleName(savedMapStyleName ?? MapStyles.original.name);
return switch (mapStyleEnum) {
MapStyles.dark => await rootBundle.loadString(AssetImages.darkMapStylesJson),
MapStyles.night => await rootBundle.loadString(AssetImages.nightMapStylesJson),
MapStyles.nightBlue => await rootBundle.loadString(AssetImages.nightBlueMapStylesJson),
MapStyles.retro => await rootBundle.loadString(AssetImages.retroMapStylesJson),
MapStyles.original => '',
};
}
Future<void> setMapStyles(String mapStyleName) async {
// Save preference
await SharedPreferencesHelper.setString(
key: SharedKeys.mapStyle.name,
value: mapStyleName,
);
// Reload the selected style and update state
final style = await _getSelectedMapStyle();
state = AsyncValue.data(MapStylesState(mapStyle: style));
}
}
class MapStylesState {
final String mapStyle;
final bool tapped, isOffline;
MapStylesState({
this.mapStyle = '',
this.tapped = false,
this.isOffline = false,
});
MapStylesState copyWith({
String? mapStyle,
bool? tapped,
bool? isOffline,
}) {
return MapStylesState(
mapStyle: mapStyle ?? this.mapStyle,
isOffline: isOffline ?? this.isOffline,
tapped: tapped ?? this.tapped,
);
}
}
getSelectedMapStyle()
: This method checks for a saved map style in shared preferences, converts it to aMapStyles
enum using a static method, then uses a switch expression to load the corresponding style JSON file.setMapStyles()
: This method is triggered by the FloatingActionButton to choose a map style and update the state accordingly.
AsynNotifier
: Riverpod provides multiple state management APIs, such as Notifier
, AsyncNotifier
, and StateNotifier
, each is suited for different use cases. In this case, we use AsyncNotifier
because getSelectedMapStyle()
returns a Future<String>
, and AsyncNotifier
is designed to handle asynchronous state by exposing an AsyncValue<T>
and supporting build()
methods that can be async
.
This is how I defined the MapStyles Enum
enum MapStyles {
original,
dark,
night,
nightBlue,
retro;
static MapStyles styleName(String name) {
return MapStyles.values.firstWhere(
(e) => e.name == name,
orElse: () => MapStyles.original,
);
}
}
This is how I declare my assets path.
class AssetImages {
static const String offlineMapStylesJson = 'assets/json/offline_map_style.json'; //
static const String darkMapStylesJson = 'assets/json/dark_map_style.json'; //
static const String nightMapStylesJson = 'assets/json/night_map_style.json'; //
static const String retroMapStylesJson = 'assets/json/retro_map_style.json'; //
static const String nightBlueMapStylesJson = 'assets/json/night_blue_map_style.json'; //
}
Result
Conclusion
This article provides a comprehensive guide to setting up a Flutter project with Google Maps integration, highlighting essential steps such as creating a new Flutter project, installing third-party plugins like Shared_Preferences, Flutter_Riverpod, Geolocator, and Google_Maps_Flutter, and configuring Android and iOS platforms for map functionality.
We see that integrating dynamic Google Map themes into your Flutter app with Riverpod is more than just a design enhancement — it's a powerful way to deliver a seamless, user-centric experience, and Riverpod reactive architecture makes it easy to manage state and update the themes in real time.
👉 Ready to get started? Check out the full implementation on GitHub and bring your maps to life!
References
Dynamic Map Themes Github Repository
Flutter_Expandable_Fab Plugin (pub.dev)
Subscribe to my newsletter
Read articles from Nkpozi Marcel Kelechi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
