Skip to Content
Integration Guide

Integration Guide

This guide provides detailed instructions for integrating all features of the Lazarillo Android SDK into your application. By the end, you will have a fully functioning map with location tracking, indoor positioning, routing, and accessibility support.

Before starting, complete the steps in the Installation Guide and ensure you have an API key and a Place ID from the Lazarillo team.


Table of Contents

  1. Project Setup
  2. Permissions
  3. SDK Initialization
  4. Map Integration
  5. Location Tracking
  6. Navigation and Routing
  7. Indoor Positioning
  8. Place Markers
  9. Floor Selection
  10. Route Styling
  11. Accessibility
  12. Customization
  13. Multi-Language Support
  14. Lifecycle Management

Project Setup

Gradle Configuration

Step 1. Add JitPack to your repositories (settings.gradle.kts):

dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url = uri("https://jitpack.io") } } }

Step 2. Add the SDK dependency (module-level build.gradle.kts):

dependencies { implementation("com.github.lazarilloapp:sdkandroidlibrary:<VERSION>") }

Replace <VERSION> with the latest release tag. Find available versions on JitPack .

Step 3. Ensure Java 17 compatibility:

android { compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } }

Step 4. Set the minimum SDK version:

android { defaultConfig { minSdk = 23 // Android 6.0 minimum } }

Permissions

AndroidManifest.xml

Add the following permissions to your AndroidManifest.xml:

<!-- Network access --> <uses-permission android:name="android.permission.INTERNET" /> <!-- GPS location --> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- Bluetooth beacons (indoor positioning) --> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <!-- Legacy Bluetooth for API < 31 --> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <!-- Motion detection for location accuracy --> <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />

For a full explanation of each permission and why it is required, see the Permissions Guide.

Runtime Permission Handling

Declare and request permissions at runtime before using any location features:

import android.Manifest import android.content.pm.PackageManager import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat class MainActivity : AppCompatActivity() { private val locationPermissionRequest = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { permissions -> val fineLocationGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false val coarseLocationGranted = permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false if (fineLocationGranted || coarseLocationGranted) { // Location permissions granted -- proceed with location features initializeLocationTracking() } else { // Permissions denied -- inform user that location features are unavailable showPermissionDeniedMessage() } } private fun checkAndRequestPermissions() { val permissions = mutableListOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACTIVITY_RECOGNITION ) // Bluetooth permissions for API 31+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { permissions.add(Manifest.permission.BLUETOOTH_SCAN) permissions.add(Manifest.permission.BLUETOOTH_CONNECT) } val needsPermission = permissions.any { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED } if (needsPermission) { locationPermissionRequest.launch(permissions.toTypedArray()) } else { initializeLocationTracking() } } }

The “allow only while the app is in use” option for location services is sufficient for foreground SDK usage.


SDK Initialization

Initialize the SDK as early as possible, before creating any maps or requesting data. The Application.onCreate() method is the recommended place.

Basic Initialization

LzSdkManager.initialize( context = applicationContext, apiKey = "YOUR_API_KEY", targetParentPlaceId = "YOUR_PLACE_ID", enableLogging = BuildConfig.DEBUG )

With Backend Validation

For strict API key validation, pass validateWithBackend = true. This adds one network call during initialization but confirms your key is active before the user interacts with the map.

LzSdkManager.initialize( context = applicationContext, apiKey = "YOUR_API_KEY", targetParentPlaceId = "YOUR_PLACE_ID", validateWithBackend = true, enableLogging = BuildConfig.DEBUG )

Changing Venues at Runtime

If your app supports multiple venues, switch between them without restarting:

// Clear old venue data LzSdkManager.clearSubPlacesCache("old-venue-id") // Set the new venue LzSdkManager.setParentPlace("new-venue-id")

Verifying Initialization

Before calling any other SDK method, confirm the SDK is ready:

if (LzSdkManager.isSdkActive()) { // SDK is ready for use } else { // SDK not initialized -- call initialize() first }

Map Integration

Adding the Map to Your Layout

Step 1. Add an XML container in your layout file:

<FrameLayout android:id="@+id/map_container" android:layout_width="match_parent" android:layout_height="400dp" />

Step 2. Create and configure the map in your Fragment or Activity:

val mapConfig = LazarilloMapConfig().apply { parentPlaceId = "YOUR_PLACE_ID" zoom = 17 minZoom = 15.0 maxZoom = 20.0 hideZoomIn = false hideZoomOut = false hideZoomToLocation = false hideFloorSelector = false showCompass = true } lzMap = LazarilloMap( id = "my-map", config = mapConfig, passedContext = requireContext(), inflater = layoutInflater, lifecycleScope = viewLifecycleOwner.lifecycleScope, positionRepository = null ) { // Map is ready onMapReady() } binding.mapContainer.addView(lzMap)

For a complete working example, see Creating a Map.

Map Without a Venue (Coordinate-Based)

If you do not have a parentPlaceId, center the map on geographic coordinates instead:

val mapConfig = LazarilloMapConfig().apply { center = LatLng(40.4168, -3.7038) zoom = 15 minZoom = 10.0 maxZoom = 20.0 }

Camera Controls

// Set pitch (tilt angle) lzMap?.setPitch(45.0) // Set bearing (rotation) lzMap?.setBearing(90.0) // Get current camera state val position = lzMap?.getCameraPosition() val currentZoom = position?.zoom val currentTilt = position?.tilt

Location Tracking

Location tracking requires building a position repository and connecting it to the map. The repository merges GPS and beacon data automatically.

Setting Up the Position Repository

private val repoBuilder = RepoBuilder() private suspend fun setupLocationTracking() { repoBuilder.buildPositionStatusRepository( context = requireContext(), parentPlaceId = "YOUR_PLACE_ID", // Required for indoor positioning useMotionDetection = true, // Improves accuracy with step detection useCompassSensors = true, // Enables bearing for navigation route = null, // Set a route later if needed repoScope = viewLifecycleOwner.lifecycleScope ) }

Connecting Location to the Map

Call this inside the map-ready callback to display the user’s position and start following them:

private fun onMapReady() { lifecycleScope.launch { setupLocationTracking() // Get the built repository val positionRepo = repoBuilder.getRoutingStatusRepoReference() positionRepo?.let { repo -> // Connect to the map lzMap?.setLocationProvider(repo) // Show user position marker lzMap?.toggleLocationComponent(true) { // Location component activated } // Follow the user's location lzMap?.followUserLocation( value = true, pitch = 45.0, zoom = 18.0 ) } } }

Listening to Location Updates

Collect from locatorResultFlow to react to every location change:

lifecycleScope.launch { positionRepo.locatorResultFlow.collect { data -> data?.location?.let { location -> if (location.isIndoor) { println("Indoor: floor=${location.floor}, building=${location.building}") } else { println("Outdoor: ${location.latitude}, ${location.longitude}") } } } }

Tracking Configuration

For navigation apps, configure tracking reactivation to prevent accidental deactivation when a user pinch-zooms the map:

val mapConfig = LazarilloMapConfig().apply { trackingConfig = TrackingConfig( enableReactivation = true, reactivationDelayMs = 500L, preserveTrackingParams = true, maxReactivationAttempts = 10, enableDebugLogging = BuildConfig.DEBUG ) }

Calculating a Route

Routes can start from a coordinate or a named place, and end at any combination of coordinate or place ID:

lifecycleScope.launch { // From coordinates val start = LzLocation( building = null, floor = null, latitude = -33.4489, longitude = -70.6693 ) // To a place by ID val destination = IdPlaceLocation(id = "store-456", level = 1) val response = LzSdkManager.getRoute( initialLocation = start, finalLocation = destination, preferAccessibleRoute = false, language = "en" ) if (response.isSuccessful) { val routes = response.body() ?: return@launch // Routes may contain multiple options displayRouteOptions(routes) } }

Displaying a Route on the Map

fun displayRoute(route: SdkRoute) { // Store the active route LzSdkManager.setCurrentShowingRoute(route) // Render on the map lzMap?.addRoute( route = route, nextStepsLineStyle = PlainRouteLine(colorHex = "#7000FF", lineWith = 4f), prevStepsLineStyle = PlainRouteLine(colorHex = "#CCCCCC", lineWith = 4f) ) { println("Route rendered") } }

Real-Time Navigation

Connect location updates to route progress tracking. The SDK calculates the user’s status against the active route on every location update:

lifecycleScope.launch { positionRepo.locatorResultFlow.collect { data -> data?.let { positionData -> // Update route progress on the map lzMap?.newCurrentUserPosition(positionData) // Check navigation status when (positionData.status) { RoutingStatus.ON_ROUTE -> { // User is following the route } RoutingStatus.OFF_ROUTE -> { // User deviated -- consider recalculating } RoutingStatus.FINISHED -> { // User arrived at destination LzSdkManager.clearRoute() lzMap?.clearRoute() } else -> { } } } } }

Clearing a Route

Always clear both the SDK state and the map layer when a route is no longer needed:

LzSdkManager.clearRoute() lzMap?.clearRoute()

Accessible Routes

Pass preferAccessibleRoute = true to receive a route that avoids stairs and prioritizes ramps and elevators:

val response = LzSdkManager.getRoute( initialLocation = start, finalLocation = destination, preferAccessibleRoute = true, language = "en" )

Indoor Positioning

Indoor positioning uses Bluetooth beacons to determine the user’s position within a building.

Prerequisites

Before indoor positioning will work, confirm the following:

  • Beacons are physically installed in the venue
  • Beacon data is configured in the Lazarillo backend for your Place ID
  • Bluetooth permissions (BLUETOOTH_SCAN, BLUETOOTH_CONNECT) are granted at runtime
  • The correct parentPlaceId is passed to RepoBuilder

How It Works

  1. The SDK automatically scans for Bluetooth beacons (Eddystone, iBeacon)
  2. Detected beacons are triangulated to calculate a position
  3. Floor information is extracted from the beacon data
  4. The location system merges GPS and beacon data, giving priority to beacons when indoors

No additional code beyond RepoBuilder is needed. When you provide a parentPlaceId, beacon scanning is enabled automatically:

repoBuilder.buildPositionStatusRepository( context = requireContext(), parentPlaceId = "YOUR_PLACE_ID", // This enables beacon scanning useMotionDetection = true, useCompassSensors = true, route = null, repoScope = viewLifecycleOwner.lifecycleScope )

Testing Without Physical Beacons

During development, simulate beacon detection by injecting known beacon IDs:

LzSdkManager.setBeaconsIds(listOf( "beacon-id-1", "beacon-id-2", "beacon-id-3" ))

Warning: Remove all setBeaconsIds() calls before releasing to production. Simulated beacons will override real beacon scanning.


Place Markers

Place markers display stores, POIs, and other locations on the map with logos or text labels.

Adding Place Markers

lifecycleScope.launch { val subPlaces = LzSdkManager.getSubPlaces() subPlaces.forEach { place -> lzMap?.addPlaceMarker(place, markerSize = 240) { result -> result.onSuccess { marker -> println("Added marker for: ${place.name}") }.onFailure { error -> println("Failed: ${error.message}") } } } }

Managing Markers

// Get all markers val allMarkers = lzMap?.getPlaceMarkers() // Get markers for a specific floor val floorMarkers = lzMap?.getPlaceMarkersForFloor("floor-1") // Remove a specific marker lzMap?.removePlaceMarker("place-id") // Clear all place markers lzMap?.clearAllPlaceMarkers()

Custom Markers

Add a marker at any coordinate with a custom icon:

val options = SymbolOptions() .withLatLng(LatLng(40.4168, -3.7038)) .withIconImage("custom-icon") .withIconSize(1.0f) lzMap?.addMarker(options, floorId = "floor-1") { result -> result.onSuccess { markerId -> println("Custom marker added: $markerId") } }

Automatic Floor Management

Place markers are automatically shown or hidden when the active floor changes. No manual intervention is needed. Markers without a floorId (outdoor markers) are always visible regardless of the current floor.


Floor Selection

Multi-floor venues display a floor selector control when floors are available.

Programmatic Floor Changes

lzMap?.setFloorById("target-floor-id") { println("Floor changed successfully") // Markers are automatically updated }

Showing and Hiding the Floor Selector

Control the floor selector visibility through LazarilloMapConfig:

val mapConfig = LazarilloMapConfig().apply { hideFloorSelector = false // Show the floor selector }

The floor selector automatically populates with the venue’s available floors from the Lazarillo backend. No additional data setup is required.


Route Styling

The SDK supports four route line styles. Styles can be applied independently to the upcoming portion and the completed portion of a route.

Plain (Solid Color)

PlainRouteLine(colorHex = "#7000FF", lineWith = 4f)

Dashed

DashRouteLine(colorHex = "#7000FF", lineWith = 4f)

Gradient

GradientRouteLine(...)

Pattern (Bitmap)

PatternsRouteLine(...)

Using Different Styles for Route Progress

A common pattern is to render the upcoming portion with a bold color and the completed portion with a muted or dashed style:

lzMap?.addRoute( route = route, nextStepsLineStyle = PlainRouteLine(colorHex = "#7000FF", lineWith = 4f), // Upcoming prevStepsLineStyle = DashRouteLine(colorHex = "#CCCCCC", lineWith = 3f) // Completed ) { println("Route displayed") }

Accessibility

The SDK provides comprehensive accessibility support for screen readers (TalkBack). For a deeper dive into all accessibility features, see the Accessibility Guide.

Default Accessibility Texts

UI ElementString ResourceDefault Text
Zoom Inlz_map_zoom_in”Zoom in on the map”
Zoom Outlz_map_zoom_out”Zoom out on the map”
Location Buttonlz_map_current_location”Center map on your current location”
Floor Selectorlz_map_floor_selector”Select building floor to display”
Compasslz_map_compass”Reset map bearing to North”

Customizing via String Resources

Override SDK strings in your app’s res/values/strings.xml. Android will merge these with the SDK’s defaults, giving your values precedence:

<resources> <string name="lz_map_zoom_in">Increase map zoom level</string> <string name="lz_map_zoom_out">Decrease map zoom level</string> <string name="lz_map_current_location">Show my location on map</string> <string name="lz_map_floor_selector">Change building floor</string> </resources>

Customizing via AccessibilityConfig

For programmatic control at map creation time, use AccessibilityConfig objects in the map configuration. These take precedence over string resources:

val mapConfig = LazarilloMapConfig().apply { zoomInAccessibility = AccessibilityConfig( isAccessible = true, label = "Increase zoom level" ) userLocationAccessibility = AccessibilityConfig( isAccessible = true, label = "Center on my position" ) }

Hiding Elements from Screen Readers

Control screen reader visibility per element via boolean resources in res/values/bools.xml. Setting a value to true hides the element from the screen reader while keeping it visible and functional on screen:

<resources> <bool name="lz_map_hide_zoom_in_from_screen_reader">true</bool> <bool name="lz_map_hide_zoom_out_from_screen_reader">true</bool> <bool name="lz_map_hide_current_location_from_screen_reader">false</bool> <bool name="lz_map_hide_floor_selector_from_screen_reader">true</bool> </resources>

Location Button Callback

React to location button taps for custom accessibility announcements:

lzMap?.updateOnMyLocationButtonClickListener { result -> if (result.success) { val location = result.location announceForAccessibility("Map centered at your location") } else { announceForAccessibility("Could not determine your location: ${result.error}") } }

Customization

Map Styles

Map styles are managed remotely by the Lazarillo backend. Each venue can have custom styling including colors, icons, text sizes, and layer visibility. Style changes do not require code modifications or app updates.

Custom Location Icons

val mapConfig = LazarilloMapConfig().apply { // Drawable resource name (without extension) locationIcon = "my_custom_location_icon" // Or a URL (supports SVG via Coil) locationIcon = "https://example.com/icons/location.svg" // Icon that rotates to show bearing direction locationIconWithBearing = "my_directional_icon" }

Custom Compass Icon

val mapConfig = LazarilloMapConfig().apply { compassIcon = "https://example.com/icons/compass.png" showCompass = true }

Restricting Map Panning

Limit how far a user can pan away from the venue center. This is useful for kiosk applications or any scenario where keeping the user focused on a specific area is important:

val mapConfig = LazarilloMapConfig().apply { limitToRadius = 500.0 // Meters from center }

Multi-Language Support

Route Instructions

Pass the desired BCP 47 language code when requesting a route. The SDK returns turn-by-turn instructions in that language:

val response = LzSdkManager.getRoute( initialLocation = start, finalLocation = destination, language = "es" // Spanish instructions )

UI String Localization

Override SDK string resources for each locale using Android’s standard resource qualifier system. Android automatically selects the correct strings based on the device language.

English (res/values/strings.xml):

<string name="lz_map_zoom_in">Zoom in on the map</string>

Spanish (res/values-es/strings.xml):

<string name="lz_map_zoom_in">Aumentar zoom del mapa</string>

Portuguese (res/values-pt/strings.xml):

<string name="lz_map_zoom_in">Aumentar zoom do mapa</string>

You only need to override the strings relevant to your supported locales. Any string not overridden will fall back to the SDK’s built-in English default.


Lifecycle Management

LazarilloMap implements DefaultLifecycleObserver and automatically manages its own lifecycle events (onCreate, onStart, onResume, onPause, onStop, onDestroy). You do not need to forward these manually.

Recommendations

  • Hold a reference to LazarilloMap for the duration of the screen or Fragment lifecycle only.
  • Always pass viewLifecycleOwner.lifecycleScope as the lifecycleScope parameter, not lifecycleScope from the Fragment itself. This scopes coroutines to the view, not the Fragment, which prevents leaks during Fragment back-stack operations.
  • Do not store map references in static variables, singletons, or companion objects.
  • Release the reference in onDestroyView() (Fragment) or onDestroy() (Activity).

Example with Fragment Lifecycle

class MapFragment : Fragment() { private var lzMap: LazarilloMap? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { lzMap = LazarilloMap( id = "fragment-map", config = mapConfig, passedContext = requireContext(), inflater = layoutInflater, lifecycleScope = viewLifecycleOwner.lifecycleScope, positionRepository = null ) { } return binding.root } override fun onDestroyView() { super.onDestroyView() lzMap = null // Release reference to prevent memory leaks } }

Next Steps

Last updated on