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

# Build an Android marker app from a custom style and tileset

This tutorial demonstrates how to build an Android app using a [custom map style](https://docs.mapbox.com/help/dive-deeper/map-design/#what-is-a-style) with embedded data and custom markers. This app will use a custom style and data, so the tutorial will focus on the implementation of the style into the map and creating interactions with the style.

Using a custom map style is ideal for cross platform applications as the style can be used in different platforms and reduces frontend code.

Your final map will include markers representing 10 dog spas in Boston. When tapped, the markers spawn a window that scrolls up from the bottom of the screen and displays information about the selected business:

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/help/assets/medias/tutorials--android-custom-style--full-demo-d20f9c970d75389c63557ee918daf240.mp4).

## Prerequisites

Before you begin, you will need:

-   **A Mapbox account**: Sign up for a free account on [Mapbox](https://account.mapbox.com/auth/signup/).
-   **Familiarity with Android development**: Beginner experience with Jetpack Compose, Kotlin, and Android Studio.
-   **A project with the Maps SDK installed**: Follow the [Getting Started with the Maps SDK for iOS](https://docs.mapbox.com/ios/maps/guides/install/) guide.
-   **Android Studio**
-   **A custom style URL.** A custom style for this tutorial, contains a custom font, marker SVG and layers for the dog spa locations. This style can be previewed at this URL:

```
https://api.mapbox.com/styles/v1/examples/cm37hh1nx017n01qk2hngebzt.html?title=view&access_token=YOUR_MAPBOX_ACCESS_TOKEN&zoomwheel=true&fresh=true#11.41/42.3249/-71.0969
```

> **Note (warning): Troubleshooting the preview link**
> 
> When using the URL above to preview the style, make sure to replace `YOUR_MAPBOX_ACCESS_TOKEN` with your actual Mapbox access token. You can find your access token in your [Mapbox account dashboard](https://account.mapbox.com/access-tokens/).

> **Note: Creating your own style**
> 
> If you would like to create this custom style yourself instead of using the style provided, follow the steps in the [Add and style data in Mapbox Studio](https://docs.mapbox.com/help/help/tutorials/add-data-to-mapbox-style/) tutorial.

## Add a custom style to the map

To begin, you will need to create add a map to your app, and in the constructor of the map, you will need to pass the `StyleURL` so the map will render the custom style instead of the default options.

This style contains a custom tileset with geojson data for dog spas in the Boston area, as well as custom markers to represent each location. This data is represented in two layers in the style, presenting two different marker designs for the same locations, based on the zoom level.

Add the code below to your `MainActivity.kt` file to create the map:

```kotlin
package com.example.dog_spa_walkthrough

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
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.style.MapStyle

public class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // Create a map
            MapboxMap(
                Modifier.fillMaxSize(),
                scaleBar = { },
                style = {
                    MapStyle(
                        style = "mapbox://styles/examples/cm37hh1nx017n01qk2hngebzt")  //Appends custom style to the map, overriding default style and passing in relevant data 
                    },
                // Sets viewport over Boston MA
                mapViewportState = rememberMapViewportState {
                    setCameraOptions {
                        zoom(7.0)
                        center(Point.fromLngLat(-71.09290,42.34622))
                        pitch(0.0)
                        bearing(0.0)
                    }
                },
            )
        }
    }
}
```

Run the app in your emulator to load a full screen map centered over Boston, MA. The custom style will display a group of markers over the Boston area.

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/help/assets/medias/tutorials--android-custom-style--add-a-map-7fabc19308ab10d243188e941dd7c2c0.mp4).

In the next step, you will introduce interactivity to your app by adding a tap interaction to the markers and accessing the underlying POI data.

## Add a tap interaction to the markers

Next you will grab data from a marker when it is tapped, using the [`onLayerClicked`](https://docs.mapbox.com/android/maps/api/latest/mapbox-maps-android/com.mapbox.maps.extension.compose.style.standard.generated/-standard-style-interactions-state/on-layer-clicked.html) function from the Interactions API.

This functionality allows the developer to detect when a feature has been clicked, and give access to any underlying data from that feature. You'll log the data from the marker to the console for now, and in the next step, you'll display the data in a `BottomModalDrawer`.

Add the highlighted lines from the snippet below to your `MainActivity.kt` file:

> **Note**
> 
> This custom style uses two layers to represent the dog spas, circles when zoomed out, and a marker icon when zoomed in. The code below listens for taps on both layers, calling `onLayerClicked` for each layer to detect once a feature has been tapped.

```kotlin
package com.example.dog_spa_walkthrough

import android.os.Bundle
// highlight-start
import android.util.Log
// highlight-end
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
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.style.MapStyle
// highlight-start
import com.mapbox.maps.extension.compose.style.rememberStyleState
// highlight-end

public class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MapboxMap(
                Modifier.fillMaxSize(),
                scaleBar = { },
                style = {
                    MapStyle(
                        style = "mapbox://styles/examples/cm37hh1nx017n01qk2hngebzt",
                        // highlight-start
                        styleState = rememberStyleState {
                        // Implements the interactions API, grabbing reference when a feature is tapped by the user. One is needed for each layer containing data in the style.
                        styleInteractionsState
                            .onLayerClicked(id = "dog-groomers-3o4sdb") { feature, context ->
                                
                                // Accesses the underlying feature data, such as business name and phone number
                                Log.d("Mapbox", feature.properties.toString());
                                true
                            }
                            .onLayerClicked(id = "dog-groomers-boston-marker") { feature, context ->

                                // Accesses the underlying feature data, such as business name and phone number
                                Log.d("Mapbox", feature.properties.toString());
                                true
                            }
                        }
                        // highlight-end
                    )
                },
                mapViewportState = rememberMapViewportState {
                    setCameraOptions {
                        zoom(7.0)
                        center(Point.fromLngLat(-71.09290,42.34622))
                        pitch(0.0)
                        bearing(0.0)
                    }
                },
            )
        }
    }
}

```

When you run the emulator, tap on a marker and will log information using LogCat, such as the address, city, country, etc, of the selected POI.

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/help/assets/medias/tutorials--android-custom-style--layer-clicked-5b640ff3efdb631be938f31b968841da.mp4).

## Display marker properties on tap

With the app responding to taps and extracting the feature data from the tapped marker, the next step is to add UI to display information about a marker its tapped.

This code renders a `BottomModalDrawer` that contains text elements, used to describe the different properties of each POI. When a marker or circle is tapped, the related properties data of the selected feature is grabbed from the style and passed into a set of variables, which will store the data and display it in the `BottomModalDrawer`. Tapping on the marker will also manage the state of the `BottomModalDrawer`, setting `showBottomSheet` to true, when a marker is tapped, and the next tap will dismiss the UI element, setting `showBottomSheet` to false and closing the `BottomModalDrawer`.

Add the highlighted code below to your code, and replace the log statements with the new `grabPOI()` function:

```kotlin
package com.example.dog_spa_walkthrough

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
// highlight-start
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material.icons.filled.Place
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
// highlight-end
import androidx.compose.ui.Modifier
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.style.MapStyle
import com.mapbox.maps.extension.compose.style.rememberStyleState
// highlight-start
import org.json.JSONObject
import kotlin.math.roundToInt
// highlight-end

public class MainActivity : ComponentActivity() {

// highlight-start
    val locationName = mutableStateOf("none")
    val locationAddress = mutableStateOf("none")
    val locationPhoneNumber = mutableStateOf("none")
    val locationRating = mutableStateOf("none")
// highlight-end

// highlight-start
    // Necessary to use BottomModalDrawer.
    @OptIn(ExperimentalMaterial3Api::class)
// highlight-end
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
// highlight-start
            //Stores state of BottomModalDrawer, noting if the drawer is opened or closed.
            val sheetState = rememberModalBottomSheetState()
            var showBottomSheet by remember { mutableStateOf(false) }

            // Deconstructs a JSON object, allowing access to each of the property values of the passed in feature.
            fun grabPOIData(properties: JSONObject)
            {
                locationName.value  = properties.get("storeName").toString()
                locationAddress.value = properties.get("address").toString() + ", " + properties.get("city").toString() + " " + properties.get("postalCode").toString()
                locationPhoneNumber.value = properties.get("phoneFormatted").toString()
                locationRating.value = properties.get("rating").toString()

                showBottomSheet = true
            }
// highlight-end
            MapboxMap(
                Modifier.fillMaxSize(),
                scaleBar = {},
                style = {
                    MapStyle(
                        style = "mapbox://styles/examples/cm37hh1nx017n01qk2hngebzt",
                        styleState = rememberStyleState {
                        styleInteractionsState
                            .onLayerClicked(id = "dog-groomers-3o4sdb") { feature, context ->
                                // highlight-start
                                grabPOIData(feature.properties)
                                // highlight-end
                                true
                            }
                            .onLayerClicked(id = "dog-groomers-boston-marker") { feature, context ->
                                // highlight-start
                                grabPOIData(feature.properties)
                                // highlight-end
                                true
                            }
                        }
                    )
                },
                mapViewportState = rememberMapViewportState {
                    setCameraOptions {
                        zoom(7.0)
                        center(Point.fromLngLat(-71.09290,42.34622))
                        pitch(0.0)
                        bearing(0.0)
                    }
                },
            )

// highlight-start
            // Checks if a marker has been tapped, if so will open the ModalBottomSheet
            if (showBottomSheet) {
                ModalBottomSheet(
                    onDismissRequest = {
                        showBottomSheet = false
                    },
                    sheetState = sheetState
                ) {
                    // Aligns the contents of the ModalBottomSheet for better readability
                    Column(
                        modifier = Modifier.fillMaxWidth().padding(25.dp,0.dp,0.dp,0.dp),
                        horizontalAlignment = Alignment.Start
                    ) {
                        // These values update when the marker is selected by querying the GeoJSON
                        // for the related data, and then updating the global variables so the text prints correctly here.
                        Text(
                            text = locationName.value,
                            style = MaterialTheme.typography.displaySmall,
                        )

                        Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxHeight(0.04f))
                        {
                            Text("Rating: ", style = MaterialTheme.typography.bodyMedium)
                            val roundedRating = locationRating.value.toFloat().roundToInt()
                            for (i in 0..roundedRating-1){//locationRating.value.toInt()) {
                                Icon(Icons.Filled.Star,"",tint = Color.Red)
                            }
                        }

                        Spacer(modifier = Modifier.padding(vertical = 10.dp))

                        Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxHeight(0.04f))
                        {
                            Icon(Icons.Filled.Place, "", tint = Color.Red, modifier = Modifier.padding(2.dp))
                            Text(locationAddress.value, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(1.dp))
                        }
                        Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxHeight(0.04f))
                        {
                            Icon(Icons.Filled.Phone, "", tint = Color.Red, modifier = Modifier.padding(3.dp))
                            Text(
                                locationPhoneNumber.value,
                                style = MaterialTheme.typography.bodyLarge
                            )
                        }

                        // Adds padding at the bottom of the ModalBottomSheet
                        Spacer(modifier = Modifier.padding(vertical = 50.dp))

                    }

                }
            }
            // highlight-end

        }
    }

}
```

Now when you run the app in an emulator or in the preview, you should see the `BottomModalDrawer` slide up from the bottom of the screen when you tap on a marker. The feature properties will be displayed in the `BottomModalDrawer` and tapping again will close it.

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/help/assets/medias/tutorials--android-custom-style--bottom-modal-sheet-3481c49f2957b3bddd15cd0e12d1763e.mp4).

## (Optional) Add title card and call to action

In this step you will add a title card to name the app and an additional box to instruct the user how to use the app.

Add the following code to create the title card and display it over the map:

```kotlin
package com.example.dog_spa_walkthrough

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material.icons.filled.Place
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
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.style.MapStyle
import com.mapbox.maps.extension.compose.style.rememberStyleState
import org.json.JSONObject
import kotlin.math.roundToInt

public class MainActivity : ComponentActivity() {

    val locationName = mutableStateOf("none")
    val locationAddress = mutableStateOf("none")
    val locationPhoneNumber = mutableStateOf("none")
    val locationRating = mutableStateOf("none")

    @OptIn(ExperimentalMaterial3Api::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val sheetState = rememberModalBottomSheetState()
            var showBottomSheet by remember { mutableStateOf(false) }

            fun grabPOIData(properties: JSONObject)
            {
                locationName.value  = properties.get("storeName").toString()
                locationAddress.value = properties.get("address").toString() + ", " + properties.get("city").toString() + " " + properties.get("postalCode").toString()
                locationPhoneNumber.value = properties.get("phoneFormatted").toString()
                locationRating.value = properties.get("rating").toString()

                showBottomSheet = true
            }

            MapboxMap(
                Modifier.fillMaxSize(),
                scaleBar = {},
                style = {
                    MapStyle(
                        style = "mapbox://styles/examples/cm37hh1nx017n01qk2hngebzt",
                        styleState = rememberStyleState {
                        styleInteractionsState
                            .onLayerClicked(id = "dog-groomers-3o4sdb") { feature, context ->
                                grabPOIData(feature.properties)
                                true
                            }
                            .onLayerClicked(id = "dog-groomers-boston-marker") { feature, context ->
                                grabPOIData(feature.properties)
                                true
                            }
                        }
                    )
                },
                mapViewportState = rememberMapViewportState {
                    setCameraOptions {
                        zoom(7.0)
                        center(Point.fromLngLat(-71.09290,42.34622))
                        pitch(0.0)
                        bearing(0.0)
                    }
                },
            )

// highlight-start
            // Adds UI element with call to action and app title text
            Column(
                modifier = Modifier
                    .fillMaxWidth(0.5f)
                    .fillMaxHeight(0.15f)
                    .padding(16.dp),
                verticalArrangement = Arrangement.SpaceEvenly)
            {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .fillMaxHeight(0.3f)
                        .clip(shape = RoundedCornerShape(5.dp))
                        .padding(1.dp)
                        .background(Color.White)
                )
                {
                    Row(modifier = Modifier.padding(1.dp))
                    {
                        Icon(Icons.Filled.Favorite, "", tint = Color.Red, modifier = Modifier.padding(2.dp))
                        Text("Pet Spa Finder", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(8.dp,1.dp,1.dp,1.dp))
                    }
                }
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .fillMaxHeight(0.7f)
                        .padding(1.dp)
                        .clip(shape = RoundedCornerShape(5.dp))
                        .background(Color.White)
                )
                {
                    Row(modifier = Modifier.padding(1.dp))
                    {
                        Text("Click a marker for more information.", style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(5.dp,2.dp))
                    }
                }
            }
        // highlight-end

            // Checks if a marker has been tapped, if so will open the ModalBottomSheet
            if (showBottomSheet) {
                ModalBottomSheet(
                    onDismissRequest = {
                        showBottomSheet = false
                    },
                    sheetState = sheetState
                ) {
                    // Aligns the contents of the ModalBottomSheet for better readability
                    Column(
                        modifier = Modifier.fillMaxWidth().padding(25.dp,0.dp,0.dp,0.dp),
                        horizontalAlignment = Alignment.Start
                    ) {
                        // These values update when the marker is selected by querying the GeoJSON
                        // for the related data, and then updating the global variables so the text prints correctly here.
                        Text(
                            text = locationName.value,
                            style = MaterialTheme.typography.displaySmall,
                        )

                        Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxHeight(0.04f))
                        {
                            Text("Rating: ", style = MaterialTheme.typography.bodyMedium)
                            val roundedRating = locationRating.value.toFloat().roundToInt()
                            for (i in 0..roundedRating-1){//locationRating.value.toInt()) {
                                Icon(Icons.Filled.Star,"",tint = Color.Red)
                            }
                        }

                        Spacer(modifier = Modifier.padding(vertical = 10.dp))

                        Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxHeight(0.04f))
                        {
                            Icon(Icons.Filled.Place, "", tint = Color.Red, modifier = Modifier.padding(2.dp))
                            Text(locationAddress.value, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(1.dp))
                        }
                        Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxHeight(0.04f))
                        {
                            Icon(Icons.Filled.Phone, "", tint = Color.Red, modifier = Modifier.padding(3.dp))
                            Text(
                                locationPhoneNumber.value,
                                style = MaterialTheme.typography.bodyLarge
                            )
                        }

                        // Adds padding at the bottom of the ModalBottomSheet
                        Spacer(modifier = Modifier.padding(vertical = 50.dp))

                    }

                }
            }

        }
    }

}
```

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/help/assets/medias/tutorials--android-custom-style--full-demo-d20f9c970d75389c63557ee918daf240.mp4).

## Final Product

**Congratulations!** You've built an Android application with the **Mapbox Maps SDK for Android**, that imports a custom Mapbox style, access the data from the style and shows feature information when the user interacts with the map. You're app should look like the video below:

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/help/assets/medias/tutorials--android-custom-style--full-demo-d20f9c970d75389c63557ee918daf240.mp4).

## Next Steps

### What we covered

-   Adding a `Map` to display a custom style centered over Boston, MA.
-   Adding a `onLayerClicked` to manage clicks on the map layer with and set state in your app with the clicked feature.
-   Creating a `BottomModalDrawer` to display feature properties when a marker is tapped.
-   Adding title card and call to action to describe the app and how to use it.

Now that you've finished building your app, here are some potential features you could add to further build out your project:

-   Improve the UI of the information panel.
-   Create a custom style with your own data by following the [Add and style data in Mapbox Studio](https://docs.mapbox.com/help/help/tutorials/add-data-to-mapbox-style/) tutorial.
-   Re-center the map on a marker after it is tapped.
-   Add a reset map button to return users to the original view of the map.
-   Add more fields to the dataset, such as the company website.

### Learn More

-   Browse the source code for this tutorial on [GitHub](https://github.com/mapbox/tutorials/tree/main/android-marker-app/)
-   Setup and run the [**Mapbox Maps SDK for Android** examples app](https://docs.mapbox.com/help/tutorials/maps-sdk-android-examples-app/) to learn more about what you can do with Mapbox maps on Android.
-   Learn more about the [**Mapbox Maps SDK for Android**](https://docs.mapbox.com/android/maps/overview/) in the documentation.