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
- Project Setup
- Permissions
- SDK Initialization
- Map Integration
- Location Tracking
- Navigation and Routing
- Indoor Positioning
- Place Markers
- Floor Selection
- Route Styling
- Accessibility
- Customization
- Multi-Language Support
- 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?.tiltLocation 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
)
}Navigation and Routing
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
parentPlaceIdis passed toRepoBuilder
How It Works
- The SDK automatically scans for Bluetooth beacons (Eddystone, iBeacon)
- Detected beacons are triangulated to calculate a position
- Floor information is extracted from the beacon data
- 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 Element | String Resource | Default Text |
|---|---|---|
| Zoom In | lz_map_zoom_in | ”Zoom in on the map” |
| Zoom Out | lz_map_zoom_out | ”Zoom out on the map” |
| Location Button | lz_map_current_location | ”Center map on your current location” |
| Floor Selector | lz_map_floor_selector | ”Select building floor to display” |
| Compass | lz_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
LazarilloMapfor the duration of the screen or Fragment lifecycle only. - Always pass
viewLifecycleOwner.lifecycleScopeas thelifecycleScopeparameter, notlifecycleScopefrom 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) oronDestroy()(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
- Permissions Guide - Detailed permission handling and rationale
- Accessibility Guide - Full accessibility customization reference
- Examples - Complete working code samples
- API Reference - Full SDK API documentation
- Troubleshooting - Common issues and solutions
- FAQ & Troubleshooting - Frequently asked questions and common issues