
Simplifies location handling in Compose applications with a declarative API. Offers automatic permission management, customizable UI components, flexible configuration, and smart error handling for seamless location fetching.
A beautiful, declarative Kotlin Multiplatform library for effortless location handling in Compose applications. KU (short for "where are yoU?") simplifies location permissions and fetching with a clean, composable API.
Add KU to your commonMain dependencies:
commonMain {
dependencies {
implementation("io.github.eltonkola:ku:0.0.3")
}
}Start requesting location immediately when the composable loads:
LocationProvider(
config = LocationConfig(autoRequest = true),
onLocationReceived = { location ->
Text("Location: ${location.format()}")
}
)Let users control when to request location:
LocationProvider(
onLocationReceived = { location ->
Column {
Text("Latitude: ${location.latitude}")
Text("Longitude: ${location.longitude}")
Text("Accuracy: ${location.accuracy}m")
}
},
onInitial = { onRequestLocation ->
Button(onClick = onRequestLocation) {
Text("Get My Location")
}
}
)Customize location behavior with LocationConfig:
LocationProvider(
config = LocationConfig(
autoRequest = true,
singleRequest = false, // Set to true for one-time location
highAccuracy = true, // Use GPS for high accuracy
updateIntervalMs = 10_000L, // Update every 10 seconds
minUpdateIntervalMs = 5_000L, // Minimum 5 seconds between updates
timeoutMs = 30_000L // 30 second timeout
),
onLocationReceived = { location ->
MapView(location)
}
)Override any part of the UI to match your design:
LocationProvider(
onLocationReceived = { location ->
LocationCard(location)
},
onInitial = { onRequestLocation ->
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onRequestLocation() }
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
"Find My Location",
style = MaterialTheme.typography.titleMedium
)
Text(
"Tap to get your current position",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Icon(
Icons.Default.LocationOn,
contentDescription = "Location",
tint = MaterialTheme.colorScheme.primary
)
}
}
},
onLoading = {
Card {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Finding your location...",
style = MaterialTheme.typography.bodyMedium
)
Text(
"This may take a few moments",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
},
onPermissionDenied = { requestPermission ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.LocationOff,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
"Location Permission Required",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onErrorContainer
)
Text(
"We need access to your location to show nearby places and services.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = requestPermission,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Text("Grant Permission")
}
}
}
},
onError = { errorMessage, retry ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.Error,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Location Error",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onErrorContainer
)
Text(
errorMessage,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = retry) {
Text("Try Again")
}
}
}
}
)For cases where you prefer callbacks over composable content:
LocationProvider(
onLocationReceived = { location ->
// Handle location update
updateMapLocation(location)
},
onError = { error ->
// Handle error
showErrorToast(error)
},
onPermissionDenied = {
// Handle permission denied
showPermissionDialog()
},
config = LocationConfig(autoRequest = true)
)Handle different types of location errors:
LocationProvider(
onError = { errorMessage, retry ->
when {
errorMessage.contains("GPS") || errorMessage.contains("Location services") -> {
Column {
Text("GPS is disabled")
Text("Please enable location services in your device settings")
Spacer(modifier = Modifier.height(8.dp))
Row {
Button(onClick = { openLocationSettings() }) {
Text("Open Settings")
}
Spacer(modifier = Modifier.width(8.dp))
OutlinedButton(onClick = retry) {
Text("Retry")
}
}
}
}
errorMessage.contains("timeout") -> {
Column {
Text("Location request timed out")
Text("Make sure you're not indoors and have a clear view of the sky")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = retry) {
Text("Try Again")
}
}
}
else -> {
Column {
Text("Location Error")
Text(errorMessage)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = retry) {
Text("Retry")
}
}
}
}
},
onLocationReceived = { location ->
MapView(location)
}
)Use the useLocation hook for more control:
@Composable
fun MyLocationScreen() {
val locationState = useLocation(
config = LocationConfig(autoRequest = true, highAccuracy = true)
)
when (locationState) {
LocationState.Loading -> {
CircularProgressIndicator()
}
is LocationState.Success -> {
LocationDetails(locationState.location)
}
is LocationState.Error -> {
ErrorMessage(locationState.message)
}
LocationState.PermissionDenied -> {
PermissionRequestUI()
}
}
}| Parameter | Type | Description | Default |
|---|---|---|---|
onLocationReceived |
@Composable (Location) -> Unit |
Content displayed when location is successfully obtained | Required |
modifier |
Modifier |
Modifier for the LocationProvider container | Modifier |
config |
LocationConfig |
Configuration for location behavior | LocationConfig() |
onPermissionDenied |
@Composable (() -> Unit) -> Unit |
Content displayed when permission is denied | Default permission UI |
onLoading |
@Composable () -> Unit |
Content displayed while fetching location | Default loading UI |
onError |
@Composable (String, () -> Unit) -> Unit |
Content displayed when an error occurs | Default error UI |
onInitial |
@Composable (() -> Unit) -> Unit |
Content displayed before location request | Default button |
| Property | Type | Description | Default |
|---|---|---|---|
autoRequest |
Boolean |
Whether to request location automatically | false |
singleRequest |
Boolean |
Whether to request location only once | false |
highAccuracy |
Boolean |
Whether to use high accuracy (GPS) | true |
updateIntervalMs |
Long |
Interval between location updates (ms) | 10_000L |
minUpdateIntervalMs |
Long |
Minimum interval between updates (ms) | 5_000L |
maxUpdateDelayMs |
Long |
Maximum delay for location updates (ms) | 15_000L |
timeoutMs |
Long |
Timeout for location requests (ms) | 30_000L |
data class Location(
val latitude: Double,
val longitude: Double,
val accuracy: Float, // Accuracy in meters
val altitude: Double?, // Altitude in meters (if available)
val bearing: Float?, // Direction of travel (if available)
val speed: Float?, // Speed in m/s (if available)
val timestamp: Long, // Time when location was obtained
val provider: String? // Location provider (GPS, Network, etc.)
)// Format location coordinates
val formatted = location.format(precision = 4) // "40.7128, -74.0060"
// Calculate distance between two locations
val distance = location1.distanceTo(location2) // Distance in meterssealed class LocationState {
object Loading : LocationState()
data class Success(val location: Location) : LocationState()
data class Error(val message: String) : LocationState()
object PermissionDenied : LocationState()
}KU automatically handles location errors intelligently:
Add location permissions to your AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Optional: For background location (if needed) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />ProGuard/R8 Rules (if using code obfuscation):
-keep class com.google.android.gms.location.** { *; }
-dontwarn com.google.android.gms.**
Add location usage descriptions to your Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to show your current position and nearby places.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs location access to provide location-based services.</string>@Test
fun testLocationProvider() {
// Use Android Location Testing framework or iOS Location Simulator
// KU integrates well with standard platform testing tools
}val mockLocation = Location(
latitude = 40.7128,
longitude = -74.0060,
accuracy = 10.0f,
altitude = 100.0,
speed = null,
bearing = null,
timestamp = System.currentTimeMillis(),
provider = "mock"
)Location always returns null/error:
timeoutMs in LocationConfigUI keeps flashing between states:
Battery drain:
updateIntervalMs and minUpdateIntervalMs
singleRequest = true if you only need one locationhighAccuracy = false for less precise but battery-friendly locationiOS permission issues:
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
Copyright 2024 Elton Kola
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
http://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.
A beautiful, declarative Kotlin Multiplatform library for effortless location handling in Compose applications. KU (short for "where are yoU?") simplifies location permissions and fetching with a clean, composable API.
Add KU to your commonMain dependencies:
commonMain {
dependencies {
implementation("io.github.eltonkola:ku:0.0.3")
}
}Start requesting location immediately when the composable loads:
LocationProvider(
config = LocationConfig(autoRequest = true),
onLocationReceived = { location ->
Text("Location: ${location.format()}")
}
)Let users control when to request location:
LocationProvider(
onLocationReceived = { location ->
Column {
Text("Latitude: ${location.latitude}")
Text("Longitude: ${location.longitude}")
Text("Accuracy: ${location.accuracy}m")
}
},
onInitial = { onRequestLocation ->
Button(onClick = onRequestLocation) {
Text("Get My Location")
}
}
)Customize location behavior with LocationConfig:
LocationProvider(
config = LocationConfig(
autoRequest = true,
singleRequest = false, // Set to true for one-time location
highAccuracy = true, // Use GPS for high accuracy
updateIntervalMs = 10_000L, // Update every 10 seconds
minUpdateIntervalMs = 5_000L, // Minimum 5 seconds between updates
timeoutMs = 30_000L // 30 second timeout
),
onLocationReceived = { location ->
MapView(location)
}
)Override any part of the UI to match your design:
LocationProvider(
onLocationReceived = { location ->
LocationCard(location)
},
onInitial = { onRequestLocation ->
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onRequestLocation() }
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
"Find My Location",
style = MaterialTheme.typography.titleMedium
)
Text(
"Tap to get your current position",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Icon(
Icons.Default.LocationOn,
contentDescription = "Location",
tint = MaterialTheme.colorScheme.primary
)
}
}
},
onLoading = {
Card {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Finding your location...",
style = MaterialTheme.typography.bodyMedium
)
Text(
"This may take a few moments",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
},
onPermissionDenied = { requestPermission ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.LocationOff,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
"Location Permission Required",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onErrorContainer
)
Text(
"We need access to your location to show nearby places and services.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = requestPermission,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Text("Grant Permission")
}
}
}
},
onError = { errorMessage, retry ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.Error,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Location Error",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onErrorContainer
)
Text(
errorMessage,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = retry) {
Text("Try Again")
}
}
}
}
)For cases where you prefer callbacks over composable content:
LocationProvider(
onLocationReceived = { location ->
// Handle location update
updateMapLocation(location)
},
onError = { error ->
// Handle error
showErrorToast(error)
},
onPermissionDenied = {
// Handle permission denied
showPermissionDialog()
},
config = LocationConfig(autoRequest = true)
)Handle different types of location errors:
LocationProvider(
onError = { errorMessage, retry ->
when {
errorMessage.contains("GPS") || errorMessage.contains("Location services") -> {
Column {
Text("GPS is disabled")
Text("Please enable location services in your device settings")
Spacer(modifier = Modifier.height(8.dp))
Row {
Button(onClick = { openLocationSettings() }) {
Text("Open Settings")
}
Spacer(modifier = Modifier.width(8.dp))
OutlinedButton(onClick = retry) {
Text("Retry")
}
}
}
}
errorMessage.contains("timeout") -> {
Column {
Text("Location request timed out")
Text("Make sure you're not indoors and have a clear view of the sky")
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = retry) {
Text("Try Again")
}
}
}
else -> {
Column {
Text("Location Error")
Text(errorMessage)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = retry) {
Text("Retry")
}
}
}
}
},
onLocationReceived = { location ->
MapView(location)
}
)Use the useLocation hook for more control:
@Composable
fun MyLocationScreen() {
val locationState = useLocation(
config = LocationConfig(autoRequest = true, highAccuracy = true)
)
when (locationState) {
LocationState.Loading -> {
CircularProgressIndicator()
}
is LocationState.Success -> {
LocationDetails(locationState.location)
}
is LocationState.Error -> {
ErrorMessage(locationState.message)
}
LocationState.PermissionDenied -> {
PermissionRequestUI()
}
}
}| Parameter | Type | Description | Default |
|---|---|---|---|
onLocationReceived |
@Composable (Location) -> Unit |
Content displayed when location is successfully obtained | Required |
modifier |
Modifier |
Modifier for the LocationProvider container | Modifier |
config |
LocationConfig |
Configuration for location behavior | LocationConfig() |
onPermissionDenied |
@Composable (() -> Unit) -> Unit |
Content displayed when permission is denied | Default permission UI |
onLoading |
@Composable () -> Unit |
Content displayed while fetching location | Default loading UI |
onError |
@Composable (String, () -> Unit) -> Unit |
Content displayed when an error occurs | Default error UI |
onInitial |
@Composable (() -> Unit) -> Unit |
Content displayed before location request | Default button |
| Property | Type | Description | Default |
|---|---|---|---|
autoRequest |
Boolean |
Whether to request location automatically | false |
singleRequest |
Boolean |
Whether to request location only once | false |
highAccuracy |
Boolean |
Whether to use high accuracy (GPS) | true |
updateIntervalMs |
Long |
Interval between location updates (ms) | 10_000L |
minUpdateIntervalMs |
Long |
Minimum interval between updates (ms) | 5_000L |
maxUpdateDelayMs |
Long |
Maximum delay for location updates (ms) | 15_000L |
timeoutMs |
Long |
Timeout for location requests (ms) | 30_000L |
data class Location(
val latitude: Double,
val longitude: Double,
val accuracy: Float, // Accuracy in meters
val altitude: Double?, // Altitude in meters (if available)
val bearing: Float?, // Direction of travel (if available)
val speed: Float?, // Speed in m/s (if available)
val timestamp: Long, // Time when location was obtained
val provider: String? // Location provider (GPS, Network, etc.)
)// Format location coordinates
val formatted = location.format(precision = 4) // "40.7128, -74.0060"
// Calculate distance between two locations
val distance = location1.distanceTo(location2) // Distance in meterssealed class LocationState {
object Loading : LocationState()
data class Success(val location: Location) : LocationState()
data class Error(val message: String) : LocationState()
object PermissionDenied : LocationState()
}KU automatically handles location errors intelligently:
Add location permissions to your AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Optional: For background location (if needed) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />ProGuard/R8 Rules (if using code obfuscation):
-keep class com.google.android.gms.location.** { *; }
-dontwarn com.google.android.gms.**
Add location usage descriptions to your Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to show your current position and nearby places.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs location access to provide location-based services.</string>@Test
fun testLocationProvider() {
// Use Android Location Testing framework or iOS Location Simulator
// KU integrates well with standard platform testing tools
}val mockLocation = Location(
latitude = 40.7128,
longitude = -74.0060,
accuracy = 10.0f,
altitude = 100.0,
speed = null,
bearing = null,
timestamp = System.currentTimeMillis(),
provider = "mock"
)Location always returns null/error:
timeoutMs in LocationConfigUI keeps flashing between states:
Battery drain:
updateIntervalMs and minUpdateIntervalMs
singleRequest = true if you only need one locationhighAccuracy = false for less precise but battery-friendly locationiOS permission issues:
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
Copyright 2024 Elton Kola
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
http://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.