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.
client-id/client-secret credentials - safe to use directly from mobile/IoT apps. See the Backend Examples for production-ready proxy implementations.AndroidManifest.xml as a <meta-data> element.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.
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" />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))
}
}
}
}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.
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
}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
TextToSpeechto speak each maneuver fromtrip.legs[0].maneuvers[].instructionas 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.