Skip to Content
ExamplesAdd Geojson Layer

MapsGL Android - Adding custom GeoJSON data

This example renders a static GeoJSON object consisting of a polygon feature to the map. Two separate style layers are associated with the data source: one that fills the geometry and another that draws a thicker outline along the geometry edges.

It is the Android port of the MapsGL JS Adding custom GeoJSON data example and renders the same Portugal polygon. The complete activity ships in the MapsGL Android SDK demo  as AddGeoJsonLayerActivity, reachable from the demo’s Docs Examples menu.

The Portugal polygon on the map, filled semi-transparent red with a thicker red outline

Set up the map

Create a MapboxMapController for the MapView, position the camera, and wait for the map to finish loading before adding sources and layers.

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: [-8, 40], zoom: 5`. controller.setCenter(Coordinate(40.0, -8.0)) controller.setZoom(5.0) val mapLoadedCancelable = mapboxMap.subscribeMapLoaded { // Add the source and layers here — see below. }

subscribeMapLoaded is the Android equivalent of the JS example’s controller.on('load', …) block. Keep the returned Cancelable so it can be cancelled in onDestroy.

Add the GeoJSON source

Assign the features to GeoJSONSourceDescriptor.data and register the descriptor with MapController.addSource. This is the direct equivalent of the JS example’s addSource('country-region', { type: 'geojson', data: … }).

import com.mapbox.geojson.FeatureCollection import com.xweather.mapsgl.sources.source.spec.GeoJSONSourceDescriptor // Add a custom polygon as GeoJSON data. if (!controller.hasSource("country-region")) { controller.addSource( GeoJSONSourceDescriptor(id = "country-region").apply { data = FeatureCollection.fromJson(COUNTRY_REGION_GEOJSON) }, ) }

COUNTRY_REGION_GEOJSON is the same single-polygon FeatureCollection the JS example embeds — see the GeoJSON data below.

You can also add the source first and assign data on the runtime GeoJSONSource afterwards. That is the right choice when the features arrive later than the source does.

import com.xweather.mapsgl.sources.GeoJSONSource controller.addSource(GeoJSONSourceDescriptor(id = "country-region")) (controller.getSource("country-region") as? GeoJSONSource)?.data = FeatureCollection.fromJson(COUNTRY_REGION_GEOJSON)

See custom data sources for the remote-URL and bundled-asset variants.

Fill the polygon region

Add a FillLayerDescriptor that references the source id. Style values are wrapped in StyleValue.Constant, the Kotlin equivalent of the JS example’s literal paint values.

import androidx.compose.ui.graphics.Color import com.xweather.mapsgl.layers.spec.FillLayerDescriptor import com.xweather.mapsgl.style.FillLayerPaint import com.xweather.mapsgl.style.FillPaint import com.xweather.mapsgl.style.StyleValue val regionColor = Color(0xFFFF0000) // `#ff0000`, as in the JS example // Fill the polygon region. if (!controller.hasLayer("region-fill")) { controller.addLayer( FillLayerDescriptor( id = "region-fill", source = "country-region", paint = FillLayerPaint( fill = FillPaint( color = StyleValue.Constant(regionColor), opacity = StyleValue.Constant(0.5), ), ), ), beforeID = null, ) }

Add a thicker outline

Because sources and layers are created and cached separately, the same source id can back a second layer. Add a LineLayerDescriptor for the outline.

import com.xweather.mapsgl.layers.spec.LineLayerDescriptor import com.xweather.mapsgl.style.LineLayerPaint import com.xweather.mapsgl.style.StrokePaint // Add a thicker outline to the region. if (!controller.hasLayer("region-outline")) { controller.addLayer( LineLayerDescriptor( id = "region-outline", source = "country-region", paint = LineLayerPaint( stroke = StrokePaint( color = StyleValue.Constant(regionColor), thickness = StyleValue.Constant(5.0), ), ), ), beforeID = null, ) }

Clean up

Unlike a browser page, an Android activity is torn down and recreated across configuration changes. Remove the layers and then the source they reference, and cancel the map-loaded subscription, in onDestroy.

override fun onDestroy() { if (controller.hasLayer("region-outline")) controller.removeLayer("region-outline") if (controller.hasLayer("region-fill")) controller.removeLayer("region-fill") if (controller.hasSource("country-region")) controller.removeSource("country-region") mapLoadedCancelable?.cancel() mapView.onDestroy() super.onDestroy() }

The GeoJSON data

The polygon is the same FeatureCollection used by the JS example, stored as a raw Kotlin string:

private const val COUNTRY_REGION_GEOJSON = """ { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "type": "Sovereign country", "name": "Portugal", "continent": "Europe", "region_un": "Europe", "subregion": "Southern Europe", "region_wb": "Europe & Central Asia" }, "geometry": { "type": "Polygon", "coordinates": [ [ [-9.034817674180246, 41.88057058365967], [-8.67194576662672, 42.13468943945496], [-8.263856980817792, 42.28046865495034], [-8.013174607769912, 41.790886135417125], [-7.422512986673795, 41.79207469335983], [-7.251308966490824, 41.91834605566505], [-6.668605515967656, 41.883386949219584], [-6.389087693700915, 41.381815497394655], [-6.851126674822552, 41.11108266861753], [-6.864019944679385, 40.33087189387483], [-7.026413133156595, 40.184524237624245], [-7.066591559263529, 39.71189158788277], [-7.498632371439725, 39.62957103124181], [-7.098036668313128, 39.03007274022378], [-7.374092169616318, 38.37305858006492], [-7.029281175148796, 38.07576406508977], [-7.166507941099865, 37.803894354802225], [-7.537105475281024, 37.42890432387623], [-7.453725551778092, 37.09778758396607], [-7.855613165711985, 36.83826854099627], [-8.382816127953689, 36.97888011326246], [-8.898856980820327, 36.86880931248078], [-8.746101446965554, 37.65134552667661], [-8.839997524439879, 38.26624339451761], [-9.287463751655224, 38.3584858261586], [-9.526570603869715, 38.73742910415491], [-9.446988898140232, 39.39206614842837], [-9.048305223008427, 39.75509308527877], [-8.977353481471681, 40.15930613866581], [-8.768684047877102, 40.76063894303019], [-8.79085323733031, 41.18433401139126], [-8.99078935386757, 41.54345937760364], [-9.034817674180246, 41.88057058365967] ] ] } } ] } """

Complete example

The layout is a single Mapbox MapView:

activity_add_geojson_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/addGeojsonMapView" 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 both layers once the map has loaded:

AddGeoJsonLayerActivity.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.ActivityAddGeojsonLayerBinding import com.mapbox.common.Cancelable import com.mapbox.geojson.FeatureCollection 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.FillLayerDescriptor import com.xweather.mapsgl.layers.spec.LineLayerDescriptor import com.xweather.mapsgl.map.mapbox.MapboxMapController import com.xweather.mapsgl.sources.source.spec.GeoJSONSourceDescriptor import com.xweather.mapsgl.style.FillLayerPaint import com.xweather.mapsgl.style.FillPaint import com.xweather.mapsgl.style.LineLayerPaint import com.xweather.mapsgl.style.StrokePaint import com.xweather.mapsgl.style.StyleValue import com.xweather.mapsgl.types.Coordinate class AddGeoJsonLayerActivity : AppCompatActivity() { private lateinit var binding: ActivityAddGeojsonLayerBinding private lateinit var mapView: MapView private var mapboxMap: MapboxMap? = null private lateinit var controller: MapboxMapController private var mapLoadedCancelable: Cancelable? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityAddGeojsonLayerBinding.inflate(layoutInflater) setContentView(binding.root) mapView = binding.addGeojsonMapView 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: [-8, 40], zoom: 5`. Coordinate is (lat, lon). controller.setCenter(Coordinate(40.0, -8.0)) controller.setZoom(5.0) mapLoadedCancelable = mapboxMap?.subscribeMapLoaded { mapboxMap?.style?.setProjection(projection(ProjectionName.MERCATOR)) // Add the custom polygon as GeoJSON data. if (!controller.hasSource(SOURCE_ID)) { controller.addSource( GeoJSONSourceDescriptor(id = SOURCE_ID).apply { data = FeatureCollection.fromJson(COUNTRY_REGION_GEOJSON) }, ) } // Fill the polygon region. if (!controller.hasLayer(FILL_LAYER_ID)) { controller.addLayer( FillLayerDescriptor( id = FILL_LAYER_ID, source = SOURCE_ID, paint = FillLayerPaint( fill = FillPaint( color = StyleValue.Constant(REGION_COLOR), opacity = StyleValue.Constant(0.5), ), ), ), beforeID = null, ) } // Add a thicker outline to the region. if (!controller.hasLayer(OUTLINE_LAYER_ID)) { controller.addLayer( LineLayerDescriptor( id = OUTLINE_LAYER_ID, source = SOURCE_ID, paint = LineLayerPaint( stroke = StrokePaint( color = StyleValue.Constant(REGION_COLOR), thickness = StyleValue.Constant(5.0), ), ), ), 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(OUTLINE_LAYER_ID)) controller.removeLayer(OUTLINE_LAYER_ID) if (controller.hasLayer(FILL_LAYER_ID)) controller.removeLayer(FILL_LAYER_ID) if (controller.hasSource(SOURCE_ID)) controller.removeSource(SOURCE_ID) } mapLoadedCancelable?.cancel() mapLoadedCancelable = null mapView.onDestroy() super.onDestroy() } companion object { private const val SOURCE_ID = "country-region" private const val FILL_LAYER_ID = "region-fill" private const val OUTLINE_LAYER_ID = "region-outline" /** `#ff0000`, as in the JS example. */ private val REGION_COLOR = Color(0xFFFF0000) // The FeatureCollection shown in "The GeoJSON data" above. private const val COUNTRY_REGION_GEOJSON = """…""" } }
© 2026 Xweather (opens in a new tab)Terms of Service (opens in a new tab)Privacy Policy (opens in a new tab)