Android Example

A Kotlin Android application that renders a MapLibre GL Native vector tile map and adds places search. Uses a custom OkHttp interceptor for tile authentication.

Prerequisites

  • Android Studio Ladybug (2024.2+)
  • Android API 24+ (Android 7.0) device or emulator
  • A Geog API key (create one in the console)

Quick Start

git clone https://github.com/geogdev/examples.git

Open examples/mobile/android-maplibre in Android Studio. Add your API key to local.properties:

GEOG_API_KEY=your-api-key-here

Click Sync Now when prompted for Gradle sync, then run the app.

Project Structure

FileDescription
local.propertiesAPI key storage (git-ignored) — injected via Gradle buildConfigField
app/build.gradle.ktsModule config — reads API key from local.properties into BuildConfig
app/src/main/java/.../api/GeogService.ktAPI client — OkHttp-based token exchange, caching, and places search
app/src/main/java/.../MapFragment.ktFragment — MapLibre map with programmatic style and OkHttp auth interceptor
app/src/main/java/.../PlacesSearchFragment.ktFragment — search UI, results list, and marker management
app/src/main/java/.../api/Place.kt@Serializable data model for search results

How It Works

API Key Configuration

The API key is stored in local.properties (git-ignored) and injected into the app at build time through Gradle:

// app/build.gradle.kts
android {
    defaultConfig {
        buildConfigField(
            "String",
            "GEOG_API_KEY",
            "\"${localProperties["GEOG_API_KEY"]}\""
        )
    }
}

Access it in code as BuildConfig.GEOG_API_KEY.

Token Exchange

GeogService exchanges the API key for a short-lived access token using OkHttp:

⚠️ Production note: This example embeds the API key via BuildConfig for simplicity. In a production app distributed to end users, the key can be extracted from the APK. Use a backend token-proxy instead — your server holds the long-lived key and returns short-lived tokens to the app. See Token Exchange API for details.

object GeogService {
    private var cachedToken: String? = null
    private var tokenExpiresAt: Long = 0

    suspend fun getAccessToken(): String {
        val now = System.currentTimeMillis()
        cachedToken?.let { token ->
            if (now < tokenExpiresAt) return token
        }

        val response = client.newCall(
            Request.Builder()
                .url("https://api.geog.dev/v1/auth/token")
                .header("Authorization", "Bearer ${BuildConfig.GEOG_API_KEY}")
                .post("""{"ttl":3600,"scope":"tiles:read places:read"}""".toRequestBody("application/json".toMediaType()))
                .build()
        ).await()

        val data = Json.decodeFromString<TokenResponse>(response.body!!.string())
        cachedToken = data.accessToken
        tokenExpiresAt = now + (data.expiresIn * 1000 * 0.8).toLong()
        return data.accessToken
    }
}

Custom OkHttp Interceptor

MapLibre GL Native for Android uses OkHttp under the hood. A custom interceptor attaches the Bearer token to all tile requests:

val client = OkHttpClient.Builder()
    .addInterceptor { chain ->
        val request = chain.request()
        if (request.url.host == "api.geog.dev") {
            chain.proceed(
                request.newBuilder()
                    .header("Authorization", "Bearer $accessToken")
                    .build()
            )
        } else {
            chain.proceed(request)
        }
    }
    .build()

HttpRequestUtil.setOkHttpClient(client)

Coroutines

All API calls are suspend functions and run within lifecycleScope for structured concurrency.

Full Source

github.com/geogdev/examples/tree/main/mobile/android-maplibre

See Also