Skip to main content

Get Started

Contents

Introduction

Welcome! This guide provides a foundational example demonstrating how to integrate Trimble’s Mobile Maps SDK into a Flutter application, specifically targeting the Android platform. Our goal is to help you get a basic map view up and running using Flutter’s PlatformView mechanism.

Who is this guide for?

This guide is intended for developers who have some basic experience with Flutter development. We assume you are familiar with:

  • Creating and running a new Flutter project.
  • The general structure of a Flutter application.
  • Basic Dart and Flutter concepts.

If you are new to Flutter or need a refresher, we highly recommend starting with the official Flutter ‘Get Started’ guide before proceeding.

What will you learn?

By following this guide, you will learn how to:

  1. Set up the necessary configurations in your Android Flutter project to use the Trimble Maps SDK.
  2. Implement a basic Flutter PlatformView to display a Trimble Map within your Flutter UI.
  3. Establish the initial connection between your Flutter code and the native Android map view.

Scope and Limitations

Please note that this guide focuses solely on the initial setup and integration for displaying a map on Android using PlatformView. It serves as a starting point and does not cover:

  • iOS integration (currently unsupported by this specific guide).
  • The full range of features and capabilities offered by the Trimble Maps SDK (like routing, custom styling, location services integration, etc.).
  • Advanced PlatformView communication techniques.

For comprehensive information on the SDK’s features and functionalities, this guide should be used in conjunction with the official Trimble Maps SDK for Android documentation.

Let’s get started!

Set Up Dependencies

Before writing any Dart code to display the map, you need to configure the native Android part of your Flutter application to include the Trimble Maps SDK libraries. This involves two main steps: telling Gradle where to find the SDK (adding the repository) and specifying which SDK libraries your app needs (adding the dependencies).

Step 1: Add the Trimble Maps Repository

Gradle needs to know the location of the Trimble Maps Maven repository to download the SDK libraries.

  1. Open your project-level Gradle configuration file, located at: android/build.gradle

  2. Locate the allprojects block. Inside it, find the repositories block.

  3. Add the Trimble Maps Maven repository URL within the repositories block. Make sure it’s alongside other repositories like google() and mavenCentral().

// android/build.gradle
allprojects {
	repositories {
    		google()
    		mavenCentral()

    		// Add the Trimble Maps Maven repository
    		maven {
        			url = uri("https://trimblemaps.jfrog.io/artifactory/android")
    		}
	}
}

Note: The exact structure of your android/build.gradle might differ slightly, but the key is to add the maven { ... } block inside allprojects { repositories { ... } }.

Step 2: Add the SDK Dependencies

Now, specify the Trimble Maps SDK libraries that your application module depends on.

  1. Open your app-level Gradle configuration file, located at: android/app/build.gradle

  2. Scroll down to find the dependencies { ... } block.

  3. Add the implementation lines for the Trimble Maps Android SDK and Services SDK inside this block.

// android/app/build.gradle
dependencies {
	// Other existing Flutter and Android dependencies will be here...
	// implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // Example

	// Add Trimble Maps SDK dependencies here.
	implementation("com.trimblemaps.mapsdk:maps-android-sdk:2.0.0")
	implementation("com.trimblemaps.mapsdk:maps-sdk-services:1.5.0")

	// Other existing test dependencies might be here, e.g.:
	// testImplementation 'junit:junit:4.13.2'
	// androidTestImplementation 'androidx.test.ext:junit:1.1.5'
}

Important:

  • The version numbers (2.0.0 for maps-android-sdk and 1.5.0 for maps-sdk-services) provided here are examples. While they should work for this guide, you might want to check the developer portal or the Artifactory repository for the latest released versions for your production applications.
  • Ensure you add these lines within the main dependencies { ... } block, not inside any other specific configuration block unless intended.

Step 3: Sync Gradle Files

After editing the Gradle files, your IDE (like Android Studio or VS Code) will likely show a notification prompting you to sync your project. Click Sync Now (or the equivalent option) to ensure your project recognizes the new repository and dependencies.

Implement the Native Android View (TrimbleMapView.kt)

Now that the dependencies are set up, we need to create the native Android code that will manage and display the Trimble Map. We’ll use Flutter’s PlatformView interface to embed this native view within our Flutter widget tree.

This involves creating a Kotlin class that:

  1. Initializes the Trimble Maps SDK and the native MapView.
  2. Manages the lifecycle of the MapView.
  3. Sets up a MethodChannel to receive commands from Flutter.
  4. Executes map actions (like zooming or changing style) based on those commands.
  5. Returns the actual MapView instance to be displayed by Flutter.

Step 1: Create the TrimbleMapView.kt File

First, create a new Kotlin file named TrimbleMapView.kt within your Android project structure. A common location is:

android/app/src/main/kotlin/com/your_company_name/your_project_name/TrimbleMapView.kt

(Remember to replace com/your_company_name/your_project_name/ with your actual package name structure).

Step 2: Add the TrimbleMapView Class Code

Paste the following Kotlin code into the TrimbleMapView.kt file you just created. (We’ve added extra logging in here so you can follow the workflow in your debugger.):

import android.content.Context
import android.util.Log
import android.view.View
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.platform.PlatformView
import com.trimblemaps.account.LicensedFeature
import com.trimblemaps.account.TrimbleMapsAccountManager
import com.trimblemaps.account.models.TrimbleMapsAccount
import com.trimblemaps.mapsdk.TrimbleMaps
import com.trimblemaps.mapsdk.camera.CameraPosition
import com.trimblemaps.mapsdk.camera.CameraUpdateFactory
import com.trimblemaps.mapsdk.geometry.LatLng
import com.trimblemaps.mapsdk.maps.MapView
import com.trimblemaps.mapsdk.maps.OnMapReadyCallback
import com.trimblemaps.mapsdk.maps.Style
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding // Needed for lifecycle

/**
 * A PlatformView that wraps the Trimble Map SDK's MapView and
 * exposes zoom, style, and center controls via a MethodChannel.
 * It also handles MapView lifecycle events by observing the host Activity's lifecycle.
 */
class TrimbleMapView(
	private val context: Context,        	// Android context for view creation
	messenger: BinaryMessenger,           	// Flutter BinaryMessenger for channel communication
	private val viewId: Int,              	// Unique identifier for this map view instance
	private val apiKey: String            	// API key for Trimble Maps services
) : PlatformView, DefaultLifecycleObserver { // Implement PlatformView and LifecycleObserver

	companion object {
    	private const val TAG = "TrimbleMapView"  // Tag for Android log messages
	}

	// The core native Trimble MapView instance
	private val mapView: MapView

	// MethodChannel for communication between Flutter and this native view.
	// The channel name includes the viewId to ensure uniqueness if multiple maps are shown.
	private val channel: MethodChannel = MethodChannel(
    	messenger,
    	"trimble_map_view_$viewId" // Unique channel name
	)

	// Initialization block executed when a TrimbleMapView instance is created
	init {
    	Log.d(TAG, "Initializing TrimbleMapView for viewId=$viewId")

    	// 1. Initialize Trimble Maps Account (Authentication)
    	// Sets up the account using the provided API key and specifies
    	// that we intend to use the Maps SDK feature.
    	val account: TrimbleMapsAccount = TrimbleMapsAccount.builder()
        	.apiKey(apiKey)
        	.addLicensedFeature(LicensedFeature.MAPS_SDK)
        	.build()
    	TrimbleMapsAccountManager.initialize(account)
    	// IMPORTANT: Wait for initialization to complete before proceeding.
    	TrimbleMapsAccountManager.awaitInitialization()
    	Log.d(TAG, "TrimbleMapsAccount initialized")

    	// 2. Initialize the Native MapView
    	// Ensures the TrimbleMaps singleton is ready and creates the MapView UI component.
    	TrimbleMaps.getInstance(context)
    	mapView = MapView(context)

    	// 3. Setup Asynchronous Map Ready Callback
    	// The map needs time to load resources and its style. We use getMapAsync
    	// to get a callback (`OnMapReadyCallback`) when the map is fully ready.
    	mapView.getMapAsync(OnMapReadyCallback { trimbleMap ->
        	Log.d(TAG, "Map ready, applying initial style: MOBILE_DAY")
        	// Set an initial map style (e.g., MOBILE_DAY)
        	trimbleMap.setStyle(Style.MOBILE_DAY) { style -> // Callback fires when style is loaded
            	Log.d(TAG, "Style loaded: ${style.uri}")
            	// IMPORTANT: Notify Flutter that the map is ready to be interacted with.
            	try {
                	channel.invokeMethod("mapReady", null)
                	Log.d(TAG, "Sent 'mapReady' notification to Flutter")
            	} catch (e: Exception) {
                	Log.e(TAG, "Error invoking mapReady on channel", e)
            	}
        	}
    	})

    	// 4. Set up Method Call Handler
    	// This listener waits for method calls invoked from the Flutter side using the channel.
    	channel.setMethodCallHandler { call, result ->
        	Log.d(TAG, "Received method call from Flutter: ${call.method}")
        	try {
            	// Use a 'when' block to handle different method names sent from Flutter
            	when (call.method) {
                	"zoomIn" -> {
                    	mapView.getMapAsync { map -> map.easeCamera(CameraUpdateFactory.zoomIn()) }
                    	result.success(null) // Indicate success back to Flutter
                    	Log.d(TAG, "zoomIn executed")
                	}
                	"zoomOut" -> {
                    	mapView.getMapAsync { map -> map.easeCamera(CameraUpdateFactory.zoomOut()) }
                    	result.success(null)
                    	Log.d(TAG, "zoomOut executed")
                	}
                	"toggleStyle" -> {
                    	mapView.getMapAsync { map ->
                        	// Check the current style and switch to the other
                        	val newStyle = if (map.style?.uri == Style.MOBILE_DAY) Style.MOBILE_NIGHT else Style.MOBILE_DAY
                        	map.setStyle(newStyle)
                        	Log.d(TAG, "toggleStyle executed, set to: ${newStyle}")
                    	}
                    	result.success(null)
                	}
                	"center" -> {
                    	// Extract arguments sent from Flutter
                    	val lat = call.argument<Double>("lat") ?: 0.0
                    	val lng = call.argument<Double>("lng") ?: 0.0
                    	Log.d(TAG, "center executing for Lat: $lat, Lng: $lng")
                    	// Build a camera position centered on the coordinates with a fixed zoom
                    	val position = CameraPosition.Builder()
                        	.target(LatLng(lat, lng))
                        	.zoom(8.0) // Example zoom level
                        	.build()
                    	// Animate the map camera to the new position
                    	mapView.getMapAsync { map ->
                       	map.animateCamera(CameraUpdateFactory.newCameraPosition(position))
                    	}
                    	result.success(null)
                	}
                	// Handle any methods that aren't recognized
                	else -> {
                    	Log.w(TAG, "Method not implemented: ${call.method}")
                    	result.notImplemented()
                	}
            	}
        	} catch (e: Exception) {
            	// Catch potential errors during method handling and report back to Flutter
            	Log.e(TAG, "Error handling method ${call.method}", e)
            	result.error("NATIVE_ERROR", "Error executing method ${call.method}: ${e.localizedMessage}", null)
        	}
    	}
	}

	/**
 	* Attaches this view as an observer to the host Activity's lifecycle.
 	* This is crucial for forwarding lifecycle events (like onResume, onPause)
 	* to the underlying MapView, which needs them for proper operation.
 	*/
	fun attachToActivity(binding: ActivityPluginBinding) {
    	Log.d(TAG, "Attaching to activity lifecycle")
    	// Cast the activity to LifecycleOwner and add this class as an observer
    	(binding.activity as? LifecycleOwner)?.lifecycle?.addObserver(this)
	}

	// --- Lifecycle Event Handling (from DefaultLifecycleObserver) ---
	// These methods are called automatically when the host Activity's lifecycle changes.
	// We forward these calls directly to the corresponding MapView methods.

	override fun onStart(owner: LifecycleOwner) {
    	Log.d(TAG, "Forwarding onStart to MapView")
    	mapView.onStart()
	}
	override fun onResume(owner: LifecycleOwner) {
    	Log.d(TAG, "Forwarding onResume to MapView")
    	mapView.onResume()
	}
	override fun onPause(owner: LifecycleOwner) {
    	Log.d(TAG, "Forwarding onPause to MapView")
    	mapView.onPause()
	}
	override fun onStop(owner: LifecycleOwner) {
    	Log.d(TAG, "Forwarding onStop to MapView")
    	mapView.onStop()
	}
	override fun onDestroy(owner: LifecycleOwner) {
    	Log.d(TAG, "Forwarding onDestroy to MapView and cleaning up observer")
    	mapView.onDestroy()
    	// Optional: Remove observer explicitly, though lifecycle handling usually manages this.
    	owner.lifecycle.removeObserver(this)
	}

	// --- PlatformView Implementation ---

	/**
 	* Returns the actual Android View (the MapView instance) that Flutter will embed.
 	*/
	override fun getView(): View = mapView

	/**
 	* Called when the Flutter PlatformView is disposed.
 	* Essential for cleaning up resources to prevent memory leaks.
 	*/
	override fun dispose() {
    	Log.d(TAG, "dispose called - Cleaning up MethodChannel and MapView")
    	// Remove the method call handler to break the communication link.
    	channel.setMethodCallHandler(null)
    	// Destroy the MapView to release its resources.
    	mapView.onDestroy()
    	// Note: Lifecycle observer cleanup happens in onDestroy generally.
	}
}

Step 3: Review the Code

  • Class Definition:
    • TrimbleMapView implements PlatformView (required by Flutter to get the native View) and DefaultLifecycleObserver (to hook into the Android Activity’s lifecycle).
    • The constructor receives context, messenger (for MethodChannel), viewId (unique ID for this view instance), and your apiKey.
  • mapView: MapView: This holds the actual instance of the Trimble Maps MapView widget.
  • channel: MethodChannel: This is the communication bridge. Note the channel name trimble_map_view_$viewId – using the viewId ensures that if you had multiple map views, each would have its own distinct channel.
  • init { ... } Block: This is executed when TrimbleMapView is created:
    • Account Initialization: Sets up authentication using your API key and the .addLicensedFeature(LicensedFeature.MAPS_SDK) line. awaitInitialization() is crucial to ensure the account is ready before creating the map.
    • MapView Initialization: Creates the native MapView UI component.
    • getMapAsync { ... }: This is vital. Map initialization is asynchronous. The code inside this callback runs only after the map is fully loaded and ready.
      • trimbleMap.setStyle(...): Sets an initial visual style for the map.
      • channel.invokeMethod("mapReady", null): Sends a signal back to Flutter indicating that the native map is ready. This is important so the Flutter side knows it can start sending commands.
    • channel.setMethodCallHandler { ... }: This is the listener for incoming messages from Flutter.
      • The when (call.method) block checks the name of the method invoked by Flutter (e.g., "zoomIn", "toggleStyle").
  • attachToActivity(...): This function will be called later (by the PlatformViewFactory) to link this view’s lifecycle handling to the main Android Activity.
  • Lifecycle Methods (onStart, onResume, etc.): By implementing DefaultLifecycleObserver and registering via attachToActivity, these methods automatically receive calls when the hosting Android Activity goes through lifecycle changes. It’s critical to forward these calls to the mapView’s corresponding methods (mapView.onStart(), mapView.onResume(), etc.) for the map to function correctly (e.g., render updates, save state, release resources).
  • getView(): View: Implements the PlatformView requirement, simply returning the mapView instance so Flutter can display it.
  • dispose(): Implements the PlatformView requirement for cleanup. It removes the method channel handler and, crucially, calls mapView.onDestroy() to release the map’s resources when the Flutter widget is removed from the tree. This prevents memory leaks.

With this class created, you have the core native component ready. The next step is to work on the TrimbleMapViewFactory so that Flutter can use it to create instances of this TrimbleMapView.

Create the PlatformView Factory (TrimbleMapViewFactory.kt)

Flutter needs a way to create instances of the TrimbleMapView you defined earlier whenever it needs to display the map widget. This is done using a PlatformViewFactory.

The factory is a relatively simple class whose primary responsibility is to receive creation parameters from Flutter (like your API key) and use them to instantiate and return a new TrimbleMapView.

Step 1: Create the TrimbleMapViewFactory.kt File

Create a new Kotlin file named TrimbleMapViewFactory.kt in the same directory as your TrimbleMapView.kt:

android/app/src/main/kotlin/com/your_company_name/your_project_name/TrimbleMapViewFactory.kt

(Again, replace com/your_company_name/your_project_name/ with your actual package name).

Step 2: Add the TrimbleMapViewFactory Class Code

Paste the following Kotlin code into the TrimbleMapViewFactory.kt file.

import android.content.Context                        	// Android context for view creation
import io.flutter.plugin.common.BinaryMessenger        	// Messenger for communicating with Flutter side
import io.flutter.plugin.common.StandardMessageCodec  	// Codec for encoding/decoding messages
import io.flutter.plugin.platform.PlatformView         	// Interface for embedding native views in Flutter
import io.flutter.plugin.platform.PlatformViewFactory  	// Factory for creating PlatformView instances

/**
 * Factory responsible for creating instances of TrimbleMapView.
 * Extends PlatformViewFactory to integrate with Flutter's
 * platform view mechanism using StandardMessageCodec.
 */
class TrimbleMapViewFactory(
	private val messenger: BinaryMessenger              	// BinaryMessenger from Flutter engine for channel setup
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {  // Use StandardMessageCodec for argument marshalling

	/**
 	* Called by Flutter when a new platform view is needed.
 	* @param context Android context for view creation
 	* @param viewId Unique identifier for this view instance
 	* @param args Initialization parameters passed from Dart
 	* @return A new instance of TrimbleMapView
 	*/
	override fun create(
    	context: Context,
    	viewId: Int,
    	args: Any?
	): PlatformView {
    	// Cast the args to a map of parameters (must match what Dart side sends)
    	val params = args as Map<String, Any>
    	// Extract the API key for Trimble Maps from initialization params
    	val apiKey = params["apiKey"] as String
    	// Create and return the native TrimbleMapView with provided context, messenger, view ID, and API key
    	return TrimbleMapView(context, messenger, viewId, apiKey)
	}
}

Step 3: Review the Code

  • Class Definition:
    • TrimbleMapViewFactory extends Flutter’s PlatformViewFactory.
    • It uses StandardMessageCodec.INSTANCE which is the default mechanism Flutter uses to encode the args parameter passed during view creation.
    • The constructor takes a BinaryMessenger instance. This is essential because the TrimbleMapView needs it to set up its MethodChannel for communication back to Flutter. The BinaryMessenger is typically obtained from the Flutter plugin registrar or engine.
  • create Method:
    • This is the core method implemented from PlatformViewFactory. Flutter calls this method automatically whenever your Dart code requests a new instance of the platform view associated with this factory.
    • context: Context: The Android application context.
    • viewId: Int: A unique integer ID assigned by Flutter for this specific instance of the platform view. This ID is passed to TrimbleMapView and used to create a unique MethodChannel name (trimble_map_view_$viewId).
    • args: Any?: This parameter holds the data passed from your Dart code when you create the AndroidView widget. In this example, we expect it to be a Map containing the API key.
    • Argument Parsing: The code safely casts args to Map<String, Any> and extracts the apiKey value associated with the key "apiKey". Crucially, the key used here ("apiKey") must exactly match the key you will use in your Dart code when providing creation parameters.
    • Instantiation: Finally, it creates a new TrimbleMapView instance, passing the required context, the messenger (received in the factory’s constructor), the unique viewId, and the extracted apiKey. This new TrimbleMapView object is then returned to Flutter, which handles embedding its view (mapView.getView()) into the Flutter UI.

With the TrimbleMapView and its TrimbleMapViewFactory created, the next step is to register this factory with the Flutter engine so that Flutter knows how to create your native map view when requested from Dart.

Register the PlatformView Factory

Now that you have the native view (TrimbleMapView) and its factory (TrimbleMapViewFactory), you need to tell Flutter about the factory so it can use it. This registration happens in your app’s main Android Activity, typically MainActivity.kt.

By registering the factory, you associate a unique string identifier (the “view type”) with your factory class. When Flutter encounters this identifier in your Dart code (within an AndroidView widget), it will know to ask your TrimbleMapViewFactory to create the native view.

Step 1: Locate and Modify MainActivity.kt

Open your main activity file, usually located at:

android/app/src/main/kotlin/com/your_company_name/your_project_name/MainActivity.kt

Modify the configureFlutterEngine method to register your factory. If the method doesn’t exist, you’ll need to override it.

Step 2: Add the Factory Registration Code

Update your MainActivity.kt to include the following registration logic within the configureFlutterEngine method:

// Keep existing imports like:
// import android.os.Bundle
// import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.BinaryMessenger

/**
 * MainActivity sets up the Flutter engine and registers the TrimbleMapViewFactory
 * so that the native map view can be embedded within Flutter UI.
 */
class MainActivity : FlutterActivity() {

	/**
 	* Called when the FlutterEngine is attached to this Activity.
 	* Use this hook to register platform-specific plugins or view factories.
 	*/
	override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    	super.configureFlutterEngine(flutterEngine)

    	// 1. Obtain the BinaryMessenger
    	// This is needed by the factory to pass down to the TrimbleMapView
    	// for setting up the MethodChannel communication.
    	val messenger: BinaryMessenger = flutterEngine.dartExecutor.binaryMessenger

    	// 2. Register the TrimbleMapViewFactory
    	// Associates the unique string "trimble_map_view" with your factory instance.
    	// Flutter will use this identifier from the Dart side to request the view.
    	flutterEngine
        	.platformViewsController
        	.registry
        	.registerViewFactory(
            	"trimble_map_view",       	// <-- The View Type ID (used in Dart)
            	TrimbleMapViewFactory(messenger) // <-- Instance of your factory
        	)
	}
}

Step 3: Review the Code

  • configureFlutterEngine Override: This method is the standard entry point provided by FlutterActivity for interacting with the FlutterEngine during setup.
  • Getting BinaryMessenger: We retrieve the BinaryMessenger from the flutterEngine.dartExecutor. This messenger is crucial for enabling communication (MethodChannels) between Dart and native code. It’s passed to the TrimbleMapViewFactory constructor.
  • registerViewFactory Call:
    • This is the core of the registration. It’s accessed through the engine’s platformViewsController.registry.
    • "trimble_map_view": This is the view type identifier. It’s a unique string that you define. You must use this exact same string in your Dart code when creating the AndroidView widget later. This is how Flutter knows which native view to render.
    • TrimbleMapViewFactory(messenger): An instance of the factory class you created earlier is passed here. The messenger is provided to its constructor.

With the factory registered, the Android native side is now fully configured. The next steps involve writing the Dart code in your Flutter application to display this native view and communicate with it.


Implement the Flutter UI (main.dart)

With the native Android components set up and registered, we can now write the Flutter code to display the map and add interactive controls.

This involves:

  1. Setting up the main application structure.
  2. Using the AndroidView widget to embed the native view, identified by the viewType string we registered earlier.
  3. Passing the necessary API key to the native side during view creation.
  4. Establishing a MethodChannel to communicate between Dart and the native TrimbleMapView.
  5. Handling the mapReady callback from native code to know when the map is initialized.
  6. Creating UI buttons that invoke methods (like zoomIn, toggleStyle) on the native map view via the MethodChannel.

Step 1: Locate main.dart

This code typically resides in the main entry point of your Flutter application:

lib/main.dart

Step 2: Add the Flutter Application Code

Replace the contents of your lib/main.dart with the following code:

import 'package:flutter/foundation.dart';	// Provides defaultTargetPlatform
import 'package:flutter/material.dart';  	// Material widgets and theming
import 'package:flutter/services.dart';  	// MethodChannel and message codecs

void main() {
  // Ensure Flutter framework is fully initialized before creating the app
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());                	// Launch the app
}

/// Root widget of the application
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
	return const MaterialApp(
  	// IMPORTANT: Replace 'YOUR_API_KEY' with your actual Trimble Maps API key
  	home: MapWithControls(apiKey: 'YOUR_API_KEY'),
	);
  }
}

/// Stateful widget that hosts the native Trimble map and control buttons
class MapWithControls extends StatefulWidget {
  final String apiKey; // API Key passed from MyApp
  const MapWithControls({Key? key, required this.apiKey}) : super(key: key);

  @override
  State<MapWithControls> createState() => _MapWithControlsState();
}

class _MapWithControlsState extends State<MapWithControls> {
  // Channel for communicating Dart -> Native (TrimbleMapView)
  late MethodChannel _channel;
  // Flag to track if the native map has finished loading its initial style
  bool _mapReady = false;

  /// Callback triggered by AndroidView widget AFTER the native view is created.
  void _onPlatformViewCreated(int id) {
	debugPrint('Native PlatformView created with id: $id');

	// Initialize the MethodChannel.
	// IMPORTANT: The channel name MUST match the one used in TrimbleMapView.kt
	_channel = MethodChannel('trimble_map_view_$id');

	// Set up a handler for messages FROM Native TO Dart.
	_channel.setMethodCallHandler((call) async {
  	debugPrint('Dart received method call: ${call.method}');
  	// Check if the native side signaled that the map is ready
  	if (call.method == 'mapReady') {
    	debugPrint('mapReady received from native, enabling controls.');
    	// Update state to enable UI controls that interact with the map
    	setState(() {
      	_mapReady = true;
    	});
  	}
  	// Handle other potential methods from native if needed
	});
  }

  @override
  Widget build(BuildContext context) {
	// This example uses AndroidView, so restrict to Android platform
	if (defaultTargetPlatform != TargetPlatform.android) {
  	return const Scaffold(
    	body: Center(child: Text('This example currently supports Android only.')),
  	);
	}

	// Main UI structure
	return Scaffold(
  	appBar: AppBar(title: const Text('Trimble Map Flutter Example')),
  	body: Stack( // Use Stack to overlay controls on top of the map
    	children: [
      	// The widget that embeds the native Android view
      	AndroidView(
        	// IMPORTANT: Must match the viewType registered in MainActivity.kt
        	viewType: 'trimble_map_view',
        	// Callback function called when the native view is created
        	onPlatformViewCreated: _onPlatformViewCreated,
        	// Parameters passed to the native PlatformViewFactory's 'create' method
        	creationParams: <String, dynamic>{
          	// IMPORTANT: Key 'apiKey' must match the key expected in TrimbleMapViewFactory.kt
          	'apiKey': widget.apiKey,
        	},
        	// Codec used for encoding 'creationParams', must match the factory
        	creationParamsCodec: const StandardMessageCodec(),
      	),

      	// Overlay container for control buttons
      	Positioned(
        	bottom: 20,
        	left: 20,
        	right: 20, // Allow buttons to space out if needed
        	child: Card( // Wrap buttons in a Card for better visibility
          	elevation: 4.0,
          	child: Padding(
            	padding: const EdgeInsets.all(8.0),
            	child: Wrap( // Use Wrap for button layout flexibility
              	spacing: 8.0, // Horizontal space between buttons
              	runSpacing: 4.0, // Vertical space if buttons wrap
              	alignment: WrapAlignment.center,
              	children: [
                	// --- Map Control Buttons ---

                	ElevatedButton(
                  	// Enable button only when _mapReady is true
                  	onPressed: _mapReady ? _zoomIn : null,
                  	child: const Text('Zoom In'),
                	),

                	ElevatedButton(
                  	onPressed: _mapReady ? _zoomOut : null,
                  	child: const Text('Zoom Out'),
                	),

                	ElevatedButton(
                  	onPressed: _mapReady ? _toggleStyle : null,
                  	child: const Text('Toggle Style'),
                	),

                	ElevatedButton(
                  	onPressed: _mapReady ? _centerMap : null,
                  	child: const Text('Center (CA)'),
                	),
              	],
            	),
          	),
        	),
      	),
    	],
  	),
	);
  }

  // --- Button Action Methods ---

  void _zoomIn() {
	debugPrint('Button: Zoom In');
	// Invoke the 'zoomIn' method on the native side via the channel
	_channel.invokeMethod('zoomIn');
  }

  void _zoomOut() {
	debugPrint('Button: Zoom Out');
	// Invoke the 'zoomOut' method on the native side
	_channel.invokeMethod('zoomOut');
  }

  void _toggleStyle() {
	debugPrint('Button: Toggle Style');
	// Invoke the 'toggleStyle' method on the native side
	_channel.invokeMethod('toggleStyle');
  }

  void _centerMap() {
	// Define coordinates (Example: somewhere in California)
	const double lat = 36.7783;
	const double lng = -119.4179;
	debugPrint('Button: Center Map ($lat, $lng)');
	// Invoke the 'center' method on the native side, passing arguments
	_channel.invokeMethod('center', <String, double>{
  	'lat': lat,
  	'lng': lng,
	});
  }
}

Summary and Next Steps

Congratulations! You have successfully integrated a basic Trimble Map view into a Flutter application for Android using PlatformView.

In this guide, we covered the essential steps:

  • Configured Dependencies: Added the Trimble Maps Maven repository and SDK dependencies to the Android project’s Gradle files (android/build.gradle and android/app/build.gradle).
  • Implemented Native View: Created the TrimbleMapView.kt class, which implements PlatformView, initializes the native Trimble MapView, handles its lifecycle, and sets up a MethodChannel for communication.
  • Created View Factory: Implemented TrimbleMapViewFactory.kt to enable Flutter to create instances of the native TrimbleMapView, passing the API key during creation.
  • Registered Factory: Updated MainActivity.kt to register the TrimbleMapViewFactory with the Flutter engine using a unique view type identifier ("trimble_map_view").
  • Built Flutter UI: Developed the Dart code in lib/main.dart using the AndroidView widget to embed the native map.
  • Established Communication: Set up the MethodChannel on both the Dart and native sides to:
    • Send commands (zoom, style change, center) from Flutter buttons to the native map.
    • Receive a mapReady signal from the native map to enable Flutter UI controls.

Get Support

If you encounter issues specifically related to the Trimble Maps SDK, this integration guide, or have further questions about using our SDK features, please don’t hesitate to reach out to our support team:

Further Reading

This guide focused on the fundamental setup for displaying a map using Flutter’s PlatformView on Android. The Trimble Maps SDK for Android offers a wide range of additional features not covered here, such as:

  • Displaying user location
  • Routing
  • Advanced camera controls
  • Custom map styling
  • Displaying custom data

To explore these features and gain a deeper understanding of the underlying native SDK capabilities, please refer to the comprehensive Trimble Maps SDK for Android - Getting Started Guide.

We encourage you to use the foundation built in this guide as a starting point for integrating more advanced map functionalities into your Flutter applications.

Happy mapping!

Last updated May 5, 2025.