Skip to main content

Android (Kotlin)

Show the map

Let's start by using the WebView class to set up our web map and add it to activity.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent"></WebView>

</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
package com.example.webviewintegration

import android.os.Bundle
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity

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

val webView = findViewById<WebView>(R.id.webView)
WebMap(webView)
}
}

class WebMap(private val webView: WebView) {
init {
webView.settings.javaScriptEnabled = true
webView.loadUrl("https://pcmap-website-demo.netlify.app/")
}
}

To load the web map we should allow Internet connection in the application settings.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.webviewintegration">

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

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WebViewIntegration">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

Handle messages from the map

The application could receive messages from the web map to react accordingly.

The message from the web map comes in JSON format. It always contains the messageType field and some optional fields depending on the message type. Below you can see the error and mapReady messages:

Error message
{
"messageType": "error",
"error": "Error details will be here..."
}
Map ready message
{
"messageType": "mapReady"
}

For being able to work with the strongly typed data and serialize Kotlin objects to/from JSON format we are going to add one of the serialization libraries. For Kotlin it could be kotlinx.serialization library. Let's add it to the project:

build.gradle
plugins {
...
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
}

dependencies {
...
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
}

Now we need to add the JavaScript interface and implement the postMessage function that will be used by the web map to send its messages to our host application. Below you can see an example where we handle the mapReady and error events. Both of them cause print a message to the output.

MainActivity.kt
package com.example.webviewintegration

import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

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

val webView = findViewById<WebView>(R.id.webView)
WebMap(webView)
}
}

class WebMap(private val webView: WebView) {
init {
webView.addJavascriptInterface(WebMapInterface(this), "android")
webView.settings.javaScriptEnabled = true
webView.loadUrl("https://pcmap-website-demo.netlify.app/")
}
}

@Serializable
data class WebMapMessage(val messageType: String, val error: String? = null)

class WebMapInterface(private val webMap: WebMap) {
@JavascriptInterface
fun postMessage(message: String) {
val (messageType, error) = Json.decodeFromString<WebMapMessage>(message)

when (messageType) {
"mapReady" -> {
println("Map is ready!")
}
"error" -> {
println("Error: $error")
}
}
}
}

Interaction

To interact with the map and use its Methods we can extend WebMap class with a function that evaluates a JavaScript code in the WebView. Let's implement the showOrdinal function that switch the map view to show the required ordinal:

class WebMap(private val webView: WebView) {
init {
webView.addJavascriptInterface(WebMapInterface(this), "android")
webView.settings.javaScriptEnabled = true
webView.loadUrl("https://pcmap-website-demo.netlify.app/")
}

fun showOrdinal(ordinal: Int) {
val script = "app.map.showOrdinal($ordinal)"
webView.post {
run() {
webView.evaluateJavascript(script, null)
}
}
}
}

Apply scenarios

The application can ask the web map to apply some scenarios.

The "One POI" scenario

The web map supports the onePOI scenario that, after applying, selects the required POI (Point Of Interest).

note

Other scenarios could be implemented by request.

Below, we prepare WebMapScenario structure that will be serialized into JSON and sent to the web map once the mapReady message received.

MainActivity.kt
package com.example.webviewintegration

import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

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

val webView = findViewById<WebView>(R.id.webView)
WebMap(webView)
}
}

class WebMap(private val webView: WebView) {
init {
webView.addJavascriptInterface(WebMapInterface(this), "android")
webView.settings.javaScriptEnabled = true
webView.loadUrl("https://pcmap-website-demo.netlify.app/")
}

fun showOrdinal(ordinal: Int) {
val script = "app.map.showOrdinal($ordinal)"
webView.post {
run() {
webView.evaluateJavascript(script, null)
}
}
}

fun showOnePOI(poi: String) {
val scenario = WebMapScenario(scenarioType = "onePOI", poi = poi, zoomLevel = 20)
applyScenario(scenario)
}

private inline fun <reified T> applyScenario(scenario: T) {
val json = Json.encodeToString(scenario)
val script = "app.scenarios.apply($json)"
webView.post {
run() {
webView.evaluateJavascript(script, null)
}
}
}
}

@Serializable
data class WebMapMessage(val messageType: String, val error: String? = null)

@Serializable
data class WebMapScenario(
val scenarioType: String,
val poi: String,
val showSearch: Boolean? = null,
val showLevelChoice: Boolean? = null,
val showCallout: Boolean? = null,
val showZoomControl: Boolean? = null,
val zoomLevel: Int? = null
)

class WebMapInterface(private val webMap: WebMap) {
@JavascriptInterface
fun postMessage(message: String) {
val (messageType, error) = Json.decodeFromString<WebMapMessage>(message)

when (messageType) {
"mapReady" -> {
webMap.showOnePOI("150")
}
"error" -> {
println("Error: $error")
}
}
}
}

User location

It is possible to tell the map where the user is right now. Below you can see an example of doing that.

package com.example.webviewintegration

import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

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

val webView = findViewById<WebView>(R.id.webView)
val webMap = WebMap(webView)
}
}

class WebMap(private val webView: WebView) {
init {
webView.addJavascriptInterface(WebMapInterface(this), "android")
webView.settings.javaScriptEnabled = true
webView.loadUrl("https://pcmap-website-demo.netlify.app/")
}

fun setUserPosition(latitude: Double, longitude: Double, ordinal: Int?, heading: Double?) {
val userPosition = WebMapUserPosition(latitude, longitude, ordinal, heading)
val json = Json.encodeToString(userPosition)
val script = "app.map.setUserPosition($json)"
webView.post {
run() {
webView.evaluateJavascript(script, null)
}
}
}
}

@Serializable
data class WebMapMessage(val messageType: String, val error: String? = null)

@Serializable
data class WebMapUserPosition(val latitude: Double, val longitude: Double, val ordinal: Int? = null, val heading: Double? = null)

class WebMapInterface(private val webMap: WebMap) {
@JavascriptInterface
fun postMessage(message: String) {
val (messageType, error) = Json.decodeFromString<WebMapMessage>(message)

when (messageType) {
"mapReady" -> {
webMap.setUserPosition(
latitude = 47.45232728284989,
longitude = 8.56221512953916,
ordinal = 2,
heading = 91.5
)
}
"error" -> {
println("Error: $error")
}
}
}
}

Latest map state

Sometimes you need to know the current state of the map. To make it possible you need to do a few steps:

  1. Enable the map state storing at the query part of the WebView URL by adding pcm_ms=1 query parameter to the web map URL:
https://pcmap-website-demo.netlify.app?pcm_ms=1
  1. Watch for changes of that URL at the host application:
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val webView = findViewById<WebView>(R.id.webView)
webView.webViewClient = MapWebViewClient()
}
}

private class MapWebViewClient : WebViewClient() {
override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) {
super.doUpdateVisitedHistory(view, url, isReload)

println("Web map URL with state: $url")
}
}

You can use the url parameter of the doUpdateVisitedHistory function to obtain the current WebView URL which also contains the web map state. That URL could be send to someone or used to re-initilize the map in the same state.