Kotlin Multiplatform Compose Qibla direction UI for Android and iOS apps.
GeoQibla gives Compose Multiplatform apps a ready-made Qibla compass screen, a headless controller for custom UI, and public styling, localization, and slot APIs for product-specific presentation.
Current version: 0.0.1
- Compose-first: Drop in
GeoQiblaScreenfor a complete adaptive Qibla flow. - Cross-platform: Shared Kotlin API for Android and iOS.
- Headless control: Build custom screens from
QiblaController.state. - Location and sensor state: Permission, settings, calibration, tilt, heading accuracy, and error handling.
- Configurable behavior: Alignment thresholds, smoothing, update interval, magnetic limits, haptics, and
onAligned. - Custom presentation:
QiblaStyle,QiblaStrings, andQiblaSlotsfor theming, RTL text, and region replacement. - Reusable components: Public compass dial, status panel, and state message composables.
Add the dependency to your KMP module's commonMain source set:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.shahidzbi4213:geoqibla:0.0.1")
}
}
}Add to gradle/libs.versions.toml:
[versions]
geoqibla = "0.0.1"
[libraries]
geoqibla = { module = "io.github.shahidzbi4213:geoqibla", version.ref = "geoqibla" }Then use it from your KMP module:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.geoqibla)
}
}
}Android - GeoQibla declares foreground location permissions in its Android library manifest:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />Your app still needs to handle Play policy disclosures and test heading behavior on a physical device.
iOS - Add location usage text to Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>GeoQibla uses your location to calculate the direction of the Qibla.</string>import androidx.compose.runtime.Composable
import com.shahid.tech.qibla.GeoQiblaScreen
import com.shahid.tech.qibla.rememberQiblaController
@Composable
fun QiblaRoute() {
val controller = rememberQiblaController()
GeoQiblaScreen(
controller = controller,
)
}GeoQiblaScreen starts the controller while it is in composition and stops it when it leaves composition.
| Platform | Minimum | Runtime services |
|---|---|---|
| Android | API 26+ | LocationManager, SensorManager, foreground location permission |
| iOS | App target supported by your KMP app | CoreLocation, heading updates, when-in-use location permission |
Compass behavior should be verified on physical devices. Emulators and simulators are useful for layout and permission-state checks, but not final heading accuracy.
Pass QiblaConfig to rememberQiblaController():
val controller = rememberQiblaController(
config = QiblaConfig(
nearDegrees = 10f,
alignedDegrees = 3f,
stableAlignedDurationMillis = 750L,
smoothingFactor = 0.15f,
locationUpdateIntervalMillis = 1_000L,
tiltLimitDegrees = 55f,
magneticFieldMinMicrotesla = 25f,
magneticFieldMaxMicrotesla = 65f,
hapticsEnabled = true,
onAligned = {
// Called after stable alignment.
},
),
)| Property | Default | Description |
|---|---|---|
nearDegrees |
10f |
Direction difference treated as near Qibla. |
alignedDegrees |
3f |
Direction difference treated as aligned. |
stableAlignedDurationMillis |
750L |
Delay before onAligned fires. |
smoothingFactor |
0.15f |
Compass heading smoothing, from 0f to 1f. |
locationUpdateIntervalMillis |
1_000L |
Location update interval. |
tiltLimitDegrees |
55f |
Tilt threshold before the UI warns the user. |
magneticFieldMinMicrotesla |
25f |
Low magnetic-field calibration threshold. |
magneticFieldMaxMicrotesla |
65f |
High magnetic-field calibration threshold. |
hapticsEnabled |
true |
Enables alignment haptics where supported. |
onAligned |
null |
Callback after stable alignment. |
Use the controller directly when you want your own screen:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import com.shahid.tech.qibla.QiblaUiState
import com.shahid.tech.qibla.rememberQiblaController
@Composable
fun CustomQiblaRoute() {
val controller = rememberQiblaController()
val state by controller.state.collectAsState()
when (state.uiState) {
QiblaUiState.ALIGNED -> {
// Render aligned state.
}
QiblaUiState.PERMISSION_REQUIRED -> {
// Show a permission prompt and call controller.requestPermission().
}
else -> {
// Render compass from state.compass and state.location.
}
}
}The state model exposes:
state.uiState: high-level UI state such asLOCATING,READY,NEAR_QIBLA, orALIGNED.state.compass: Qibla bearing, current azimuth, direction delta, distance, tilt, magnetic field, and alignment flags.state.location: location access, current fix, location-service availability, and address label.state.sensorAccuracy: heading sensor quality.state.orientationSource: rotation vector, accelerometer/magnetometer, platform heading, or none.state.errorMessage: optional platform or controller error text.
Use QiblaStyle, QiblaStrings, and QiblaSlots to customize the default screen without replacing its behavior:
import androidx.compose.ui.graphics.Color
import com.shahid.tech.qibla.GeoQiblaScreen
import com.shahid.tech.qibla.QiblaColors
import com.shahid.tech.qibla.QiblaSlots
import com.shahid.tech.qibla.QiblaStrings
import com.shahid.tech.qibla.QiblaStyle
GeoQiblaScreen(
controller = controller,
style = QiblaStyle.default().copy(
colors = QiblaColors(
primary = Color(0xFF096B58),
qibla = Color(0xFF0A7C66),
),
),
strings = QiblaStrings.arabic(),
slots = QiblaSlots(
topBar = { state ->
// Custom header.
},
actionButtons = { state ->
// Custom action row.
},
),
)The default screen is built from reusable public components:
| Component | Purpose |
|---|---|
GeoQiblaScreen |
Complete adaptive screen with lifecycle handling. |
QiblaCompassDial |
Compass dial and Qibla pointer. |
QiblaStatusPanel |
Bearing, distance, sensor, and location status rows. |
QiblaStateMessage |
State-specific message and supporting copy. |
interface QiblaController {
val state: StateFlow<QiblaState>
fun start()
fun stop()
fun retryLocation()
fun requestPermission()
fun openLocationSettings()
fun openAppSettings()
fun dismissCalibration()
}data class QiblaState(
val uiState: QiblaUiState = QiblaUiState.IDLE,
val compass: QiblaCompassState = QiblaCompassState(),
val location: QiblaLocationState = QiblaLocationState(),
val sensorAccuracy: QiblaSensorAccuracy = QiblaSensorAccuracy.UNKNOWN,
val orientationSource: QiblaOrientationSource = QiblaOrientationSource.NONE,
val isStarted: Boolean = false,
val errorMessage: String? = null,
)Test on a physical device, keep the device flat, and confirm location permission is granted. Emulators and simulators do not provide reliable final heading accuracy.
Call controller.requestPermission() from a user action. If the user permanently denies permission, call controller.openAppSettings().
Use controller.openLocationSettings() from your UI. The default screen already shows the relevant action for this state.
Ask the user to move away from magnetic interference and follow the device calibration motion. You can provide a custom calibration region with QiblaSlots.calibrationSheet.
Contributions are welcome. See CONTRIBUTING.md for local commands, documentation rules, and pull request expectations.
If GeoQibla helps your app, star the repository and open issues with platform details, device model, OS version, GeoQibla version, and reproduction steps.
Apache License 2.0
Copyright 2026 Shahid Iqbal
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.