> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/help/ja/llms.txt)

# Build an android marker app using GeoJSON data

This tutorial shows you how to build a map-based coffee shop finder for Android, step by step. You'll load location data from a [GeoJSON](https://docs.mapbox.com/help/ja/glossary/geojson/) file, add custom markers to the map, and add a slide-up bottom sheet that shows the details of any tapped location. Along the way you'll also polish the experience with a selected-marker state and a floating title card.

This is a common and solid pattern for map-based apps: the Mapbox Maps SDK renders your map using the [Mapbox Standard Style](https://docs.mapbox.com/map-styles/standard/guides/), a full-featured 3D global basemap that handles all the cartography, so you can focus on your own data. Your point data is loaded at runtime and layered on top — keeping your map content separate and easier to update.

Your final app will look like the video below — markers over coffee shops in Providence, a slide-up card on tap, and an enlarged selected marker:

![Tapping on marker and rendering a bottom sheet with info about the coffee shop](https://docs.mapbox.com/help/ja/assets/ideal-img/android-geojson-final-demo.480.gif)

## Prerequisites

To follow along with this guide you'll need:

-   **Familiarity with Android development**: Beginner experience with Jetpack Compose, Kotlin, and Android Studio.
-   **Mapbox account**: [Sign up](https://account.mapbox.com/auth/signup/) for free or [log in](https://console.mapbox.com/) if you already have an account.
-   **Android Studio**: The latest version of [Android Studio](https://developer.android.com/studio/install) with Gradle

> **Note: Accessing the Full Sample Code**
> 
> If you wish to copy the full code snippet in its entirety instead of following along with the tutorial, view the [full code snippet here](https://docs.mapbox.com/help/ja/tutorials/android-markers-from-geojson/?step=6).
> 
> **If you only copy the snippet, there will be two missing dependencies. Make sure to download and add the `XML` and `GeoJSON` files from Step 2 below before running the app.**

## Follow the Maps SDK for Android install guide

Before starting this tutorial, it's recommended to follow the [Get Started with the Maps SDK for Android](https://docs.mapbox.com/android/maps/guides/install/).

This getting started guide will show you how to configure your credentials, add the Mapbox dependency to your project and add a map to your application.

Once you are able to successfully render a map, you can move onto the next step of this tutorial.

## Add markers to the map

This step adds custom markers to the map by reading location data from a [GeoJSON](https://docs.mapbox.com/help/ja/glossary/geojson/) file at runtime.

A [`PointAnnotation`](https://docs.mapbox.com/android/maps/api/latest/mapbox-maps-android/com.mapbox.maps.plugin.annotation.generated/-point-annotation/) is the Mapbox primitive for placing an icon at a fixed map coordinate. Each annotation takes a `Point` (longitude/latitude) and an `iconImage`, which is a drawable resource registered with Mapbox using `rememberIconImage`. To learn more about other annotation types, see the [Annotation Guide](https://docs.mapbox.com/android/maps/guides/annotations/annotations/#default-markers).

The GeoJSON file contains a `FeatureCollection` — an array of `Feature` objects. Each `Feature` has a `geometry` (the coordinates) and a `properties` object (the data about that location). Here's what one feature looks like:

```json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [-71.41547, 41.821369]
      },
      "properties": {
        "name": "Coffee La France",
        "address": "73 Empire St, Providence, RI 02903",
        "phone": "(401) 454-3380"
      }
    }
  ]
}
```

The code reads the file once using a `remember { }` block (so it isn't re-read on every Compose recomposition), parses it into a `FeatureCollection`, then iterates over each `Feature` with a `forEach` loop to place a `PointAnnotation` at its coordinates.

The map is also wrapped in a `Box` — a Compose layout that lets you stack composables on top of each other. You'll use it to overlay the bottom sheet and title card in later steps. `enableEdgeToEdge()` extends the map behind the system bars for a full-screen experience, and the logo/attribution are nudged upward using `WindowInsets.systemBars` so they aren't hidden behind the navigation bar.

Follow these steps:

1.  Download the marker icon by clicking the button below and add the `XML` file to **app > res > drawable** in your project.

[Download XML](https://docs.mapbox.com/help/ja/help/data/ic_blue_marker.xml)

2.  Download the GeoJSON data file and add it to your project's assets folder. In Android Studio, right-click the **app** folder and select **New > Folder > Assets Folder**, then drag the file in.

[Download GeoJSON](https://docs.mapbox.com/help/ja/help/data/coffee_shops.geojson)

3.  Replace the contents of `MainActivity.kt` with the code below (*keeping your package import line on line 1*).
4.  Save and run the simulator.

![How to add assets folder to your android project](https://docs.mapbox.com/help/ja/assets/ideal-img/android-geojson-step-5-add-assets-folder.7481b14.480.png) Details

**Troubleshooting**

-   Make sure you don't delete your package import line. It should be at the top of `MainActivity.kt` written as `package com.example.INSERT-PROJECT-NAME-HERE`.
-   If you cannot drag a file into the editor, add it through your file explorer by going to `PROJECTFOLDER/app/src/main/res/drawable` for the XML or `PROJECTFOLDER/app/src/main/assets` for the GeoJSON.

Title: `MainActivity.kt`

```kotlin
package com.example.androidgeojson

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Point
import com.mapbox.maps.extension.compose.MapboxMap
import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
import com.mapbox.maps.extension.compose.annotation.generated.PointAnnotation
import com.mapbox.maps.extension.compose.annotation.rememberIconImage

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {

            // highlight-start
            val markerImage = rememberIconImage(
                key = R.drawable.ic_blue_marker,
                painter = painterResource(R.drawable.ic_blue_marker)
            )
            // highlight-end

            Box(Modifier.fillMaxSize()) {
                MapboxMap(
                    Modifier.fillMaxSize(),
                    mapViewportState = rememberMapViewportState {
                        setCameraOptions {
                            zoom(14.0)
                            center(Point.fromLngLat(-71.41547, 41.821369))
                            pitch(0.0)
                            bearing(0.0)
                        }
                    },
                    logo = {
                        Logo(Modifier.padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
                    },
                    attribution = {
                        Attribution(Modifier.padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
                    },
                    scaleBar = {}
                ) {
                    // highlight-start
                    val featureCollection = remember {
                        val geoJson = assets.open("coffee_shops.geojson")
                            .bufferedReader()
                            .use { it.readText() }
                        FeatureCollection.fromJson(geoJson)
                    }

                    featureCollection.features()?.forEach { feature ->
                        val geometry = feature.geometry()
                        if (geometry is Point) {
                            PointAnnotation(point = geometry) {
                                iconImage = markerImage
                            }
                        }
                    }
                    // highlight-end
                }
            }
        }
    }
}
```

You should now see all the coffee shop markers across Providence on a full-screen map.

![Map showing custom markers for coffee shops in Providence, Rhode Island](https://docs.mapbox.com/help/ja/assets/ideal-img/android-geojson-step-4-markers.57732be.480.png)

## Add a bottom sheet

With the markers rendering, the next step is to make them interactive. Tapping a marker will slide up a card from the bottom of the screen showing the coffee shop's name, address, and phone number. You'll use Compose's [`AnimatedVisibility`](https://developer.android.com/develop/ui/compose/animation/composables-modifiers#animatedvisibility) to handle the enter and exit animation automatically.

Here's what this step adds:

1.  **Four state variables** at the top of `setContent`: `showBottomSheet` controls whether the sheet is visible; `locationName`, `locationAddress`, and `locationPhoneNumber` hold the data for the selected marker.
2.  **Property extraction** inside the `forEach` loop — reads `name`, `address`, and `phone` from each feature's GeoJSON properties object.
3.  **`interactionsState.onClicked`** inside `PointAnnotation` — when a marker is tapped, this block writes the shop's data into the state variables and sets `showBottomSheet = true`. Returning `true` consumes the event so it doesn't also fire the map's own click listener.
4.  **The `AnimatedVisibility` block** placed after `MapboxMap` inside the `Box` — renders a rounded `Card` with a `Column` showing the shop data. It slides in from the bottom when `showBottomSheet` is `true` and slides back out when it becomes `false`.

Replace the contents of `MainActivity.kt` with the code below. The highlighted sections show every addition from the previous step.

Title: `MainActivity.kt`

```kotlin
package com.example.androidgeojson

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Point
import com.mapbox.maps.extension.compose.MapboxMap
import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
import com.mapbox.maps.extension.compose.annotation.generated.PointAnnotation
import com.mapbox.maps.extension.compose.annotation.rememberIconImage

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {

            // highlight-start
            var showBottomSheet by remember { mutableStateOf(false) }
            var locationName by remember { mutableStateOf("") }
            var locationAddress by remember { mutableStateOf("") }
            var locationPhoneNumber by remember { mutableStateOf("") }
            // highlight-end

            val markerImage = rememberIconImage(
                key = R.drawable.ic_blue_marker,
                painter = painterResource(R.drawable.ic_blue_marker)
            )

            Box(Modifier.fillMaxSize()) {
                MapboxMap(
                    Modifier.fillMaxSize(),
                    mapViewportState = rememberMapViewportState {
                        setCameraOptions {
                            zoom(14.0)
                            center(Point.fromLngLat(-71.41547, 41.821369))
                            pitch(0.0)
                            bearing(0.0)
                        }
                    },
                    logo = {
                        Logo(Modifier.padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
                    },
                    attribution = {
                        Attribution(Modifier.padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
                    },
                    scaleBar = {}
                ) {
                    val featureCollection = remember {
                        val geoJson = assets.open("coffee_shops.geojson")
                            .bufferedReader()
                            .use { it.readText() }
                        FeatureCollection.fromJson(geoJson)
                    }

                    featureCollection.features()?.forEach { feature ->
                        val geometry = feature.geometry()
                        if (geometry is Point) {
                            // highlight-start
                            val properties = feature.properties()
                            val jsonObjectStoreName = properties?.get("name")?.asString
                            val jsonObjectAddress = properties?.get("address")?.asString
                            val jsonObjectPhoneNumber = properties?.get("phone")?.asString
                            // highlight-end

                            PointAnnotation(point = geometry) {
                                iconImage = markerImage
                                // highlight-start
                                interactionsState.onClicked {
                                    locationName = jsonObjectStoreName.orEmpty()
                                    locationAddress = jsonObjectAddress.orEmpty()
                                    locationPhoneNumber = jsonObjectPhoneNumber.orEmpty()
                                    showBottomSheet = true
                                    true
                                }
                                // highlight-end
                            }
                        }
                    }
                }

                // highlight-start
                AnimatedVisibility(
                    visible = showBottomSheet,
                    modifier = Modifier.align(Alignment.BottomCenter),
                    enter = slideInVertically(initialOffsetY = { it }),
                    exit = slideOutVertically(targetOffsetY = { it })
                ) {
                    Card(
                        modifier = Modifier.fillMaxWidth(),
                        shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
                        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
                        colors = CardDefaults.cardColors(containerColor = Color.White)
                    ) {
                        Column(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(horizontal = 24.dp, vertical = 16.dp)
                                .padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding())
                        ) {
                            Text(
                                text = "☕ $locationName",
                                style = MaterialTheme.typography.headlineSmall,
                                fontWeight = FontWeight.Bold
                            )
                            Spacer(modifier = Modifier.padding(vertical = 4.dp))
                            Text("📍 $locationAddress", style = MaterialTheme.typography.bodyLarge)
                            Text("📞 $locationPhoneNumber", style = MaterialTheme.typography.bodyLarge)
                            Spacer(modifier = Modifier.padding(vertical = 32.dp))
                        }
                    }
                }
                // highlight-end
            }
        }
    }
}
```

![Tapping on marker and rendering a bottom sheet with info about the coffee shop](https://docs.mapbox.com/help/ja/assets/ideal-img/android-geojson-step-5-modal-bottom-sheet.480.gif)

Tap any marker — the bottom sheet should slide up with the shop's name, address, and phone number. Tap another marker and the sheet updates with the new location's data.

## Add a selected marker state

The app is functional, but there's no visual feedback about which marker is selected, and the sheet stays open even when the user pans the map. This step adds both refinements.

Here's what changes:

1.  **`selectedPoint` state variable** — a nullable `Point` that tracks which marker you most recently tapped. If it matches the point being rendered in the `forEach` loop, the app draws that marker at 1.5× its normal size.
2.  **`iconSize` property** on `PointAnnotation` — set to `1.5` if the annotation's point equals `selectedPoint`, `1.0` otherwise. Compose re-renders the annotation automatically when `selectedPoint` changes.
3.  **`MapEffect` block** — `MapEffect` gives you direct access to the underlying `MapView` so you can attach native Mapbox listeners. Two listeners are registered here:
    -   `addOnMapClickListener` — fires when the user taps an empty area of the map, dismissing the sheet and clearing the selected marker. Returns `false` so the event continues to propagate.
    -   `subscribeCameraChanged` — fires on any camera movement (pan, pinch-zoom, fling, rotate), also dismissing the sheet and clearing the selection.

Replace the contents of `MainActivity.kt` with the code below. The highlighted sections show every addition from the previous step.

Title: `MainActivity.kt`

```kotlin
package com.example.androidgeojson

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Point
import com.mapbox.maps.extension.compose.MapEffect
import com.mapbox.maps.extension.compose.MapboxMap
import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
import com.mapbox.maps.extension.compose.annotation.generated.PointAnnotation
import com.mapbox.maps.extension.compose.annotation.rememberIconImage
import com.mapbox.maps.plugin.gestures.gestures

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {

            var showBottomSheet by remember { mutableStateOf(false) }
            var locationName by remember { mutableStateOf("") }
            var locationAddress by remember { mutableStateOf("") }
            var locationPhoneNumber by remember { mutableStateOf("") }
            // highlight-start
            var selectedPoint by remember { mutableStateOf<Point?>(null) }
            // highlight-end

            val markerImage = rememberIconImage(
                key = R.drawable.ic_blue_marker,
                painter = painterResource(R.drawable.ic_blue_marker)
            )

            Box(Modifier.fillMaxSize()) {
                MapboxMap(
                    Modifier.fillMaxSize(),
                    mapViewportState = rememberMapViewportState {
                        setCameraOptions {
                            zoom(14.0)
                            center(Point.fromLngLat(-71.41547, 41.821369))
                            pitch(0.0)
                            bearing(0.0)
                        }
                    },
                    logo = {
                        Logo(Modifier.padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
                    },
                    attribution = {
                        Attribution(Modifier.padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
                    },
                    scaleBar = {}
                ) {
                    val featureCollection = remember {
                        val geoJson = assets.open("coffee_shops.geojson")
                            .bufferedReader()
                            .use { it.readText() }
                        FeatureCollection.fromJson(geoJson)
                    }

                    featureCollection.features()?.forEach { feature ->
                        val geometry = feature.geometry()
                        if (geometry is Point) {
                            // highlight-start
                            val point = geometry
                            // highlight-end
                            val properties = feature.properties()
                            val jsonObjectStoreName = properties?.get("name")?.asString
                            val jsonObjectAddress = properties?.get("address")?.asString
                            val jsonObjectPhoneNumber = properties?.get("phone")?.asString

                            // highlight-start
                            PointAnnotation(point = point) {
                            // highlight-end
                                iconImage = markerImage
                                interactionsState.onClicked {
                                    locationName = jsonObjectStoreName.orEmpty()
                                    locationAddress = jsonObjectAddress.orEmpty()
                                    locationPhoneNumber = jsonObjectPhoneNumber.orEmpty()
                                    // highlight-start
                                    selectedPoint = point
                                    // highlight-end
                                    showBottomSheet = true
                                    true
                                }
                                // highlight-start
                                iconSize = if (point == selectedPoint) 1.5 else 1.0
                                // highlight-end
                            }
                        }
                    }

                    // highlight-start
                    MapEffect(Unit) { mapView ->
                        mapView.gestures.addOnMapClickListener {
                            showBottomSheet = false
                            selectedPoint = null
                            false
                        }
                        mapView.getMapboxMap().subscribeCameraChanged {
                            showBottomSheet = false
                            selectedPoint = null
                        }
                    }
                    // highlight-end
                }

                AnimatedVisibility(
                    visible = showBottomSheet,
                    modifier = Modifier.align(Alignment.BottomCenter),
                    enter = slideInVertically(initialOffsetY = { it }),
                    exit = slideOutVertically(targetOffsetY = { it })
                ) {
                    Card(
                        modifier = Modifier.fillMaxWidth(),
                        shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
                        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
                        colors = CardDefaults.cardColors(containerColor = Color.White)
                    ) {
                        Column(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(horizontal = 24.dp, vertical = 16.dp)
                                .padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding())
                        ) {
                            Text(
                                text = "☕ $locationName",
                                style = MaterialTheme.typography.headlineSmall,
                                fontWeight = FontWeight.Bold
                            )
                            Spacer(modifier = Modifier.padding(vertical = 4.dp))
                            Text("📍 $locationAddress", style = MaterialTheme.typography.bodyLarge)
                            Text("📞 $locationPhoneNumber", style = MaterialTheme.typography.bodyLarge)
                            Spacer(modifier = Modifier.padding(vertical = 32.dp))
                        }
                    }
                }
            }
        }
    }
}
```

Tap a marker — it should grow larger to show that it's selected. Pan the map or tap an empty area and the sheet slides away while the marker returns to its normal size.

![Tapping on marker and rendering a bottom sheet with info about the coffee shop](https://docs.mapbox.com/help/ja/assets/ideal-img/android-geojson-step-6-selected-state.480.gif)

## Add a floating title

The last piece of UI is a floating title card anchored to the top of the screen. Because the map is edge-to-edge, `WindowInsets.systemBars` is used to offset the card below the status bar so it isn't obscured.

The title card is a Material3 `Card` placed inside the `Box`, above the `MapboxMap` and `AnimatedVisibility` blocks in the z-order. `Modifier.align(Alignment.TopCenter)` positions it at the top center, and `padding(top = ... calculateTopPadding() + 16.dp)` adds a comfortable gap below the status bar.

Replace the contents of `MainActivity.kt` with the code below. The highlighted section shows the new `Card` added to the `Box`.

Title: `MainActivity.kt`

```kotlin
package com.example.androidgeojson

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Point
import com.mapbox.maps.extension.compose.MapEffect
import com.mapbox.maps.extension.compose.MapboxMap
import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
import com.mapbox.maps.extension.compose.annotation.generated.PointAnnotation
import com.mapbox.maps.extension.compose.annotation.rememberIconImage
import com.mapbox.maps.plugin.gestures.gestures

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {

            var showBottomSheet by remember { mutableStateOf(false) }
            var locationName by remember { mutableStateOf("") }
            var locationAddress by remember { mutableStateOf("") }
            var locationPhoneNumber by remember { mutableStateOf("") }
            var selectedPoint by remember { mutableStateOf<Point?>(null) }

            val markerImage = rememberIconImage(
                key = R.drawable.ic_blue_marker,
                painter = painterResource(R.drawable.ic_blue_marker)
            )

            Box(Modifier.fillMaxSize()) {
                MapboxMap(
                    Modifier.fillMaxSize(),
                    mapViewportState = rememberMapViewportState {
                        setCameraOptions {
                            zoom(14.0)
                            center(Point.fromLngLat(-71.41547, 41.821369))
                            pitch(0.0)
                            bearing(0.0)
                        }
                    },
                    logo = {
                        Logo(Modifier.padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
                    },
                    attribution = {
                        Attribution(Modifier.padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
                    },
                    scaleBar = {}
                ) {
                    val featureCollection = remember {
                        val geoJson = assets.open("coffee_shops.geojson")
                            .bufferedReader()
                            .use { it.readText() }
                        FeatureCollection.fromJson(geoJson)
                    }

                    featureCollection.features()?.forEach { feature ->
                        val geometry = feature.geometry()
                        if (geometry is Point) {
                            val point = geometry
                            val properties = feature.properties()
                            val jsonObjectStoreName = properties?.get("name")?.asString
                            val jsonObjectAddress = properties?.get("address")?.asString
                            val jsonObjectPhoneNumber = properties?.get("phone")?.asString

                            PointAnnotation(point = point) {
                                iconImage = markerImage
                                interactionsState.onClicked {
                                    locationName = jsonObjectStoreName.orEmpty()
                                    locationAddress = jsonObjectAddress.orEmpty()
                                    locationPhoneNumber = jsonObjectPhoneNumber.orEmpty()
                                    selectedPoint = point
                                    showBottomSheet = true
                                    true
                                }
                                iconSize = if (point == selectedPoint) 1.5 else 1.0
                            }
                        }
                    }

                    MapEffect(Unit) { mapView ->
                        mapView.gestures.addOnMapClickListener {
                            showBottomSheet = false
                            selectedPoint = null
                            false
                        }
                        mapView.getMapboxMap().subscribeCameraChanged {
                            showBottomSheet = false
                            selectedPoint = null
                        }
                    }
                }

                // highlight-start
                Card(
                    modifier = Modifier
                        .align(Alignment.TopCenter)
                        .padding(top = WindowInsets.systemBars.asPaddingValues().calculateTopPadding() + 16.dp)
                        .padding(horizontal = 16.dp),
                    elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
                    colors = CardDefaults.cardColors(containerColor = Color.White)
                ) {
                    Text(
                        text = "☕ Coffee Finder",
                        style = MaterialTheme.typography.headlineMedium,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.padding(horizontal = 24.dp, vertical = 14.dp)
                    )
                }
                // highlight-end

                AnimatedVisibility(
                    visible = showBottomSheet,
                    modifier = Modifier.align(Alignment.BottomCenter),
                    enter = slideInVertically(initialOffsetY = { it }),
                    exit = slideOutVertically(targetOffsetY = { it })
                ) {
                    Card(
                        modifier = Modifier.fillMaxWidth(),
                        shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
                        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
                        colors = CardDefaults.cardColors(containerColor = Color.White)
                    ) {
                        Column(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(horizontal = 24.dp, vertical = 16.dp)
                                .padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding())
                        ) {
                            Text(
                                text = "☕ $locationName",
                                style = MaterialTheme.typography.headlineSmall,
                                fontWeight = FontWeight.Bold
                            )
                            Spacer(modifier = Modifier.padding(vertical = 4.dp))
                            Text("📍 $locationAddress", style = MaterialTheme.typography.bodyLarge)
                            Text("📞 $locationPhoneNumber", style = MaterialTheme.typography.bodyLarge)
                            Spacer(modifier = Modifier.padding(vertical = 32.dp))
                        }
                    }
                }
            }
        }
    }
}
```

You should now see the "☕ Coffee Finder" title card floating over the map at the top of the screen.

![Map with floating title card at the top that says 'Coffee Finder'](https://docs.mapbox.com/help/ja/assets/ideal-img/android-geojson-step-7-floating-title.c913ea1.480.png)

## Finished Product

You've now built a complete map-based app from scratch: GeoJSON data loaded at runtime, custom markers for every location, a slide-up bottom sheet with real data, a selected-marker visual state, gesture-based dismissal, and a polished floating title. This is a foundational interaction pattern for any map app that presents point-based data.

If you would like to view the finished code in its entirety, view the [Finished Code Snippet](#finished-code-snippet) at the bottom of the page.

Lastly, if you experienced any issues during the integration, here is a list of common troubleshooting solutions:

Details

**Troubleshooting Solutions**

-   Make sure you don't delete your package import line when copying code snippets. Your package import line should be at the top of your `MainActivity.kt` file and should be `package com.example.INSERT-PROJECT-NAME-HERE`.
-   If you cannot drag a file into editor you can also add the file to your project through your file explorer/finder window and dragging the file in there.
-   If you're experiencing unexpected errors, try these solutions:
    -   Click on **File > Sync Project with Gradle Files**
    -   Restarting your editor
    -   Updating your editor if you're not on the latest version of Android Studio.

![Map showing the final state of the app with markers, bottom sheet, and floating title](https://docs.mapbox.com/help/ja/assets/ideal-img/android-geojson-final-demo.480.gif)

### Finished Code Snippet

Title: `MainActivity.kt`

```kotlin
package com.example.androidgeojson

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Point
import com.mapbox.maps.extension.compose.MapEffect
import com.mapbox.maps.extension.compose.MapboxMap
import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
import com.mapbox.maps.extension.compose.annotation.generated.PointAnnotation
import com.mapbox.maps.extension.compose.annotation.rememberIconImage
import com.mapbox.maps.plugin.gestures.gestures

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // enableEdgeToEdge() allows the app to draw behind the system bars
        // (status bar and navigation bar), giving a full-screen map experience.
        enableEdgeToEdge()

        setContent {

            // Controls whether the bottom sheet is visible.
            var showBottomSheet by remember { mutableStateOf(false) }

            // Load the marker icon from drawable resources and register it with Mapbox
            // so it can be referenced by PointAnnotations on the map.
            val markerImage = rememberIconImage(
                key = R.drawable.ic_blue_marker,
                painter = painterResource(R.drawable.ic_blue_marker)
            )

            // These three state variables hold the data for whichever coffee shop
            // was most recently tapped. Wrapping them in remember + mutableStateOf
            // ensures the UI re-renders automatically when they change.
            var locationName by remember { mutableStateOf("") }
            var locationAddress by remember { mutableStateOf("") }
            var locationPhoneNumber by remember { mutableStateOf("") }

            // Tracks which point annotation is currently selected so we can
            // enlarge its icon to show an active state.
            var selectedPoint by remember { mutableStateOf<Point?>(null) }

            // Box lets us layer composables on top of the map — the title card
            // and bottom sheet sit above the MapboxMap in the z-order.
            Box(Modifier.fillMaxSize()) {
                MapboxMap(
                    Modifier.fillMaxSize(),
                    mapViewportState = rememberMapViewportState {
                        // Set the initial camera position over downtown Providence, RI.
                        setCameraOptions {
                            zoom(14.0)
                            center(Point.fromLngLat(-71.41547, 41.821369))
                            pitch(0.0)
                            bearing(0.0)
                        }
                    },
                    // Nudge the Mapbox logo and attribution up by the height of the
                    // system navigation bar so they aren't hidden behind it.
                    logo = {
                        Logo(Modifier.padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
                    },
                    attribution = {
                        Attribution(Modifier.padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
                    },
                    // Hide the scale bar — not needed for this use case.
                    scaleBar = {}
                ) {
                    // Read the GeoJSON file from assets once and cache the result.
                    // remember ensures we don't re-read the file on every recomposition.
                    val featureCollection = remember {
                        val geoJson = assets.open("coffee_shops.geojson")
                            .bufferedReader()
                            .use { it.readText() }
                        FeatureCollection.fromJson(geoJson)
                    }

                    // Loop over every feature in the GeoJSON and place a marker for
                    // each one that has a Point geometry.
                    featureCollection.features()?.forEach { feature ->
                        val geometry = feature.geometry()
                        if (geometry is Point) {
                            val point = geometry

                            // Extract the display properties from the feature's JSON object.
                            val properties = feature.properties()
                            val jsonObjectStoreName = properties?.get("name")?.asString
                            val jsonObjectAddress = properties?.get("address")?.asString
                            val jsonObjectPhoneNumber = properties?.get("phone")?.asString

                            PointAnnotation(point = point) {
                                iconImage = markerImage

                                // When a marker is tapped, store its data in state and
                                // show the bottom sheet. Returning true consumes the event
                                // so it doesn't also fire the map click listener.
                                interactionsState.onClicked {
                                    locationName = jsonObjectStoreName.orEmpty()
                                    locationAddress = jsonObjectAddress.orEmpty()
                                    locationPhoneNumber = jsonObjectPhoneNumber.orEmpty()
                                    selectedPoint = point
                                    showBottomSheet = true
                                    true
                                }

                                // Scale the active marker up by 50% so it stands out
                                // from the rest of the pins on the map.
                                iconSize = if (point == selectedPoint) 1.5 else 1.0
                            }
                        }
                    }

                    // MapEffect gives access to the underlying MapView so we can attach
                    // native Mapbox listeners that aren't exposed through the Compose API.
                    MapEffect(Unit) { mapView ->
                        // Tapping an empty part of the map dismisses the sheet.
                        mapView.gestures.addOnMapClickListener {
                            showBottomSheet = false
                            selectedPoint = null
                            false // returning false allows the event to propagate
                        }

                        // Any gesture that moves the camera (pan, pinch-zoom, rotate,
                        // fling) also dismisses the sheet and clears the active marker.
                        mapView.getMapboxMap().subscribeCameraChanged {
                            showBottomSheet = false
                            selectedPoint = null
                        }
                    }
                }

                // Floating title card — sits at the top of the screen, inset below
                // the status bar using the top system bar inset.
                Card(
                    modifier = Modifier
                        .align(Alignment.TopCenter)
                        .padding(top = WindowInsets.systemBars.asPaddingValues().calculateTopPadding() + 16.dp)
                        .padding(horizontal = 16.dp),
                    elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
                    colors = CardDefaults.cardColors(containerColor = Color.White)
                ) {
                    Text(
                        text = "☕ Coffee Finder",
                        style = MaterialTheme.typography.headlineMedium,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.padding(horizontal = 24.dp, vertical = 14.dp)
                    )
                }

                // Bottom sheet — slides up from the bottom when a marker is tapped
                // and slides back down when dismissed. AnimatedVisibility handles
                // the enter/exit transitions automatically.
                AnimatedVisibility(
                    visible = showBottomSheet,
                    modifier = Modifier.align(Alignment.BottomCenter),
                    enter = slideInVertically(initialOffsetY = { it }),
                    exit = slideOutVertically(targetOffsetY = { it })
                ) {
                    Card(
                        modifier = Modifier.fillMaxWidth(),
                        shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
                        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
                        colors = CardDefaults.cardColors(containerColor = Color.White)
                    ) {
                        Column(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(horizontal = 24.dp, vertical = 16.dp)
                                // Add bottom padding equal to the navigation bar height
                                // so content isn't hidden behind the home indicator.
                                .padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding())
                        ) {
                            Text(
                                text = "☕ $locationName",
                                style = MaterialTheme.typography.headlineSmall,
                                fontWeight = FontWeight.Bold
                            )
                            Spacer(modifier = Modifier.padding(vertical = 4.dp))
                            Text("📍 $locationAddress", style = MaterialTheme.typography.bodyLarge)
                            Text("📞 $locationPhoneNumber", style = MaterialTheme.typography.bodyLarge)
                            Spacer(modifier = Modifier.padding(vertical = 32.dp))
                        }
                    }
                }
            }
        }
    }
}
```

## Next steps

Now that you've finished this tutorial, here are some other relevant resources:

-   To see other examples, view our [Jetpack Compose Examples](https://docs.mapbox.com/android/maps/examples/compose/)
-   To learn more about other types of annotations, see our [Annotation Guide](https://docs.mapbox.com/android/maps/guides/annotations/annotations/#default-markers)

Here are some ideas for improving this project if you would like to continue working on it:

-   Swap out the sample dataset for your own GeoJSON file. You can use [geojson.io](https://geojson.io/) to create and edit your own GeoJSON file.
-   Adjust the map's initial camera view to show a different location.
-   Improve the layout and styling of the data showing in the bottom sheet to make it more visually appealing.
-   Instead of using a local GeoJSON file, load one via a network request.
-   Add clustering so nearby markers group together at lower zoom levels.