Skip to Content
ExamplesAdd Vector Layer

MapsGL Android - Adding a custom vector tile layer

This example renders a custom vector tile layer on the map to draw water boundaries.

It is the Android port of the MapsGL JS Adding a custom vector tile layer example: one vector source for the tile endpoint, and one line layer that styles the source’s water layer. The complete activity ships in the MapsGL Android SDK demo  as AddVectorLayerActivity.

The JS version pulls its tiles from Nextzen , which needs an API key of its own. This uses Xweather’s OpenStreetMap-derived vector source instead — WeatherSource.base_osm — which carries the same water source layer and authenticates with the Xweather credentials you already have, so the example runs with nothing extra to sign up for. Everything downstream of the source is unchanged, including the #333333 stroke.

Coastlines across the North Atlantic outlined in dark grey lines over the base map

Set up the map

Create a MapboxMapController, position the camera, and wait for the map to load before adding the source and layer.

import com.mapbox.maps.MapboxMap import com.xweather.mapsgl.config.weather.account.XweatherAccount import com.xweather.mapsgl.map.mapbox.MapboxMapController import com.xweather.mapsgl.types.Coordinate val xweatherAccount = XweatherAccount( getString(R.string.xweather_client_id), getString(R.string.xweather_client_secret), ) val controller = MapboxMapController(mapView, xweatherAccount) val mapboxMap = controller.mapboxMap ?: return // Matches the JS example's `center: [-30.33207, 40.60621], zoom: 2`. controller.setCenter(Coordinate(40.60621, -30.33207)) controller.setZoom(2.0) val mapLoadedCancelable = mapboxMap.subscribeMapLoaded { // Add the source and layer here — see below. }

Add the vector tile source

Vector tiles use a VectorSourceDescriptor. WeatherSource.base_osm returns one already configured with the tile URL and an authenticator, so there is no key to supply and no URL to assemble.

import com.xweather.mapsgl.sources.source.spec.VectorSourceDescriptor import com.xweather.mapsgl.weather.WeatherSource // Add the vector tile source. val waterSource: VectorSourceDescriptor = WeatherSource.base_osm(controller.service) if (!controller.hasSource(waterSource.id)) { controller.addSource(waterSource) }

For a third-party endpoint you would build the descriptor yourself instead, giving it a tile URL with {z}, {x}, and {y} templates — the JS example’s Nextzen source looks like this:

import com.xweather.mapsgl.types.TileSize VectorSourceDescriptor( id = "base", url = "https://tile.nextzen.org/tilezen/vector/v1/256/all/{z}/{x}/{y}.mvt?api_key=$nextzenKey", tileSize = TileSize(256), attribution = "© Nextzen, OpenStreetMap contributors", )

See custom data sources for tile URL templates and credential expansion.

Style the water layer

A vector tile carries several named layers; sourceLayer selects which one to draw. The JS example’s paint.stroke fields map one-to-one onto StrokePaint.

import androidx.compose.ui.graphics.Color import com.xweather.mapsgl.layers.spec.LineLayerDescriptor import com.xweather.mapsgl.style.LineCap import com.xweather.mapsgl.style.LineJoin import com.xweather.mapsgl.style.LineLayerPaint import com.xweather.mapsgl.style.StrokePaint import com.xweather.mapsgl.style.StyleValue val boundaryColor = Color(0xFF333333) // `#333333`, as in the JS example // Style the source's `water` layer as lines. if (!controller.hasLayer("boundaries")) { controller.addLayer( LineLayerDescriptor( id = "boundaries", source = waterSource.id, sourceLayer = "water", paint = LineLayerPaint( stroke = StrokePaint( color = StyleValue.Constant(boundaryColor), opacity = StyleValue.Constant(1.0), thickness = StyleValue.Constant(2.0), lineCap = StyleValue.Constant(LineCap.ROUND), lineJoin = StyleValue.Constant(LineJoin.ROUND), ), ), ), beforeID = null, ) }

lineCap and lineJoin take the LineCap and LineJoin enums rather than the JS string literals 'round'.

Clean up

An Android activity is torn down and recreated across configuration changes, so remove the layer and then the source it references, and cancel the map-loaded subscription, in onDestroy.

override fun onDestroy() { if (controller.hasLayer("boundaries")) controller.removeLayer("boundaries") sourceId?.let { if (controller.hasSource(it)) controller.removeSource(it) } mapLoadedCancelable?.cancel() mapView.onDestroy() super.onDestroy() }

Complete example

The layout is a single Mapbox MapView:

activity_add_vector_layer.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.mapbox.maps.MapView android:id="@+id/addVectorMapView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:mapbox_mapSurface="texture_view" /> </androidx.constraintlayout.widget.ConstraintLayout>

And the activity, which adds the source and layer once the map has loaded:

AddVectorLayerActivity.kt
package com.example.mapsgldemo.docExamples import android.os.Bundle import android.view.ViewTreeObserver import androidx.appcompat.app.AppCompatActivity import androidx.compose.ui.graphics.Color import com.example.mapsgldemo.R import com.example.mapsgldemo.databinding.ActivityAddVectorLayerBinding import com.mapbox.common.Cancelable import com.mapbox.maps.MapView import com.mapbox.maps.MapboxMap import com.mapbox.maps.extension.style.layers.properties.generated.ProjectionName import com.mapbox.maps.extension.style.projection.generated.projection import com.mapbox.maps.extension.style.projection.generated.setProjection import com.xweather.mapsgl.config.weather.account.XweatherAccount import com.xweather.mapsgl.layers.spec.LineLayerDescriptor import com.xweather.mapsgl.map.mapbox.MapboxMapController import com.xweather.mapsgl.sources.source.spec.VectorSourceDescriptor import com.xweather.mapsgl.style.LineCap import com.xweather.mapsgl.style.LineJoin import com.xweather.mapsgl.style.LineLayerPaint import com.xweather.mapsgl.style.StrokePaint import com.xweather.mapsgl.style.StyleValue import com.xweather.mapsgl.types.Coordinate import com.xweather.mapsgl.weather.WeatherSource class AddVectorLayerActivity : AppCompatActivity() { private lateinit var binding: ActivityAddVectorLayerBinding private lateinit var mapView: MapView private var mapboxMap: MapboxMap? = null private lateinit var controller: MapboxMapController private var mapLoadedCancelable: Cancelable? = null /** Id of the vector source, taken from the descriptor rather than hard-coded. */ private var sourceId: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityAddVectorLayerBinding.inflate(layoutInflater) setContentView(binding.root) mapView = binding.addVectorMapView val xweatherAccount = XweatherAccount( getString(R.string.xweather_client_id), getString(R.string.xweather_client_secret), ) mapView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { mapView.viewTreeObserver.removeOnGlobalLayoutListener(this) if (mapView.parent == null) return controller = MapboxMapController(mapView, xweatherAccount) mapboxMap = controller.mapboxMap // Matches the JS example's `center: [-30.33207, 40.60621], zoom: 2`. // Coordinate is (lat, lon), the reverse of the GeoJSON-style pair above. controller.setCenter(Coordinate(40.60621, -30.33207)) controller.setZoom(2.0) mapLoadedCancelable = mapboxMap?.subscribeMapLoaded { mapboxMap?.style?.setProjection(projection(ProjectionName.MERCATOR)) // Add the vector tile source. `base_osm` is Xweather's OpenStreetMap-derived // vector product; the descriptor it returns already carries the tile URL and // the authenticator, so no separate key is needed. val waterSource: VectorSourceDescriptor = WeatherSource.base_osm(controller.service) sourceId = waterSource.id if (!controller.hasSource(waterSource.id)) { controller.addSource(waterSource) } // Style the source's `water` layer as lines. if (!controller.hasLayer(LAYER_ID)) { controller.addLayer( LineLayerDescriptor( id = LAYER_ID, source = waterSource.id, sourceLayer = SOURCE_LAYER, paint = LineLayerPaint( stroke = StrokePaint( color = StyleValue.Constant(BOUNDARY_COLOR), opacity = StyleValue.Constant(1.0), thickness = StyleValue.Constant(2.0), lineCap = StyleValue.Constant(LineCap.ROUND), lineJoin = StyleValue.Constant(LineJoin.ROUND), ), ), ), beforeID = null, ) } } } }) } override fun onStart() { super.onStart() mapView.onStart() } override fun onStop() { mapView.onStop() super.onStop() } override fun onLowMemory() { super.onLowMemory() mapView.onLowMemory() } override fun onDestroy() { if (::controller.isInitialized) { if (controller.hasLayer(LAYER_ID)) controller.removeLayer(LAYER_ID) sourceId?.let { if (controller.hasSource(it)) controller.removeSource(it) } } mapLoadedCancelable?.cancel() mapLoadedCancelable = null mapView.onDestroy() super.onDestroy() } companion object { private const val LAYER_ID = "boundaries" /** The layer inside the vector tile to draw — the same name Nextzen uses. */ private const val SOURCE_LAYER = "water" /** `#333333`, as in the JS example. */ private val BOUNDARY_COLOR = Color(0xFF333333) } }
© 2026 Xweather (opens in a new tab)Terms of Service (opens in a new tab)Privacy Policy (opens in a new tab)