DocsAndroid (Kotlin)

Android Navigator - Kotlin + Google Maps

We'll use Google Maps SDK for the map and FusedLocationProviderClient for accurate location. PickPoint handles address search and routing. Network calls use OkHttp with Kotlin coroutines.

Never embed your PickPoint API key in client-side code. Browser JavaScript, iOS/Android apps, and Flutter bundles can all be inspected or decompiled. Always route geocoding and routing calls through your own backend. Exception: device tracking uses per-device client-id/client-secret credentials - safe to use directly from mobile/IoT apps. See the Backend Examples for production-ready proxy implementations.
Prerequisites: Android Studio, a Google Maps API key (Maps SDK for Android), and a PickPoint API key. Add the Google Maps key to AndroidManifest.xml as a <meta-data> element.
1

Dependencies

// build.gradle.kts (app)
dependencies {
    implementation("com.google.android.gms:play-services-maps:19.0.0")
    implementation("com.google.android.gms:play-services-location:21.3.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    // Optional - live tracking
    implementation("io.socket:socket.io-client:2.1.0")
}

Add ACCESS_FINE_LOCATION to AndroidManifest.xml and request the permission at runtime on first launch.

2

Add the map to the layout

// layout/activity_main.xml
<fragment
    android:id="@+id/map"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
3

Initialise the map and get the user's location

Use FusedLocationProviderClient.lastLocation for a quick initial fix. For continuous updates during navigation, use requestLocationUpdates:

class MainActivity : AppCompatActivity(), OnMapReadyCallback {
    private lateinit var map: GoogleMap
    private lateinit var fusedLocation: FusedLocationProviderClient
    private var origin: LatLng? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        fusedLocation = LocationServices.getFusedLocationProviderClient(this)

        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }

    override fun onMapReady(googleMap: GoogleMap) {
        map = googleMap
        map.isMyLocationEnabled = true    // requires ACCESS_FINE_LOCATION permission

        fusedLocation.lastLocation.addOnSuccessListener { loc ->
            if (loc != null) {
                origin = LatLng(loc.latitude, loc.longitude)
                map.moveCamera(CameraUpdateFactory.newLatLngZoom(origin!!, 14f))
            }
        }
    }
}
4

Address search

Call /v2/address/search on each keystroke (debounce with Flow.debounce(250)). Pass the current visible region as viewbox:

data class GeoResult(val lat: String, val lon: String, val display_name: String)

suspend fun searchAddress(query: String, region: LatLngBounds): List<GeoResult> =
    withContext(Dispatchers.IO) {
        val url = HttpUrl.Builder()
            .scheme("https").host("api.pickpoint.io")
            .addPathSegments("v2/address/search")
            .addQueryParameter("q",       query)
            .addQueryParameter("limit",   "6")
            .addQueryParameter("viewbox",
                "${region.southwest.longitude},${region.southwest.latitude}," +
                "${region.northeast.longitude},${region.northeast.latitude}")
            .build()

        val req = Request.Builder().url(url)
            .addHeader("X-Api-Key", API_KEY).build()

        val body = OkHttpClient().newCall(req).execute().body!!.string()
        Gson().fromJson(body, Array<GeoResult>::class.java).toList()
    }

Show results in a RecyclerView or AutoCompleteTextView. On selection, use the result's lat/lon as the route destination.

5

Fetch the route

POST to /v2/route. Decode the encoded polyline from trip.legs[0].shape using PolyUtil.decode() from the Maps Utils library:

data class Location(val lat: Double, val lon: Double)
data class RouteBody(val locations: List<Location>, val costing: String)

suspend fun fetchRoute(origin: LatLng, dest: LatLng): List<LatLng> =
    withContext(Dispatchers.IO) {
        val body = Gson().toJson(
            RouteBody(
                locations = listOf(
                    Location(origin.latitude, origin.longitude),
                    Location(dest.latitude,   dest.longitude)
                ),
                costing = "auto"
            )
        )
        val req = Request.Builder()
            .url("https://api.pickpoint.io/v2/route")
            .post(body.toRequestBody("application/json".toMediaType()))
            .addHeader("X-Api-Key", API_KEY)
            .build()

        val resp     = OkHttpClient().newCall(req).execute().body!!.string()
        val shape    = JSONObject(resp).getJSONObject("trip")
                                       .getJSONArray("legs")
                                       .getJSONObject(0)
                                       .getString("shape")
        decodePolyline(shape)   // use PolyUtil.decode() from maps-utils
    }
6

Draw the route and fit the camera

// Draw the route and fit the camera
fun drawRoute(coords: List<LatLng>) {
    map.addPolyline(
        PolylineOptions()
            .addAll(coords)
            .width(10f)
            .color(Color.parseColor("#008080"))
    )
    val bounds  = LatLngBounds.builder().apply { coords.forEach { include(it) } }.build()
    map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 96))
}

What's next

  • Voice guidance - use TextToSpeech to speak each maneuver from trip.legs[0].maneuvers[].instruction as the user passes each waypoint.
  • Re-routing - subscribe to location updates; if the user deviates more than ~50 m from the polyline (use PolyUtil.isLocationOnPath), fetch a new route.
  • Live tracking - use the Socket.IO Java client to stream location updates. See the Device Tracking guide.

← Navigator AppiOS →Flutter →