FusedLocationProviderClient
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 mapbox
map, 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 tolocal.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.✨
Job Offers
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 CurrentLocationRequest
with 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
This article was previously published on proandroiddev.com