Setup Navigation¶
Set up your own location provider or use partner wrappers to enable indoor navigation in a plan.
Recommended: Prefer coroutine-based APIs. Callback APIs are available for compatibility (e.g., Java).
Overview¶
- A plan consumes positions via
IExpoFpLocationProvider
. - You can implement your own provider or use ExpoFP wrappers (CrowdConnected, IndoorAtlas).
- A provider may be attached to one presenter or shared as a global provider across screens.
Step 1. Add Dependencies (Gradle)¶
Use the 5.x line of ExpoFP artifacts. If you start from the first 5.x release, pin to 5.0.0 (or 5.0.+ to receive only patch updates). You can also use 5.+ to stay on the latest 5.x.
Kotlin DSL (build.gradle.kts
):
dependencies {
// Core ExpoFP SDK
implementation("com.expofp:fplan:5.0.0") // or "5.0.+", or "5.+"
// Partner wrappers (pick what you need)
implementation("com.expofp:crowdconnected:5.0.0") // CrowdConnected (foreground)
implementation("com.expofp:crowdconnectedbackground:5.0.0") // CrowdConnected (background-capable)
implementation("com.expofp:indooratlas:5.0.0") // IndoorAtlas
}
Version Catalogs (gradle/libs.versions.toml
):
[versions]
expofp = "5.0.0"
[libraries]
expofp-fplan = { group = "com.expofp", name = "fplan", version.ref = "expofp" }
expofp-crowdconnected = { group = "com.expofp", name = "crowdconnected", version.ref = "expofp" }
expofp-crowdconnected-background = { group = "com.expofp", name = "crowdconnectedbackground", version.ref = "expofp" }
expofp-indooratlas = { group = "com.expofp", name = "indooratlas", version.ref = "expofp" }
Then use in build.gradle.kts
:
dependencies {
implementation(libs.expofp.fplan)
implementation(libs.expofp.crowdconnected) // optional
implementation(libs.expofp.crowdconnected.background) // optional
implementation(libs.expofp.indooratlas) // optional
}
Step 2. Manifest Setup¶
Permissions depend on the provider (GPS, BLE beacons, IPS SDK). Include only what you actually use.
<manifest>
<!-- Network (map tiles, assets, etc.) -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Location -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- Background location (optional; only if needed) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<!-- Bluetooth for IPS beacons (optional; Android 12+) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!-- Legacy Bluetooth (API < 31) -->
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<!-- Foreground service for location (if your provider runs a FGS) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<!-- Optional: BLE not strictly required -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
<application>
...
</application>
</manifest>
Step 3. Request Runtime Permissions (example)¶
For production, prefer the Activity Result API. Below is a minimal example for clarity.
val permsModern = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
val permsLegacy = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
ActivityCompat.requestPermissions(
this,
if (Build.VERSION.SDK_INT >= 31) permsModern else permsLegacy,
100
)
Step 4. Implement Your Own Provider¶
Implement IExpoFpLocationProvider
and forward positions to the presenter through the delegate.
class YourLocationProvider(private val context: Context) : IExpoFpLocationProvider {
override var expoFpLocationProviderDelegate: ExpoFpLocationProviderDelegate? = null
private val fused by lazy { LocationServices.getFusedLocationProviderClient(context) }
private var callback: LocationCallback? = null
@MainThread
override suspend fun startUpdatingLocation() {
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 2000L).build()
callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
val loc = result.lastLocation ?: return
val position = ExpoFpPosition(lat = loc.latitude, lng = loc.longitude)
expoFpLocationProviderDelegate?.positionDidChange(position)
}
}
fused.requestLocationUpdates(request, callback as LocationCallback, Looper.getMainLooper())
}
override fun stopUpdatingLocation() {
callback?.let { fused.removeLocationUpdates(it) }
callback = null
}
}
You can also use partner wrappers (CrowdConnected, IndoorAtlas) via ExpoFP modules. See below.
Step 5. Attach to Presenter¶
During presenter creation (recommended)¶
val locationProvider = YourLocationProvider(context)
val presenter = ExpoFpPlan.createPlanPresenter(
planLink = ExpoFpLinkType.ExpoKey("YourExpoKey"),
locationProvider = locationProvider
)
After presenter creation¶
presenter.setLocationProvider(YourLocationProvider(context))
When the plan view appears, the presenter calls
startUpdatingLocation()
automatically.
When the plan view disappears, it callsstopUpdatingLocation()
only if the provider is not global.
Manual control remains available:
locationProvider.startUpdatingLocation()
locationProvider.stopUpdatingLocation()
Step 6. Global Location Provider¶
Use one provider instance across multiple plans.
// Set once
val provider = YourLocationProvider(context)
ExpoFpPlan.globalLocationProvider.sharedProvider = provider
// Optional manual control
ExpoFpPlan.globalLocationProvider.startUpdatingLocation()
// ...
ExpoFpPlan.globalLocationProvider.stopUpdatingLocation()
Attach global provider to a presenter:
val presenter = ExpoFpPlan.createPlanPresenter(
planLink = ExpoFpLinkType.ExpoKey("YourExpoKey"),
locationProvider = ExpoFpPlan.globalLocationProvider
)
// or later:
presenter.setLocationProvider(ExpoFpPlan.globalLocationProvider)
For global providers the presenter does not stop updates on view disappearance — manage lifecycle yourself.
Step 7. Listen to Location Updates Manually¶
When a presenter uses a provider, it sets itself as the provider's delegate.
If you set your own delegate, forward updates to the plan manually viaselectCurrentPosition(position, focus)
.
class YourLocationProviderDelegate(
private val presenter: IExpoFpPlanPresenter
) : ExpoFpLocationProviderDelegate {
@MainThread
override fun positionDidChange(newPosition: ExpoFpPosition) {
presenter.selectCurrentPosition(newPosition, focus = false)
}
}
Attach the delegate:
val provider = YourLocationProvider(context)
provider.expoFpLocationProviderDelegate = YourLocationProviderDelegate(presenter)
// For global provider:
// ExpoFpPlan.globalLocationProvider.expoFpLocationProviderDelegate = YourLocationProviderDelegate(presenter)
CrowdConnected (ExpoFP Wrapper)¶
Settings¶
val ccSettings = ExpoFpCrowdConnectedLocationProviderSettings(
appKey = "YOUR_APP_KEY",
token = "YOUR_TOKEN",
secret = "YOUR_SECRET",
navigationType = ExpoFpCrowdConnectedNavigationType.IPS, // or GEO / ALL
isAllowedInBackground = false, // true -> include ACCESS_BACKGROUND_LOCATION in checks
isHeadingEnabled = true, // enable azimuth heading
aliases = mapOf("visitorId" to "12345"),
notificationText = "Indoor navigation is active", // foreground service notification
serviceIcon = R.drawable.ic_navigation // notification icon
)
Choose a Provider Class¶
// Foreground-only
val ccProvider = ExpoFpCrowdConnectedLocationProvider(applicationContext, ccSettings)
// Background-capable (adds BackgroundModule and background permission flow)
val ccBgProvider = ExpoFpCrowdConnectedBackgroundLocationProvider(applicationContext, ccSettings)
Start with Auto-Permissions¶
Activity vs RegistryOwner - Use
activity
for a simple Activity-based flow. - UseregistryOwner
when you integrate withActivityResultRegistry
(common inside Compose and custom components).
> You can pass both when available.
Coroutine + Activity
lifecycleScope.launch {
ccProvider.startWithAutoPermissions(
owner = this@YourActivity, // LifecycleOwner
activity = this@YourActivity // Activity
)
}
Coroutine + ActivityResultRegistryOwner
lifecycleScope.launch {
ccProvider.startWithAutoPermissions(
owner = this@YourActivity, // LifecycleOwner
registryOwner = this@YourActivity // ActivityResultRegistryOwner
)
}
Callback + Activity
ccProvider.startWithAutoPermissions(
owner = this,
activity = this,
onStarted = { /* ready */ },
onDenied = { denied, permanentlyDenied -> /* handle */ },
onError = { e -> /* log */ }
)
Callback + ActivityResultRegistryOwner
ccProvider.startWithAutoPermissions(
owner = this,
registryOwner = this,
onStarted = { /* ready */ },
onDenied = { denied -> /* handle */ },
onError = { e -> /* log */ }
)
Attach to Presenter¶
val presenter = ExpoFpPlan.createPlanPresenter(
planLink = ExpoFpLinkType.RawLink("https://demo.expofp.com"),
locationProvider = ccProvider // or ccBgProvider
)
Use in Compose¶
setContent {
val context = LocalContext.current
val activity = context as ComponentActivity
val scope = rememberCoroutineScope()
val expoView = remember { ExpoFpView(context).apply { attachPresenter(presenter) } }
Column {
Button(onClick = {
scope.launch {
ccProvider.startWithAutoPermissions(
owner = activity,
registryOwner = activity
)
}
}) { Text("Start navigation") }
AndroidView(factory = { expoView }, modifier = Modifier.fillMaxSize())
}
}
Stop and Dispose¶
ccProvider.stopUpdatingLocation()
ccProvider.close()
IndoorAtlas (ExpoFP Wrapper)¶
Create Provider¶
val iaProvider = ExpoFpIndoorAtlasLocationProvider(
appContext = applicationContext,
apiKey = "YOUR_IA_API_KEY",
apiSecret = "YOUR_IA_API_SECRET"
)
Start with Auto-Permissions¶
Activity vs RegistryOwner — same rationale as above.
Coroutine + Activity
lifecycleScope.launch {
iaProvider.startWithAutoPermissions(
owner = this@YourActivity,
activity = this@YourActivity
)
}
Coroutine + ActivityResultRegistryOwner
lifecycleScope.launch {
iaProvider.startWithAutoPermissions(
owner = this@YourActivity,
registryOwner = this@YourActivity
)
}
Callback + Activity
iaProvider.startWithAutoPermissions(
owner = this,
activity = this,
onStarted = { /* ready */ },
onDenied = { denied, permanentlyDenied -> /* handle */ },
onError = { e -> /* log */ }
)
Callback + ActivityResultRegistryOwner
iaProvider.startWithAutoPermissions(
owner = this,
registryOwner = this,
onStarted = { /* ready */ },
onDenied = { denied -> /* handle */ },
onError = { e -> /* log */ }
)
Attach to Presenter¶
val presenter = ExpoFpPlan.createPlanPresenter(
planLink = ExpoFpLinkType.RawLink("https://demo.expofp.com"),
locationProvider = iaProvider
)
Use in Compose¶
setContent {
val context = LocalContext.current
val activity = context as ComponentActivity
val scope = rememberCoroutineScope()
val expoView = remember { ExpoFpView(context).apply { attachPresenter(presenter) } }
Column {
Button(onClick = {
scope.launch {
iaProvider.startWithAutoPermissions(
owner = activity,
registryOwner = activity
)
}
}) { Text("Start navigation") }
AndroidView(factory = { expoView }, modifier = Modifier.fillMaxSize())
}
}
Stop and Dispose¶
iaProvider.stopUpdatingLocation()
iaProvider.close()
Best Practices¶
- Request only needed permissions: tailor to your provider (GPS/BLE/IPS).
- Prefer coroutine APIs for lifecycle-aware control.
- Explain background location to users and keep a persistent notification for foreground service.
- Use a global provider only if several screens share the same location stream.
- Stop updates when not needed to save battery.
- Do not block the main thread inside provider callbacks; offload heavy work to coroutines.