Appearance
Android SDK
Embed interactive Layota maps into your Android app with a native Kotlin API.
Requirements
- Android API 23+ (Android 6.0)
- Kotlin 1.9+
- Jetpack Compose (optional)
Installation
Source distribution for now
The Android SDK is not published to Maven Central yet — contact us to get the module source. A Maven release is planned.
The SDK ships as a standard Gradle library module. Once you have the source, include it in your project's settings.gradle.kts:
kotlin
include(":layota-map-sdk")
project(":layota-map-sdk").projectDir = file("../layota-map-sdk")Then add the dependency to your module's build.gradle.kts:
kotlin
dependencies {
implementation(project(":layota-map-sdk"))
}Quick Start
XML Layout
kotlin
import app.layota.sdk.*
class MapActivity : AppCompatActivity() {
private lateinit var mapView: LayotaMapView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_map)
mapView = findViewById(R.id.map_view)
mapView.initialize(LayotaMapOptions(
projectId = "your-project-id",
apiKey = "sk_..."
))
val map = mapView.map!!
map.on(SdkEventType.AREA_CLICK) { event ->
if (event is LayotaEvent.AreaClick) {
Log.d("Map", "Tapped area: ${event.area.title}")
}
}
lifecycleScope.launch {
map.ready()
map.selectArea("coffee-shop").focusOn("coffee-shop")
}
}
}xml
<app.layota.sdk.LayotaMapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />Jetpack Compose
kotlin
import app.layota.sdk.*
@Composable
fun MapScreen() {
var map by remember { mutableStateOf<LayotaMap?>(null) }
LayotaMapCompose(
options = LayotaMapOptions(
projectId = "your-project-id",
apiKey = "sk_...",
language = "en"
),
modifier = Modifier.fillMaxSize(),
onMapReady = { map = it }
)
LaunchedEffect(map) {
map?.let {
it.on(SdkEventType.AREA_CLICK) { event ->
if (event is LayotaEvent.AreaClick) {
Log.d("Map", "Tapped: ${event.area.title}")
}
}
it.ready()
it.selectArea("entrance")
}
}
}Configuration
kotlin
val options = LayotaMapOptions(
projectId = "proj-uuid", // Required
apiKey = "sk_...", // Required if project has allowed domains
theme = MapTheme.DARK, // LIGHT, DARK, BLUE, AUTO
language = "ru", // Language code
levelId = "floor-2", // Start on this level (id, key, or name)
areaId = "cafe", // Focus this area on load
markerId = "exit-north", // Focus this marker on load
baseUrl = "https://layota.app", // Override for self-hosted
)Methods
WARNING
All SDK methods must be called from the main thread. The SDK enforces this with an internal assertMainThread() check.
Lifecycle
kotlin
// Wait for the map to finish loading (suspending function)
map.ready()
// Clean up WebView and all event listeners
map.destroy()Level Control
kotlin
// Switch level by id, key (slug), or name
map.selectLevel("floor-2")
// Fetch all visible levels (suspending function)
val levels: List<Level> = map.getLevels()
// Level(id, name)Area & Marker Selection
kotlin
// Select by id, key (slug), or title — opens the info card
map.selectArea("cafe")
map.selectArea("cafe", levelId = "floor-1")
map.selectMarker("exit-north")
map.selectMarker("exit-north", levelId = "floor-1")
// Close the info card and deselect
map.clearSelection()Navigation
kotlin
// Pan and zoom to area or marker — switches level if needed
map.focusOn("cafe")
// Reset zoom and pan to default
map.resetView()
// Set zoom (throws LayotaInvalidScaleException if scale <= 0)
map.setZoom(1.5)Search
kotlin
// Open the search panel and filter results by query
map.search("coffee")
// Clear the search
map.search("")Routing
kotlin
val origin = RouteEntity(id = "entrance", type = RouteEntity.EntityType.AREA, levelId = "floor-1")
val destination = RouteEntity(id = "cafe", type = RouteEntity.EntityType.AREA, levelId = "floor-2")
map.buildRoute(from = origin, to = destination)
map.clearRoute()Language
kotlin
// Get available languages
val (languages, current) = map.getLanguages()
// Language(code, name)
// current: currently active language code
// Switch language at runtime
map.setLanguage("de")Areas & Markers Data
kotlin
// All areas across all levels
val allAreas: List<Area> = map.getAreas()
// Areas for a specific level (by id, key, or name)
val floorAreas: List<Area> = map.getAreas("floor-1")
// Same for markers
val allMarkers: List<Marker> = map.getMarkers()
val floorMarkers: List<Marker> = map.getMarkers("floor-1")Project Data
kotlin
val project: ProjectInfo = map.getProject()
// ProjectInfo(id, levels: List<ProjectLevel>)
// ProjectLevel(id, name, areas: List<Area>, markers: List<Marker>)Method Chaining
kotlin
map.selectLevel("floor-1")
.selectArea("cafe")
.focusOn("cafe")
.setZoom(1.5)Events
Subscribe
kotlin
val subscription = map.on(SdkEventType.AREA_CLICK) { event ->
if (event is LayotaEvent.AreaClick) {
println("Clicked: ${event.area.title}")
}
}
map.on(SdkEventType.LEVEL_CHANGE) { event ->
if (event is LayotaEvent.LevelChange) {
println("Switched to: ${event.level.name}")
}
}
map.on(SdkEventType.ROUTE_BUILT) { event ->
if (event is LayotaEvent.RouteBuilt) {
println("Distance: ${event.route.totalDistance}")
}
}Subscribe Once
kotlin
map.once(SdkEventType.READY) { println("Map is ready") }Unsubscribe
kotlin
// Cancel a specific subscription
subscription.cancel()
// Remove ALL handlers for an event type
map.off(SdkEventType.AREA_CLICK)Event Reference
| Event | Payload | Description |
|---|---|---|
READY | — | Map fully loaded |
LEVEL_CHANGE | LayotaEvent.LevelChange(level) | Level switched |
AREA_CLICK | LayotaEvent.AreaClick(area) | User tapped an area |
MARKER_CLICK | LayotaEvent.MarkerClick(marker) | User tapped a marker |
AREA_SELECT | LayotaEvent.AreaSelect(area) | Area selected programmatically |
MARKER_SELECT | LayotaEvent.MarkerSelect(marker) | Marker selected programmatically |
ROUTE_BUILT | LayotaEvent.RouteBuilt(route, levelId) | Route calculated |
ROUTE_NOT_FOUND | LayotaEvent.RouteNotFound | No route found |
LANGUAGE_CHANGE | LayotaEvent.LanguageChange(language) | Language switched via setLanguage() |
ERROR | LayotaEvent.Error(message) | SDK error |
Exceptions
| Exception | When |
|---|---|
LayotaTimeoutException | ready() or getLevels() timed out |
LayotaDestroyedException | Method called after destroy() |
LayotaInvalidScaleException | setZoom() called with scale <= 0 |
LayotaDecodingException | Response from map could not be parsed |
LayotaLoadException | WebView failed to load the map URL |
Architecture
The SDK loads https://layota.app/embed/{projectId}?native=1 in a WebView. The ?native=1 parameter enables the native bridge — events are sent via window.layotaAndroid.postMessage() using @JavascriptInterface.
Commands are sent to the web page via WebView.evaluateJavascript(). Commands issued before the map finishes loading are queued and flushed once READY is received. All messages from the JavaScript bridge are dispatched to the main thread via Handler(Looper.getMainLooper()).