A quick guide to using Mapbox in Android apps
Share on twitter
Share on facebook
Share on linkedin
Share on tumblr

A quick guide to using Mapbox in Android apps

Introduction

Google maps has been one of the developers’ preferred choices when thinking of adding maps to their applications but as prices increased and a reduction of free API calls took place, developers had to start looking for an alternative that let them work with a cheaper map tool and that’s when Mapbox started to gain popularity.


Mapbox is one of the largest platforms that provide designed maps for websites and mobile applications; according to its documentation, it offers features such as maps, search, and navigation as well as better map customization compared to other options such as LeafLet or OpenLayers (If you want to know how many maps platforms exists you can visits this link). It’s powered by OpenStreetMap, a massive collaborative project to create free, editable maps.
 

What you should already know

This tutorial assumes a basic knowledge of:

  • Kotlin
  • MVVM (Model-View-ViewModel)
  • Android coroutines
  • Android Studio

What you’ll learn

In this tutorial, we’ll cover the most common implementations of Mapbox in our applications:

  • Ask for location permissions.
  • Get users’ addresses by their current location (geocoder).
  • Get both latitude and longitude and set a pin on the map by typing an address on a certain radius (reverse geocoder).

Overview

You’ll be working on an application that allows you to keep track of the user’s current location by setting a pin over the map. Also, you can search for addresses by using the Places API.
 

Getting started

Download the base code here or you can clone the repository on Github instead.

$ git clone https://github.com/tangosource/mapbox-app
$ cd mapbox-app
$ git checkout step_1

Note: Since the project you’ll download contains the main view file already made, we’ll just focus on adding the functionality to our application.
 

Dependencies

This project already has added dependencies that we need to work through this tutorial; if you downloaded or cloned the project the build.gradle file has to look like this:
Mapbox dependencies
These dependencies are needed to start working with Mapbox SDK and build our application using view models, remember that we are going to use MVVM as our design pattern.
 

Step 1: Getting Mapbox Access Token

Before coding, we have to get a Mapbox access token; this token allows us to use the SDK on our Android application. According to the official Mapbox documentation regarding access token:

“To use any of Mapbox’s tools, APIs, or SDKs, you’ll need a Mapbox access token. Mapbox uses access tokens to associate API requests with your account. You can find your access tokens, create new ones, or delete existing ones on your Access Tokens page or programmatically using the Mapbox Tokens API.”

To get an access token you need to sign up to your Mapbox account or create a new one if you don’t have it yet. Once you are logged in, go to the dashboard page and copy the default token.
For this tutorial, it’s ok if you use the default token.
Mapbox access tokens
Copy the access token to your clipboard and go to your build.gradle file inside your app folder, here we’ll create a custom field at the end of the defaultConfig block; in this way, you can use this variable in the whole project.
 

buildConfigField('String', 'MAPBOX_ACCESS_TOKEN', '"here goes your mapbox access token"')
Mapbox Access Tokens. buildConfigField

Step 2 – Ask for locations permissions

Open your AndroidManifest.xml and add the ACCESS_FINE_LOCATION permission. We’ll use this permission to access the user’s current location.
 

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

 
Since Android 6 Marshmallow is required to ask for permissions at runtime, we have to write some extra code for doing so, if we don’t do it, the app will crash because access to the current location is dangerous permission that needs to be approved by the user.
In this case, we’ll use PermissionsManager, which is a useful class that helps request permissions at runtime and it’s part of Mapbox SDK, so, you don’t need to add an extra dependency. 
Go to your MainActivity.kt and create a new nullable variable permissionsManager
 

private var permissionsManager: PermissionsManager? = null

 
Then let’s create a new function on which we’ll do two things; a) create a new instance for permissionManager and b) request for the current user location.
 

private fun enableLocationComponent(loadedMapStyle: Style) {
        if (!PermissionsManager.areLocationPermissionsGranted(this)) {
            permissionsManager = PermissionsManager(this)
            permissionsManager?.requestLocationPermissions(this)
            return
        }
    }

 
enableLocationComponent(loadedMapStyle: Style) is a function that will be called whenever our map it’s ready to be used, and we’ll pass the map style as a parameter, but we’ll see that later on.
PermissionsManager.areLocationPermissionsGranted(this) is a method that will verify if the user has already granted the location’s permissions. If those permissions haven’t been granted yet, we have to create a new PermissionManager instance and then request for those permissions; this process will display a dialog asking the user if he wants to allow the Mapbox App access to the location.
 
Mapbox App access
 
When the user hits on either DENY or ALLOW, the system invokes onRequestPermissionsResult() method, passing it the user response. Here, we would have to know whether the user accepted the permissions or not, but since we’re using PermissionManager, we don’t have to do that, the only thing for us to do is calling onRequestPermissionsResult() method from the permissionManager instance.


Override onRequestPermissionResult() method under onCreate() method
 

override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        permissionsManager?.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

 
Finally, the last thing for us to do is adding PermissionListener to MainActicvity class and implement the two functions required: onExplanationNeeded and onPermissionResult
 

override fun onExplanationNeeded(permissionsToExplain: MutableList<String>?) {
        Toast.makeText(applicationContext, "This app needs location permissions in order to work properly", Toast.LENGTH_LONG)
            .show()
    }
    override fun onPermissionResult(granted: Boolean) {
        if (!granted) {
            Toast.makeText(applicationContext, "You didn\'t grant location permissions.", Toast.LENGTH_LONG).show()
            return
        }
    }

 
onExplanationNeeded() method will be called when the user has previously denied the permission and we have to show an explanation asynchronously without blocking the main thread. After the user sees the explanation, we can request the permissions again.


onPermissionResult() method will be called when the user denies or accepts the permissions; here, we’ll show a message when permissions have been denied, otherwise we’ll get the user’s current location.


Once you’ve finished adding permissions, your MainActivity class has to look like this:
 
MainActivity class
 

Step 3 – Setting up Mapbox

Next, we have to create variables that will handle the instance of our map:
 

// set these two variables on top of onCreate() method 
private var mapView: MapView? = null
private var mapboxMap: MapboxMap? = null
// set these variables inside onCreate() method
mapView = findViewById(R.id.mapView)
mapView?.onCreate(savedInstanceState)
mapView?.getMapAsync(this)

 
Once you are done adding the lines of code above, you will see that getMapAsync(this) is throwing a complaining error, this is due to a listener that we don’t have implemented on MainActivity class yet; to solve this problem let MainActivity implement interface OnMapReadyCallback.
 
OnMapReadyCallback.
 
onMapReady() is a method that will trigger every time when our map is ready to be used, here is when we have to set the map style and assign the value of our mapboxMap variable.
Inside of the onMapReady() method add these lines of code:
 

this.mapboxMap = mapboxMap
mapboxMap?.setStyle(Style.MAPBOX_STREETS) {
  enableLocationComponent(it)

 
setStyle() method needs two parameters, the style of our map and a callback that will tell us when the style is already available for use.
The available Mapbox styles are listed below; you can use your favorite style or the one that fits your project’s preferences. Also, if none of the default styles are what you need, you can create a custom style from the Mapbox dashboard.

  • MAPBOX_STREETS
  • OUTDOORS
  • LIGHT
  • DARK
  • SATELLITE
  • SATELLITE_STREETS
  • TRAFFIC_DAY
  • TRAFFIC_NIGHT

 
Also, the Mapbox map has its own lifecycle the same as an activity, it’s important to add it because by doing this, we avoid any error or memory leaks on our application when using the map. 
We have to set up the Mapbox lifecycle by adding the next methods just under onCreate() method
 

 override fun onStart() {
        super.onStart()
        mapView?.onStart()
    }
    override fun onResume() {
        super.onResume()
        mapView?.onResume()
    }
    override fun onPause() {
        super.onPause()
        mapView?.onPause()
    }
    override fun onStop() {
        super.onStop()
        mapView?.onStop()
    }
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mapView?.onSaveInstanceState(outState)
    }
    override fun onLowMemory() {
        super.onLowMemory()
        mapView?.onLowMemory()
    }
    override fun onDestroy() {
        super.onDestroy()
        mapView?.onDestroy()
    }

 
We need to add just one more thing to run the application and see the map in action. To do so, Mapbox has to be initialized before getting it started, we can accomplish it by setting the next line of code before setContentView(R.layout.activity_main). This is important because if we don’t do that, we’ll get a RunTimeException
 
Mapbox.getInstance
 
Once you’ve added this line, you may run the application. You have to see something like this:
 
Mapbox
 

Step 4 – Get the user’s current location

In order to enable the user’s current location, we’ll use Mapbox’s locationComponent method; this method can be used to display the user’s location on the map.
Go to enableLocationComponent(loadedMapStyle: Style) and set this locationComponent configuration under the if which we use for asking for permission.
 

 val locationComponent = mapboxMap?.locationComponent
        locationComponent?.activateLocationComponent(
            LocationComponentActivationOptions.builder(
                this,
                loadedMapStyle
            ).build()
        )
        locationComponent?.isLocationComponentEnabled = true
        locationComponent?.cameraMode = CameraMode.NONE
        locationComponent?.renderMode = RenderMode.COMPASS

 
activateLocationComponent method initializes the component and needs to be called before any other operations are performed.
We used locationComponent?.isLocationComponentEnabled method for enabling location updates.


locationComponent?.cameraMode camera mode determines how the camera will track the user’s current location, in this case, we set it not to track the user’s location, that way we set CameraMode.NONE. If you need another mode for your camera, you can visit the official documentation here, where the different modes for the camera are shown.


locationComponent?.renderMode we finally have the renderMode method, which is how the user’s current location will be rendered on the map.
We have already set the configurations for enabling the location component to our map, now we’re going to get the last known location and move the camera to it.


Add these lines of code just below the locationComponent?.renderMode = RenderMode.COMPASS line
 

val lastLocation = locationComponent?.lastKnownLocation
if (lastLocation != null) {
   val lat = lastLocation.latitude
   val lng = lastLocation.longitude
   val location = LatLng(lat, lng) 
   moveCameraToLocation(location)
}
private fun moveCameraToLocation(location: LatLng) {
    val position = CameraPosition.Builder()
        .target(location) // the location where to camera will move
        .zoom(10.0) // the zoom of our map
        .tilt(20.0) // title in degrees
        .build()
    // animateCamera method let us move the camera map. Needs to parameters
    // the new position of the camera and the millisecond that will
    mapboxMap?.animateCamera(CameraUpdateFactory.newCameraPosition(position), 3000)
}

 
With the code above, we’re getting the user’s location and creating a CameraPosition.Builder that we’ll use to move the camera map to the location found. Now, run the project again to see how this works!
 
Mapbox
 

Step 5 – Searching addresses

Note: For this step of the tutorial, we won’t explain how coroutines work and the way they were implemented. We’ll only see how to search for addresses and how to handle the Places API response.
First, let’s create the variables that we’re going to need; add them above of onCreate() method
 

  // this is our view model
    private lateinit var mainViewModel: MainViewModel
    // the adapter that will handle the addresses found
    private lateinit var searchAddressAdapter: SearchAdapter
    // central point of our map
    private var centerLocation: LatLng? = null

 
Inside of onCreate() method, create the instance for the mainViewModel and then, let’s define our observe method as well.
 

mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
/** this observe will execute every time we have a response of our Places API request
If addresses list is empty cvAddresses will hide otherwise will be visible to the user with the addresses found
*/
mainViewModel.address.observe(this, Observer {
    cvAddresses.visibility = if(it.isEmpty()) View.GONE else View.VISIBLE
    searchAddressAdapter.updateAddressList(it)
})

 
Also, let’s create the adapter’s instance that will handle the address found.
 

// SearchListener: we’ll use this listener whenever user select an // address and here, we’ll add a new marker over the map
searchAddressAdapter = SearchAdapter(object : SearchListener {
    override fun onSelectAddress(address: CarmenFeature) {
//                  addMarker()
    }
})
// Here, we asign searchAddressAdapter as the adpater of the 
// RecylcerView that holds all the addresses returned by the request
rvAddressesList.adapter = searchAddressAdapter

 
The way the application works is as follows: on every character the user is typing, a new request has to be triggered in order to get the addresses that match with that letter or word; in that way, the Places API can give us results matching with the string that is being added by the user.
 

etAddress.addTextChangedListener(object : TextWatcher {
    override fun afterTextChanged(s: Editable?) {
        if (s.toString().isEmpty()) {
            searchAddressAdapter.updateAddressList(ArrayList())
            cvAddresses.visibility = View.GONE
            return
        }
        mainViewModel.fetchAddress(s.toString(), centerLocation)
    }
// ...
})

 
afterTextChanged(s: Editable?) is the function that we’ll use to send a request on every character typed by the user. There are two things that are inside this function

  1. Cleaning the search adapter when there isn’t any text on the edit text and hiding the addresses list.
  2. Starting the request using the string on edit text in conjunction with centerLocation.

 
afterTextChanged
 
When a user clicks on etAddress, we have to get the location that the camera is pointing at.
Let’s add a touch listener to the Edit Text so when a user clicks on the address field we’ll get the values of centerLocation.
 

/** When the user clicks on “edit text” to start typing an address, we need to get the central position in the visible region of our map  */
etAddress.setOnTouchListener { v, event ->
    if(MotionEvent.ACTION_UP == event.action) {
        centerLocation = mapboxMap?.cameraPosition?.target
    }
    false
}

 
Now, you are ready to run the application and start looking for addresses.
 
Mapbox looking for addresses
 

Step 6 – Looking for addresses near the user’s current location

If you run the application and look for an address, you can see the response is returning addresses far from your location or even from another continent; this behavior is completely fine and it’s because we haven’t configured the Places API, which just looks in a certain area by default.
Go to the MainRepository class, then navigate to the getMapboxGeocoding() method, here we’re creating a MapboxGeocoding object needed to search for addresses; this object must have two required parameters, the Mapbox access token, and the location query. 


To start looking for addresses near the user’s location you can use the proximity() method, passing in the user’s location as a Point object to bias results to around the location.

First, let’s validate if centerLocation isn’t null, to prevent the application from crashing due to a NullPointerException.
 

if (centerLocation != null) {
// create a new Point object, needs longitude and latitude that we can get from centerLocation variable
    val point = Point.fromLngLat(centerLocation.longitude, centerLocation.latitude)
    builder.proximity(point)
}

 
Your getMapboxGeocoding() function has to look like this:
 
getMapboxGeocoding() function
 

Step 7 – add a marker

Once we get the list of addresses, we’re going to print a marker over the map when a user clicks on an address. 


On the latest version of Mapbox, for drawing any visual property over the map, we have to create them as Annotations.


According to Mapbox’s documentation, Annotation simplifies the way to set and adjust the visual properties of annotations on a Mapbox map;  “annotations” means circles, polygons, lines, text, and icons that we, as developers, can draw over the map.


For this tutorial, we’ll use the Symbol annotation. First, in order to create a new symbol, let’s create a symbol manager, add this line just above the onCreate() method.
 

private lateinit var symbolManager: SymbolManager

 
This manager will let us create symbols over the map with specific methods and properties that we can use to configure every symbol.
Next, let’s create a new function in which we’ll create the instance of symbolManager.
 

private fun initMarkerIconSymbolManager(loadedMapStyle: Style) {
// .addImage() is the method we use to set the image for our symbol
    loadedMapStyle.addImage(
        "marker_icon", BitmapFactory.decodeResource(
            this.resources, R.drawable.red_marker
        )
    )
// sumboManager needs the mapView, the instance of our
// mapbox map and the style that we chose for our map
    symbolManager = SymbolManager(mapView!!, mapboxMap!!, loadedMapStyle)
// true, the icon will be visible even if it collides with other previously drawn symbols.
    symbolManager.iconAllowOverlap = true
// true, other symbols can be visible even if they collide with the icon.
    symbolManager.iconIgnorePlacement = true
}

 
Now, go to the onMapReady() function and inside the setStyle block add initMarkerIconSymbolManager function. When the map is ready to be used, a new SymbolManager instance will be created, and we’ll be allowed to set symbols on that new map instance.
 
map instance
 
Next, create an addMarker() function that will receive a Point object as a parameter; inside this function, we’re creating a new SymbolOption object, this object will let us define the new symbol that will be created.
 

private fun addMarker(latLng: LatLng) {
    val symbolOptions = SymbolOptions()
    symbolOptions
        // set the location on which the marker will be set
        .withLatLng(latLng)
        // the image id
        .withIconImage("marker_icon")
        // the icon size
        .withIconSize(0.3f)
        // Offset distance of icon from its anchor
        .withIconOffset(arrayOf(0f, -7f))
    symbolManager.create(symbolOptions)
}

 
Also, add this function, it will work to hide the keyboard once the user selects an address from the list.
 

/**
* this function will close the keyboard automatically
*/
private fun hideKeyboard() {
  val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
  imm.hideSoftInputFromWindow(etAddress.windowToken, 0)
}

 
Finally, override the onSelectAddress() method of the SearchListener that belongs to the SearchAdapter.
 

searchAddressAdapter = SearchAdapter(object : SearchListener {
    override fun onSelectAddress(address: CarmenFeature) {
        val point = address.geometry() as Point
  // create a new LatLng object
        val latLng = LatLng(point.latitude(), point.longitude())
        cvAddresses.visibility = View.GONE
        addMarker(latLng)
        moveCameraToLocation(latLng)
      // clean text from editText and then hide the keyboard automatically
       etAddress.setText("")
       hideKeyboard()
    }
})

 
And that’s it! Now, run your application, search for an address, and select one of the options on the list.
 
Mapbox gif
 

EOF

You just learned the basics of adding Mapbox to your project! I hope that you enjoyed this tutorial as much as I did. I invite you to share this tutorial so more people understand Mapbox. If you have questions, suggestions or you think something is missing in this tutorial, or you just want to share your thoughts, leave a comment below, I’d appreciate the feedback. Happy coding!
 
Cover image: Bobby Sudekum
 

Share on twitter
Share on facebook
Share on linkedin
Share on tumblr