Beta
Maps SDK for Android v10

Migrate to v10

The Mapbox Maps SDK v10 introduces improvements to how Mapbox works on the Android platform, as well as changes to how developers use the SDK. This document summarizes the most important changes and walks you through how to upgrade an application using a previous version of the Mapbox Maps SDK to v10.

Requirements

  • Minimum Android SDK version is now 19 (formerly 16).
  • Kotlin version 1.4.x.

Package name changes

Packagepre-v10v10
Maven group ID for mapcom.mapbox.mapboxsdkcom.mapbox.maps
Maven artifact ID for mapmapbox-android-sdkandroid
Maven group ID for pluginscom.mapbox.mapboxsdkcom.mapbox.plugin
Maven group ID for pluginscom.mapbox.mapboxsdkcom.mapbox.extension
Maven artifact ID for pluginsmapbox-android-plugin-PLUGINNAMEmaps-PLUGINNAME
Package name for mapscom.mapbox.mapboxsdk.mapscom.mapbox.maps
MapView class in the layoutcom.mapbox.mapboxsdk.maps.MapViewcom.mapbox.maps.MapView

Simplified lifecycle management

With the Maps SDK v10, managing activity lifecycle methods has been reduced by a factor of four. It is no longer required to hook into Activity#onCreate, Activity#onPause, Activity#onResume and Activity#onSaveInstanceState. Below is an example implementation of lifecycle management:

class SimpleMapActivity : AppCompatActivity() {

 override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_simple_map)
   mapView.getMapboxMap().loadStyleUri(Style.MAPBOX_STREETS)
 }

 override fun onStart() {
   super.onStart()
   mapView.onStart()
 }

 override fun onStop() {
   super.onStop()
   mapView.onStop()
 }

 override fun onLowMemory() {
   super.onLowMemory()
   mapView.onLowMemory()
 }

 override fun onDestroy() {
   super.onDestroy()
   mapView.onDestroy()
 }
}

Modular architecture

The Maps SDK v10 has a modular architecture to set the foundation for a plugin-based architecture that can be minimized or extended. Because of this architectural change, the SDK employs extension functions widely used to augment the map’s control class, MapboxMap. These extension functions must be imported. They will typically appear in the Android Studio autocomplete suggestions, but with lower priorities than the native functions inside the class.

Import the Maps SDK, including all packaged plugins as:

implementation "com.mapbox.maps:android:10.0.0-beta.12"

Example: Replace modular HTTP stack

The SDK allows for swapping certain components for a customized implementation. For example, the HTTP stack can be swapped out by:

  1. Removing the default HTTP stack bundled with the SDK and include Mapbox Annotation processor dependency:

    plugins {
        ...
        kotlin("kapt")
    }
    
    dependencies {
        ...
        implementation('com.mapbox.maps:android:10.0.0-beta.12') {
            exclude group: 'com.mapbox.common', module: 'okhttp'
        }
        implementation('androidx.annotation:annotation:${version.androidX}')
        compileOnly('com.mapbox.base:annotations:${version.mapboxBaseAndroid}')
        kapt('com.mapbox.base:annotations-processor:${versions.mapboxBaseAndroid}')
    }
    
  2. Provide a custom HTTP Client class and let it implement com.mapbox.common.HttpServiceInterface and annotate the class with @MapboxModule(type = MapboxModuleType.CommonHttpClient):

    @MapboxModule(type = MapboxModuleType.CommonHttpClient)
    class CustomizedHttpService : HttpServiceInterface() {
        ...
    
        override fun setMaxRequestsPerHost(max: Byte) {
            TODO("Not yet implemented")
        }
    
        override fun request(request: HttpRequest, callback: HttpResponseCallback): Long {
            TODO("Not yet implemented")
        }
    
        override fun cancelRequest(id: Long, callback: ResultCallback) {
            TODO("Not yet implemented")
        }
    
        override fun supportsKeepCompression(): Boolean {
            TODO("Not yet implemented")
        }
    
        override fun download(options: DownloadOptions, callback: DownloadStatusCallback): Long {
            TODO("Not yet implemented")
        }
    
    }
    
  3. Implement the abstract functions.

Access token configuration

The pre-v10 required configuration of the access token through the Mapbox.getInstance API. With v10, the singleton Mapbox object has been removed and access token management is done either with a MapView XML attribute (mapbox_resourcesAccessToken), a predefined string identifier resource named mapbox_access_token, or programmatically with MapboxMapOptions when creating a MapView.

A unified configuration can be implemented using MapboxOptions#setDefaultResourceOptions:

/**
* Override the default used resource options given a context and access token.
* This function allows you to use standard configuration, Context is used to look up
* the app specific directories. Access token is used to authenticate with Mapbox API endpoints.
*/
fun setDefaultResourceOptions(context: Context, accessToken: String): ResourceOptions {
 defaultOptions = createResourceOptions(accessToken, context.filesDir.absolutePath)
 return defaultOptions
}

/**
* Override the default used resource options given a [ResourceOptions] configuration object.
* This allows to have full control over the location of the database.
*/
fun setDefaultResourceOptions(resourceOptions: ResourceOptions) {
 defaultOptions = resourceOptions
}

Map styles

Synchronized getMapboxMap()

In the Maps SDK v10, you can safely access MapboxMap from the MapView in a synchronized manner. You are no longer required to wait for the map to become ready before calling functions on the MapboxMap:

pre-v10:

mapView.getMapAsync { mapboxMap ->
  mapboxMap.setStyle(Style.MAPBOX_STREETS) { style ->
    // Map is set up and the style has loaded. Now you can add data or make other map adjustments.
  }
}

v10:

mapView.getMapboxMap().loadStyleUri(Style.MAPBOX_STREETS) { style ->
  // Map is set up and the style has loaded. Now you can add data or make other map adjustments.
}

Style loading functions

The setStyle API has been split into multiple functions with more descriptive names:

FunctionDescription
loadStyleUri(styleUri: String)Load a new map style asynchronous from the specified URI.
loadStyleJson(styleJson: String)Load style from a JSON string.
loadStyle(style: StyleExtension)Load the style from the style extension provided DSL.

Style DSL

The Style domain-specific language (DSL) is a collection of functions that allows composition of a style in a block that is applied to a receiver class. In the Maps SDK v10, the style API more closely matches the Mapbox Style Specification.

In v10, DSL functions for layers, sources, light, and expressions are provided to construct the instances. The DSL function names match the class name being created, but the first character in the name is lowercase. In cases where there are mandatory constructor parameters of the class, assign the mandatory constructor parameters before the code block. Inside the DSL block, use code completion in Android Studio to find all available receiver functions.

With the Style DSL, authoring or updating map styles is more like writing style JSON directly. The higher level style API is exposed as DSL, allowing construction of a StyleExtension object using the same paradigm and using the overloaded operator + inside the Style DSL closure to add layers, sources, or light to the StyleExtension.

Below is an example using the Style DSL to add a GeoJsonSource and a circle layer:

mapView.getMapboxMap().loadStyle(
  style(styleUri = Style.TRAFFIC_DAY) {
    +geoJsonSource(id = "earthquakes") {
      url(GEOJSON_URL)
      cluster(false)
    }
    +circleLayer(layerId = "earthquakeCircle", sourceId = "earthquakes") {
      circleRadius(get { literal("mag") })
      circleColor(Color.RED)
      circleOpacity(0.3)
      circleStrokeColor(Color.WHITE)
    }
  },
  object : Style.OnStyleLoaded {
    override fun onStyleLoaded(style: Style) {
      // Map is set up and the style has loaded. Now you can add data or make other map adjustments.
    }
  },
  object : OnMapLoadErrorListener {
    override fun onMapLoadError(mapViewLoadError: MapLoadError, msg: String) {
      // Error occurred when loading the map, try to handle it gracefully here
    }
  }
)

Styles parity with pre-v10 Maps SDK

The APIs for layer, source, and light are flattened, meaning you can find all the operations and properties directly under the Layer, Source and Light classes themselves. There are no more nested options like Layer.setProperties() and GeoJsonOptions. This makes the APIs safe because you cannot assign a property that isn’t supported by the layer with the new style API.

Below is a comparison of v10 and pre-v10 style APIs in specific use cases.

PropertyFactory deprecation

Instead of PropertyFactory, compatible properties are exposed directly inside the layer as functions with strongly typed value parameters.

pre-v10:

SymbolLayer stretchLayer = new SymbolLayer(STRETCH_LAYER, STRETCH_SOURCE)
    .withProperties(
        textField(get("name")),
        iconImage(get("image-name")),
        iconAllowOverlap(true),
        textAllowOverlap(true),
        iconTextFit(ICON_TEXT_FIT_BOTH));

v10:

val stretchLayer = symbolLayer(STRETCH_LAYER, STRETCH_SOURCE) {
    textField(get { literal("name") })
    iconImage(get { literal("image-name") })
    iconAllowOverlap(true)
    textAllowOverlap(true)
    iconTextFit(IconTextFit.BOTH)
}

StringDef deprecation

The StringDef static property values have been replaced by enum class.

Use Double instead of Float

The Float type properties in the style APIs are replaced with the more precise type, Double.

Use expressions

To better utilize the Style DSL and to be able to use the nested expression DSL, all constant values inside the expressions are treated as literal expressions and have to be wrapped inside the literal expression.

Expressions can be constructed using DSL as well:

pre-v10:

heatmapWeight(
        interpolate(
          linear(), get("mag"),
          stop(0, 0),
          stop(6, 1)
        )
      )

v10:

heatmapWeight(
        interpolate {
          linear()
          get { literal("mag") }
          stop {
            literal(0)
            literal(0)
          }
          stop {
            literal(6)
            literal(1)
          }
        }
      )

Update a single style property

After a layer, source, and light is added to the style, any change to the layer’s property will be passed through style and be shown on the Map.

pre-v10:

circleLayer.setProperties(
    circleColor(Color.RED)
);

v10:

circleLayer.circleColor(Color.RED)

Retrieve style properties from the map

After the layer, source, and light is added to the style, get the current style properties using the getter function directly exposed to the Layer, Source, and Light classes.

In most cases, there will be two getter functions of the property: get{PropertyName} : {PropertyType}? and get{PropertyName}AsExpression : Expression?. The first will return the property in its own type, and the second will return a property as an Expression (constant values will be wrapped into a literal expression).

Float types are replaced with Double in the v10 style APIs.

pre-v10:

val radiusPropertyValue: PropertyValue<Float> = circleLayer.getCircleRadius()
val radius: Float = radius.getValue()

v10:

val radius: Double = circleLayer.circleRadius!!

Changes to sources

Sources share the same API structure: all source properties mutators are available on the Source class. After a source is added to the style, changes to the source properties are passed to the style and reflected on the map directly. Setting the source properties can be done through methods such as url(), geometry(), feature(), or featureCollection().

val geoJsonSource = geoJsonSource(id = "earthquakes") {
      featureCollection(FEATURE_COLLECTION)
      cluster(false)
      clusterRadius(50L)
}

Annotations

With the Maps SDK v10, the legacy annotations have been replaced with the plugin annotation of v9.x.x. Most of the plugin APIs in v10 are the same as those in pre-v10, the main difference is the way of getting AnnotationManagers. Instead of creating an AnnotationManger directly like you did in pre-v10, in v10 an Annotation plugin instance must be created first.

pre-v10:

val circleManager = new CircleManager(mapView, mapboxMap, style);

v10:

val annotationPlugin = mapView.getAnnotationPlugin()
val circleManager = annotationPlugin.getCircleManager()

In pre-v10, AnnotationManager needed to be recycled in the onDestroy method of the host activity. In v10, there is no need to do it manually because AnnotationManager will be cleaned automatically along with the host, AnnotationPlugin.

In v10, all the LatLng related methods and parameters are replaced with Point. Remember that the latitude and longitude parameters are reversed between LatLng and Point.

pre-v10:

CircleOptions circleOptions = new CircleOptions()
 .withLatLng(new LatLng(6.687337, 0.381457))

v10:

val circleOptions: CircleOptions = CircleOptions()
 .withPoint(Point.fromLngLat(0.381457, 6.687337))

New features and improvements

Platform-driven animation system

A new camera animation system leverages the Android animator framework and provides two sets of camera APIs: high-level animataion APIs and low-level animation APIs.

High-level animation APIs

High-level APIs provide parity with pre-v10 animations and cater to most typical use cases. In v10, customization options have been expanded. The Maps SDK v10 also leverages Android SDK listeners and time interpolators, delivering results that are more performant and implementations that are more convenient to use and enhance.

Important API changes:

  • flyTo replaces animateCamera.
  • easeTo replaces easeCamera.

pre-v10:

public final void easeCamera(
 @NonNull CameraUpdate update,
 int durationMs,
 @Nullable final MapboxMap.CancelableCallback callback
)

public final void animateCamera(
 @NonNull CameraUpdate update,
 final int durationMs
 @Nullable MapboxMap.CancelableCallback callback
)

v10:

MapboxMap.flyTo(
 cameraOptions: CameraOptions,
 animationOptions: MapAnimationOptions? = null
)

MapboxMap.easeTo(
 cameraOptions: CameraOptions,
 animationOptions: MapAnimationOptions? = null
)

// additional camera functions

MapboxMap.rotateBy(
 first: ScreenCoordinate,
 second: ScreenCoordinate,
 animationOptions: MapAnimationOptions? = null
)

MapboxMap.pitchBy(
 pitch: Double,
 animationOptions: MapAnimationOptions? = null
)

MapboxMap.scaleBy(
 amount: Double,
 screenCoordinate: ScreenCoordinate?,
 animationOptions: MapAnimationOptions? = null
)

MapboxMap.moveBy(
 screenCoordinate: ScreenCoordinate,
 animationOptions: MapAnimationOptions? = null
)

// MapAnimationOptions class definition

class MapAnimationOptions(
 val owner: String?,
 val duration: Long?,
 val interpolator: TimeInterpolator?,
 val animatorListener: Animator.AnimatorListener?
)

Low-level animation APIs

The Maps SDK v10 also introduces a new set of lower-level animation APIs. Each camera property can be animated independently through the Android SDK animator framework.

The following example constructs three independent animations (bearing, zoom, and pitch) and creates an animation set so they execute simultaneously. The animation can be customized by utilizing the flexible low-level APIs.

import com.mapbox.maps.plugin.animation.CameraAnimatorOptions.Companion.cameraAnimatorOptions
import com.mapbox.maps.plugin.animation.getCameraAnimationsPlugin

val plugin = mapView.getCameraAnimationsPlugin()
val bearing = plugin.createBearingAnimator(cameraAnimatorOptions(0.0, 160.0)) {
 duration = 8500
 interpolator = AnticipateOvershootInterpolator()
}
val zoom = plugin.createZoomAnimator(
 cameraAnimatorOptions(18.0) {
   startValue = 15.0
 }
) {
 duration = 5000
 interpolator = AccelerateDecelerateInterpolator()
}
val pitch = plugin.createPitchAnimator(
 cameraAnimatorOptions(55.0) {
   startValue = 0.0
 }
) {
 duration = 7000
 interpolator = BounceInterpolator()
}

plugin.registerAnimators(bearing, zoom, pitch)

val animatorSet = AnimatorSet()
animatorSet.startDelay = 5000
animatorSet.playTogether(bearing, zoom, pitch)
animatorSet.addListener(object : AnimatorListenerAdapter() {
   override fun onAnimationEnd(animation: Animator?) {
     super.onAnimationEnd(animation)
     plugin.unregisterAnimators(bearing, pitch, zoom)
   }
})
animatorSet.start()

Location component plugin with puck and camera decoupled

With v10, we have introduced the location component plugin for managing the location indicator decoupled from camera updates. The location component plugin will only handle the location puck updates; tracking modes are removed. Camera updates can be manually handled and synced. We will be surfacing a plugin for this convenience soon.

3D model capabilities

The new location component plugin supports a new API to use a 3D model to visualize the end user’s location on the map. Under the hood, it uses ModelLayer and ModelSource.

mapView.getLocationComponentPlugin()?.enabled = true
mapView.getLocationComponentPlugin()?.locationPuck = LocationPuck3D(
  modelUri = "asset://race_car_model.gltf",
  modelScale = listOf(0.1f, 0.1f, 0.1f)
)

The LocationModelLayer options can be configured by:

data class LocationPuck3D(
  /**
   * An URL for the model file in gltf format.
   */
  var modelUri: String,
  /**
   * The scale of the model.
   */
  var position: List<Float> = listOf(0f, 0f),
  /**
   * The opacity of the model.
   */
  var modelOpacity: Float = 1f,
  /**
   * The scale of the model.
   */
  var modelScale: List<Float> = listOf(1f, 1f, 1f),
  /**
   * The scale expression of the model, which will overwrite the default scale expression that keeps the model size constant during zoom.
   */
  var modelScaleExpression: String? = null,
  /**
   * The rotation of the model.
   */
  var modelRotation: List<Float> = listOf(0f, 0f, 90f),
) : LocationPuck()

CacheManager

A new interface for managing the device ambient cache exposes APIs to reset, clear, reconfigure, and pre-load data. Below is a snippet of the class definition:

/**
* Forces a revalidation of the tiles in the ambient cache and downloads
* a fresh version of the tiles from the tile server.
*
* This is more efficient than clearing the cache using \c clearAmbientCache()
* because tiles in the ambient cache are re-downloaded to remove outdated data
* from a device. It does not erase resources from the ambient cache or delete
* the database, which can be computationally expensive operations that may carry
* unintended side effects.
*
* @param callback Called once the request is complete or an error occurred.
*/
@Override
public native void invalidateAmbientCache(@NonNull CacheStatusCallback callback);

/**
* Sets the maximum size of the ambient cache in bytes.
*
* This call is potentially expensive because it will try to trim the data
* in case the database is larger than the size defined.
*
* Setting the size to 0 will effectively disable the cache.
*
* Preferably, this method should be called before using the database,
* otherwise the default maximum size will be used.
*
* @param size The maximum size of the ambient cache in bytes.
* @param callback Called once the request is complete or an error occurred.
*/
@Override
public native void setMaximumAmbientCacheSize(long size, @NonNull CacheStatusCallback callback);

/**
* Erase resources from the ambient cache, freeing storage space.
*
* This operation can be potentially slow Compared to \c invalidateAmbientCache()
* because it will trigger a VACUUM on SQLite, forcing the database to move pages
* on the filesystem.
*
* @param callback Called once the request is complete or an error occurred.
*/
@Override
public native void clearAmbientCache(@NonNull CacheStatusCallback callback);

/**
* Inserts the provided resource into the ambient cache.
*
* This method mimics the caching that would take place if the equivalent
* resources were requested in the process of map rendering. Use this method
* to preload the cache with resources you know will be requested.
*
* This method is asynchronous; the data may not be immediately available for
* in-progress requests, though subsequent requests should have access to the
* cached data.
*
* @param url The URL at which the data can normally be found.
* @param data Response data to store for this resource. The data is expected to be uncompressed;
* internally, the cache will compress data as necessary.
* @param expires The date after which the resource is no longer valid.
* @param httpEntityTag An HTTP entity tag.
* @param mustRevalidate A Boolean value indicating whether the data is still usable past the expiration date.
*/
@Override
public native void preloadData(@NonNull String url, @NonNull String data, @Nullable Date modified, @Nullable Date expires, @Nullable String httpEntityTag, boolean mustRevalidate);

/**
* Sets path of a database to be used by the ambient cache and invokes provided
* callback when a database path is set.
*
* @param dbPath The new database path
* @param callback Callback to call once the request is completed or an error occurred.
*/
@Override
public native void setDatabasePath(@NonNull String dbPath, @NonNull CacheStatusCallback callback);

/**
* Prefetches the resources for the from network and populates the ambient cache.
*
* @param area Map area to pre-fetch and put into cache
* @param callback Callback to call once the operation is complete or encounters an error.
* @return Returns the request id
*/
@Override
public native long prefetchAmbientCache(@NonNull CacheAreaDefinition cacheArea, @Nullable CachePrefetchCallback callback);

/**
* Cancels the prefetch request.
*
* If there is no request corresponding to the given id, or the request has been already
* finished, this call is ignored.
*
* @param id Request id
*/
@Override
public native void cancelPrefetchRequest(long id);

3D terrain and sky layers

In the Maps SDK v10, you can show dramatic elevation changes against an atmospheric backdrop by enabling 3D terrain and using the new sky layer.

3D Terrain can be applied to a DEM data source. Below is a code snippet of enabling terrain using Style DSL.

mapboxMap.loadStyle(
 styleExtension = style(Style.SATELLITE_STREETS) {
   +rasterDemSource("TERRAIN_SOURCE") {
     url("mapbox://mapbox.terrain-rgb")
     tileSize(512)
   }
   +terrain("TERRAIN_SOURCE") {
     exaggeration(1.1) 
   }
)
Note

3D terrain is still in an experimental state. It might not work as expected with the map camera animation system.

Asynchronous query rendered features

Getting a list of map features is now performed asynchronously and does not block the executing thread.

pre-v10:

public List<Feature> queryRenderedFeatures(@NonNull PointF coordinates, @Nullable String... layerIds)

v10:

void queryRenderedFeatures(@NonNull List<ScreenCoordinate> shape, @NonNull RenderedQueryOptions options, @NonNull QueryFeaturesCallback callback);

// where callback is executed on background thread
public interface QueryFeaturesCallback {
   void run(@NonNull Expected<List<Feature>, String> features);
}

Deprecations and removals

PropertyFactory

PropertyFactory has been deprecated in favor of direct property setter and getter APIs inside layers.

StringDef annotations

StringDef annotations have been replaced by enum classes.

Point and LatLng

LatLngBounds and LatLng has been removed in favor of coordinate bounds and point geometry.

Renaming tilt to pitch

tilt has been renamed to pitch throughout our codebase. For example, Camera#tilt is replaced by CameraOptions#Pitch.

CameraUpdateFactory

CameraUpdateFactory has been removed in favor of the new platform-driven camera animation system.

CameraPosition

CameraPosition has been replaced with CameraOptions.

getVisibleRegion

The function getVisibleRegion() is not available, but we have an equivalent implementation that can get the CoordinateBounds for the visible region.

val cameraPosition = mapView.getMapboxMap().getCameraOptions(null)
val bounds = mapView.getMapboxMap().coordinateBoundsForCamera(cameraPosition)

LegacyOfflineManager

OfflineManager has been deprecated and renamed to OfflineRegionManager.