How to use Official Mapbox Maps Compose Extension with Permission Flow and FusedLocationProviderClient

Debanshu Datta
ProAndroidDev
Published in
5 min readOct 23, 2023

--

Photo by T.H. Chia on Unsplash

Recently Mapbox released its official compose extension just two months back. So I wanted to try it out though it doesn’t support all the existing Mapbox features out of the box in 0.1.0 the release, but I hope they will fix all the shortcomings soon.

[22 Oct 2023] There is presently an incompatibility between com.mapbox.maps:android:11.0.0-beta.1 any other dependencies of navigation etc. Due to multiple duplicate class errors in common block, the team is currently trying to resolve it here is the issue link.

Here is the repository if you want to take a look.

Let’s get Started

We will have a simple problem statement to display the mapboxmap, get user location permission with permission-flow-android and finally get the location with FusedLocationProviderClient.

On permission is given the Map transitions and adds a marker to the current location

Mapbox Setup With Compose Extension

  • Getting API Key: To get the Access tokens, first make an account in Mapbox. Make sure to check DOWNLOADS:READ. Copy this token, and add this to local.properties
  • Adding Dependencies: For adding dependency we have only two steps to consider, setting up the source to fetch dependencies with our mapbox api keysecondly, our Mapbox compose dependency, and finally the dependencies for getting user location.
/*
File Name: settings.gradle.kts
*/

// Get the API key properties from local.properties
val keyProps = Properties().apply {
file("local.properties").takeIf { it.exists() }?.inputStream()?.use { load(it) }
}

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
// below code is added
maven {
url = uri("https://api.mapbox.com/downloads/v2/releases/maven")
authentication {
create<BasicAuthentication>("basic")
}
credentials {
username = "mapbox"
password = keyProps.getProperty("MAPBOX_MAP_TOKEN")
}
}
}
}
/*
File Name: build.gradle.kts(:app)
add the compose extension with your other dependencies.
*/
dependencies {
implementation("com.mapbox.extension:maps-compose:0.1.0")

// Pick your versions of Android Mapbox Map SDK
// Note that Compose extension is compatible with Maps SDK v11.0+.
implementation("com.mapbox.maps:android:11.0.0-beta.1")

// Handling Permission scenario
implementation("dev.shreyaspatil.permission-flow:permission-flow-compose:1.2.0")

// libs for fetching user current location and handling this Task API
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.0")
implementation("com.google.android.gms:play-services-location:21.0.1")
}

Simple View with Marker

We will set the initial map style and the initial camera position by constructing the MapInitOptions using the context provided by the MapInitOptionsFactory. MapboxMap provides us will all the options to configure taps, compass settings and other interactive options. PointAnnotation is used to add markers on the map, we had added the marker image in the resource files, we have access to the onClick and other handle settings.
That's it! It is that simple to render a map with Mapbox.✨

class MainActivity : ComponentActivity() {
@OptIn(MapboxExperimental::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MapboxMap(
modifier = Modifier.fillMaxSize(),
mapInitOptionsFactory = { context ->
MapInitOptions(
context = context,
styleUri = Style.LIGHT,
cameraOptions = CameraOptions.Builder()
.center(Point.fromLngLat(24.9384, 60.1699))
.zoom(12.0)
.build()
)
}
){
AddPointer(Point.fromLngLat(24.9384, 60.1699))
}
}
}

@OptIn(MapboxExperimental::class)
@Composable
fun AddPointer(point:Point){
val drawable = ResourcesCompat.getDrawable(
resources,
R.drawable.marker,
null
)
val bitmap = drawable!!.toBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
PointAnnotation(
iconImageBitmap = bitmap,
iconSize = 0.5,
point = point,
onClick = {
Toast.makeText(
this,
"Clicked on Circle Annotation: $it",
Toast.LENGTH_SHORT
).show()
true
}
)
}
}

State Management & Current Location with Marker

Let’s start by making a LocationService which will provide us with the location of the user. It is quite simple FusedLocationProviderClient which is already an existing solution and a widely adapted approach to get device location.

The below implementation follows a pattern of throwing exceptions for permissions and GPS services. We got the FusedLoactionProviderClient and set up CurrentLocationRequestwith priority and accuracy. Finally, after the permission check, we make the call loactionProvider.getCurrentLoaction(request,null).await() to get the current user location.

This await() function comes from kotlinx-coroutines-play-services which is a library that integrates with the Google Play Services Tasks API. It includes extension functions like Task.asDeferred.

internal object LocationService {
suspend fun getCurrentLocation(context: Context): Point {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
when {
!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) -> throw LocationServiceException.LocationDisabledException()
!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) -> throw LocationServiceException.NoNetworkEnabledException()
else -> {
// Building FusedLocationProviderClient
val locationProvider = LocationServices.getFusedLocationProviderClient(context)
val request = CurrentLocationRequest.Builder()
.setPriority(Priority.PRIORITY_BALANCED_POWER_ACCURACY)
.build()

runCatching {
val location = if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
throw LocationServiceException.MissingPermissionException()
} else {
locationProvider.getCurrentLocation(request, null).await()
}
return Point.fromLngLat(location.longitude, location.latitude)
}.getOrElse {
throw LocationServiceException.UnknownException(stace = it.stackTraceToString())
}
}
}
}

sealed class LocationServiceException : Exception() {
class MissingPermissionException : LocationServiceException()
class LocationDisabledException : LocationServiceException()
class NoNetworkEnabledException : LocationServiceException()
class UnknownException(val stace: String) : LocationServiceException()
}

}

Add permission in Manifest.xml

 <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

We will be making a list of all the required permissions list val permissionList = listOf(.....) to get the permission request we will be adding a button and an onClick listener. permissionLauncher.launch(permissionList.toTypedArray()). We will be listening to the permission state using rememberPermissionState(). In LaunchEffect we will add state as a dependency when the state changes, then check the state.isGranted or not. Finally, we will trigger the LoactionService to get the user's location and update the currentLocation. This will eventually recompose the map add marker and redirect to the location on the map camera.

internal class MainActivity : ComponentActivity() {
@OptIn(MapboxExperimental::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val permissionList = listOf(android.Manifest.permission.ACCESS_FINE_LOCATION)
setContent {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val permissionLauncher = rememberPermissionFlowRequestLauncher()
val state by rememberPermissionState(android.Manifest.permission.ACCESS_FINE_LOCATION)
var currentLocation: Point? by remember { mutableStateOf(null) }
val mapViewportState = rememberMapViewportState {
setCameraOptions {
center(Point.fromLngLat(0.0, 0.0))
zoom(1.0)
pitch(0.0)
}
}

LaunchedEffect(state){
coroutineScope.launch {
if(state.isGranted) {
currentLocation = LocationService.getCurrentLocation(context)
val mapAnimationOptions =
MapAnimationOptions.Builder().duration(1500L).build()
mapViewportState.flyTo(
CameraOptions.Builder()
.center(currentLocation)
.zoom(12.0)
.build(),
mapAnimationOptions
)
}
}
}

Column {
if (state.isGranted) {
//TODO: adding search section
} else {
Button(onClick = { permissionLauncher.launch(permissionList.toTypedArray()) }) {
Text("Request Permissions")
}
}
MainMapViewComposable(mapViewportState, currentLocation)
}
}
}

@Composable
@OptIn(MapboxExperimental::class)
private fun MainMapViewComposable(
mapViewportState: MapViewportState,
currentLocation: Point?
) {
val gesturesSettings by remember {
mutableStateOf(DefaultSettingsProvider.defaultGesturesSettings)
}

MapboxMap(
modifier = Modifier.fillMaxSize(),
mapViewportState = mapViewportState,
gesturesSettings = gesturesSettings,
mapInitOptionsFactory = { context ->
MapInitOptions(
context = context,
styleUri = Style.TRAFFIC_DAY,
cameraOptions = CameraOptions.Builder()
.center(Point.fromLngLat(24.9384, 60.1699))
.zoom(12.0)
.build()
)
}
) {
currentLocation?.let { AddSingleMarkerComposable(it, resources) }
}
}
}

That’s it! It is rendered a map with Mapbox ✨ adding a marker to the current location and handling location permission.

Reference Docs

--

--

Android(L2) @Gojek | Mobile Developer | Backend Developer (Java/Kotlin)