├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── simple
├── iosApp
│ ├── iosApp
│ │ ├── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── Preview Content
│ │ │ └── Preview Assets.xcassets
│ │ │ │ └── Contents.json
│ │ ├── Info.plist
│ │ └── iosApp.swift
│ └── iosApp.xcodeproj
│ │ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── iosApp.xcscheme
│ │ └── project.pbxproj
└── composeApp
│ ├── src
│ ├── commonMain
│ │ ├── composeResources
│ │ │ ├── font
│ │ │ │ └── IndieFlower-Regular.ttf
│ │ │ ├── values
│ │ │ │ └── strings.xml
│ │ │ └── drawable
│ │ │ │ ├── ic_dark_mode.xml
│ │ │ │ ├── ic_rotate_right.xml
│ │ │ │ ├── ic_cyclone.xml
│ │ │ │ └── ic_light_mode.xml
│ │ └── kotlin
│ │ │ └── io
│ │ │ └── github
│ │ │ └── sample
│ │ │ ├── LocationViewModel.kt
│ │ │ ├── theme
│ │ │ ├── Color.kt
│ │ │ └── Theme.kt
│ │ │ └── App.kt
│ ├── jvmMain
│ │ └── kotlin
│ │ │ ├── io
│ │ │ └── github
│ │ │ │ └── sample
│ │ │ │ └── theme
│ │ │ │ └── Theme.jvm.kt
│ │ │ └── main.kt
│ ├── wasmJsMain
│ │ ├── kotlin
│ │ │ ├── io
│ │ │ │ └── github
│ │ │ │ │ └── sample
│ │ │ │ │ └── theme
│ │ │ │ │ └── Theme.wasmJs.kt
│ │ │ └── main.kt
│ │ └── resources
│ │ │ └── index.html
│ ├── iosMain
│ │ └── kotlin
│ │ │ ├── main.kt
│ │ │ └── io
│ │ │ └── github
│ │ │ └── sample
│ │ │ └── theme
│ │ │ └── Theme.ios.kt
│ ├── androidMain
│ │ ├── kotlin
│ │ │ └── io
│ │ │ │ └── github
│ │ │ │ └── sample
│ │ │ │ ├── theme
│ │ │ │ └── Theme.android.kt
│ │ │ │ └── App.android.kt
│ │ └── AndroidManifest.xml
│ └── commonTest
│ │ └── kotlin
│ │ └── io
│ │ └── github
│ │ └── sample
│ │ └── ComposeTest.kt
│ └── build.gradle.kts
├── .gitignore
├── klocation
├── src
│ ├── androidMain
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ │ └── io
│ │ │ └── github
│ │ │ └── tbib
│ │ │ └── klocation
│ │ │ ├── AndroidKLocationService.kt
│ │ │ └── KLocationService.android.kt
│ ├── iosMain
│ │ └── kotlin
│ │ │ └── io
│ │ │ └── github
│ │ │ └── tbib
│ │ │ └── klocation
│ │ │ ├── IOSKLocationServices.kt
│ │ │ ├── getTopViewController.kt
│ │ │ └── KLocationService.ios.kt
│ └── commonMain
│ │ └── kotlin
│ │ └── io
│ │ └── github
│ │ └── tbib
│ │ └── klocation
│ │ └── KLocationService.kt
└── build.gradle.kts
├── gradle.properties
├── settings.gradle.kts
├── gradlew.bat
├── README.MD
└── gradlew
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-best-is-best/klocation/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/simple/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "author": "xcode",
4 | "version": 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/simple/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "author": "xcode",
4 | "version": 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/simple/composeApp/src/commonMain/composeResources/font/IndieFlower-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-best-is-best/klocation/HEAD/simple/composeApp/src/commonMain/composeResources/font/IndieFlower-Regular.ttf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | *.iml
3 | .gradle
4 | .idea
5 | .kotlin
6 | .DS_Store
7 | build
8 | */build
9 | captures
10 | .externalNativeBuild
11 | .cxx
12 | local.properties
13 | xcuserdata/
14 | Pods/
15 | *.jks
16 | *yarn.lock
17 |
--------------------------------------------------------------------------------
/simple/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/simple/composeApp/src/jvmMain/kotlin/io/github/sample/theme/Theme.jvm.kt:
--------------------------------------------------------------------------------
1 | package io.github.sample.theme
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | internal actual fun SystemAppearance(isDark: Boolean) {
7 | }
--------------------------------------------------------------------------------
/simple/composeApp/src/wasmJsMain/kotlin/io/github/sample/theme/Theme.wasmJs.kt:
--------------------------------------------------------------------------------
1 | package io.github.sample.theme
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | internal actual fun SystemAppearance(isDark: Boolean) {
7 | }
--------------------------------------------------------------------------------
/simple/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors": [
3 | {
4 | "idiom": "universal"
5 | }
6 | ],
7 | "info": {
8 | "author": "xcode",
9 | "version": 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/simple/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "idiom": "universal",
5 | "platform": "ios",
6 | "size": "1024x1024"
7 | }
8 | ],
9 | "info": {
10 | "author": "xcode",
11 | "version": 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/simple/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/simple/composeApp/src/commonMain/composeResources/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Cyclone
3 | Open github
4 | Run
5 | Stop
6 | Theme
7 |
--------------------------------------------------------------------------------
/simple/composeApp/src/wasmJsMain/kotlin/main.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.ExperimentalComposeUiApi
2 | import androidx.compose.ui.window.CanvasBasedWindow
3 | import io.github.sample.App
4 |
5 | @OptIn(ExperimentalComposeUiApi::class)
6 | fun main() {
7 | CanvasBasedWindow("sample") {
8 | App()
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/klocation/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/simple/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/simple/composeApp/src/iosMain/kotlin/main.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.window.ComposeUIViewController
2 | import io.github.sample.App
3 | import io.github.tbib.klocation.IOSKLocationServices
4 | import platform.UIKit.UIViewController
5 |
6 | fun MainViewController(): UIViewController {
7 | IOSKLocationServices().requestPermission()
8 | return ComposeUIViewController { App() }
9 | }
10 |
--------------------------------------------------------------------------------
/simple/composeApp/src/wasmJsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | sample
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/klocation/src/iosMain/kotlin/io/github/tbib/klocation/IOSKLocationServices.kt:
--------------------------------------------------------------------------------
1 | package io.github.tbib.klocation
2 |
3 | import platform.CoreLocation.CLLocationManager
4 |
5 | class IOSKLocationServices {
6 | private val locationManager = CLLocationManager()
7 |
8 | fun requestPermission(isBackgroundSupport: Boolean = false) {
9 | if (isBackgroundSupport) {
10 | locationManager.requestAlwaysAuthorization()
11 | } else {
12 | locationManager.requestWhenInUseAuthorization()
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/simple/composeApp/src/commonMain/composeResources/drawable/ic_dark_mode.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/simple/composeApp/src/jvmMain/kotlin/main.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.unit.dp
2 | import androidx.compose.ui.window.Window
3 | import androidx.compose.ui.window.application
4 | import androidx.compose.ui.window.rememberWindowState
5 | import java.awt.Dimension
6 | import io.github.sample.App
7 |
8 | fun main() = application {
9 | Window(
10 | title = "sample",
11 | state = rememberWindowState(width = 800.dp, height = 600.dp),
12 | onCloseRequest = ::exitApplication,
13 | ) {
14 | window.minimumSize = Dimension(350, 600)
15 | App()
16 | }
17 | }
--------------------------------------------------------------------------------
/simple/iosApp/iosApp/iosApp.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import ComposeApp
3 |
4 | @main
5 | class AppDelegate: UIResponder, UIApplicationDelegate {
6 | var window: UIWindow?
7 |
8 | func application(
9 | _ application: UIApplication,
10 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
11 | ) -> Bool {
12 | window = UIWindow(frame: UIScreen.main.bounds)
13 | if let window = window {
14 | window.rootViewController = MainKt.MainViewController()
15 | window.makeKeyAndVisible()
16 | }
17 | return true
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #Gradle
2 | org.gradle.jvmargs=-Xmx4G -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx4G"
3 | org.gradle.caching=true
4 | org.gradle.configuration-cache=true
5 | org.gradle.daemon=true
6 | org.gradle.parallel=true
7 | #Kotlin
8 | kotlin.code.style=official
9 | kotlin.js.compiler=ir
10 | #Android
11 | android.useAndroidX=true
12 | android.nonTransitiveRClass=true
13 | #Compose
14 | org.jetbrains.compose.experimental.jscanvas.enabled=true
15 | mavenCentralUsername=username
16 | mavenCentralPassword=password
17 | signing.keyId=first 8 number from key
18 | signing.password=password key
19 | signing.secretKeyRingFile=path key
--------------------------------------------------------------------------------
/klocation/src/commonMain/kotlin/io/github/tbib/klocation/KLocationService.kt:
--------------------------------------------------------------------------------
1 | package io.github.tbib.klocation
2 |
3 | import androidx.compose.runtime.Composable
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | expect class KLocationService() {
7 | suspend fun getCurrentLocation(): Location
8 | fun startLocationUpdates(intervalMillis: Long = 10000): Flow
9 |
10 | @Deprecated("Use EnableLocation")
11 | fun enableLocation()
12 |
13 | @Composable
14 | fun EnableLocation()
15 | fun isLocationEnabled(): Boolean
16 | suspend fun gpsStateFlow(): Flow
17 |
18 |
19 | }
20 |
21 | data class Location(val latitude: Double, val longitude: Double)
22 |
--------------------------------------------------------------------------------
/simple/composeApp/src/iosMain/kotlin/io/github/sample/theme/Theme.ios.kt:
--------------------------------------------------------------------------------
1 | package io.github.sample.theme
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import platform.UIKit.UIApplication
6 | import platform.UIKit.UIStatusBarStyleDarkContent
7 | import platform.UIKit.UIStatusBarStyleLightContent
8 | import platform.UIKit.setStatusBarStyle
9 |
10 | @Composable
11 | internal actual fun SystemAppearance(isDark: Boolean) {
12 | LaunchedEffect(isDark) {
13 | UIApplication.sharedApplication.setStatusBarStyle(
14 | if (isDark) UIStatusBarStyleDarkContent else UIStatusBarStyleLightContent
15 | )
16 | }
17 | }
--------------------------------------------------------------------------------
/simple/composeApp/src/androidMain/kotlin/io/github/sample/theme/Theme.android.kt:
--------------------------------------------------------------------------------
1 | package io.github.sample.theme
2 |
3 | import android.app.Activity
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.ui.platform.LocalView
7 | import androidx.core.view.WindowInsetsControllerCompat
8 |
9 | @Composable
10 | internal actual fun SystemAppearance(isDark: Boolean) {
11 | val view = LocalView.current
12 | LaunchedEffect(isDark) {
13 | val window = (view.context as Activity).window
14 | WindowInsetsControllerCompat(window, window.decorView).apply {
15 | isAppearanceLightStatusBars = isDark
16 | isAppearanceLightNavigationBars = isDark
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/simple/composeApp/src/commonMain/composeResources/drawable/ic_rotate_right.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/simple/composeApp/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/klocation/src/androidMain/kotlin/io/github/tbib/klocation/AndroidKLocationService.kt:
--------------------------------------------------------------------------------
1 | package io.github.tbib.klocation
2 |
3 | import android.app.Activity
4 | import java.lang.ref.WeakReference
5 |
6 | enum class AccuracyPriority(val value: Int) {
7 | HIGH_ACCURACY(100),
8 | BALANCED_POWER_ACCURACY(102),
9 | LOW_POWER(104),
10 | PASSIVE(105);
11 | }
12 |
13 | object AndroidKLocationService {
14 | private var activity: WeakReference = WeakReference(null)
15 | private var accuracyPriority: AccuracyPriority? = null
16 |
17 | internal fun getActivity(): Activity {
18 | return activity.get()!!
19 | }
20 |
21 | internal fun getAccuracyPriority(): AccuracyPriority {
22 | return accuracyPriority!!
23 | }
24 |
25 | fun initialization(activity: Activity, priority: AccuracyPriority) {
26 | this.activity = WeakReference(activity)
27 | this.accuracyPriority = priority
28 |
29 | }
30 | }
--------------------------------------------------------------------------------
/simple/composeApp/src/androidMain/kotlin/io/github/sample/App.android.kt:
--------------------------------------------------------------------------------
1 | package io.github.sample
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.tooling.preview.Preview
9 | import io.github.tbib.klocation.AccuracyPriority
10 | import io.github.tbib.klocation.AndroidKLocationService
11 | import io.github.tbib.klocation.KLocationService
12 |
13 | class AppActivity : ComponentActivity() {
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | enableEdgeToEdge()
17 | AndroidKLocationService.initialization(this, AccuracyPriority.HIGH_ACCURACY)
18 | setContent {
19 | KLocationService().ListenerToPermission()
20 | App()
21 | }
22 | }
23 | }
24 |
25 | @Preview
26 | @Composable
27 | fun AppPreview() {
28 | App()
29 | }
30 |
--------------------------------------------------------------------------------
/simple/composeApp/src/commonMain/composeResources/drawable/ic_cyclone.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "klocation"
2 |
3 | pluginManagement {
4 | repositories {
5 | google {
6 | content {
7 | includeGroupByRegex("com\\.android.*")
8 | includeGroupByRegex("com\\.google.*")
9 | includeGroupByRegex("androidx.*")
10 | includeGroupByRegex("android.*")
11 | }
12 | }
13 | gradlePluginPortal()
14 | mavenCentral()
15 | }
16 | }
17 |
18 | dependencyResolutionManagement {
19 | repositories {
20 | google {
21 | content {
22 | includeGroupByRegex("com\\.android.*")
23 | includeGroupByRegex("com\\.google.*")
24 | includeGroupByRegex("androidx.*")
25 | includeGroupByRegex("android.*")
26 | }
27 | }
28 | mavenCentral()
29 | maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental")
30 | maven("https://maven.pkg.jetbrains.space/public/p/ktor/eap")
31 | }
32 | }
33 | include(":klocation")
34 | include(":simple:composeApp")
35 |
--------------------------------------------------------------------------------
/klocation/src/iosMain/kotlin/io/github/tbib/klocation/getTopViewController.kt:
--------------------------------------------------------------------------------
1 | package io.github.tbib.klocation
2 |
3 | import platform.UIKit.UIApplication
4 | import platform.UIKit.UINavigationController
5 | import platform.UIKit.UITabBarController
6 | import platform.UIKit.UIViewController
7 |
8 | // Helper function to get the current view controller
9 | fun getTopViewController(): UIViewController? {
10 | val window = UIApplication.sharedApplication.keyWindow
11 | return getTopViewControllerFromRoot(window?.rootViewController)
12 | }
13 |
14 | // Recursive function to traverse the view controller hierarchy
15 | fun getTopViewControllerFromRoot(rootViewController: UIViewController?): UIViewController? {
16 | if (rootViewController == null) return null
17 |
18 | if (rootViewController.presentedViewController != null) {
19 | return getTopViewControllerFromRoot(rootViewController.presentedViewController)
20 | }
21 |
22 | if (rootViewController is UINavigationController) {
23 | return getTopViewControllerFromRoot(rootViewController.visibleViewController)
24 | }
25 |
26 | if (rootViewController is UITabBarController) {
27 | return getTopViewControllerFromRoot(rootViewController.selectedViewController)
28 | }
29 |
30 | return rootViewController
31 | }
32 |
--------------------------------------------------------------------------------
/simple/composeApp/src/commonMain/composeResources/drawable/ic_light_mode.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/simple/composeApp/src/commonTest/kotlin/io/github/sample/ComposeTest.kt:
--------------------------------------------------------------------------------
1 | package io.github.sample
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.material3.Button
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.platform.testTag
12 | import androidx.compose.ui.test.ExperimentalTestApi
13 | import androidx.compose.ui.test.assertTextEquals
14 | import androidx.compose.ui.test.onNodeWithTag
15 | import androidx.compose.ui.test.performClick
16 | import androidx.compose.ui.test.runComposeUiTest
17 | import kotlin.test.Test
18 |
19 | @OptIn(ExperimentalTestApi::class)
20 | class ComposeTest {
21 |
22 | @Test
23 | fun simpleCheck() = runComposeUiTest {
24 | setContent {
25 | var txt by remember { mutableStateOf("Go") }
26 | Column {
27 | Text(
28 | text = txt,
29 | modifier = Modifier.testTag("t_text")
30 | )
31 | Button(
32 | onClick = { txt += "." },
33 | modifier = Modifier.testTag("t_button")
34 | ) {
35 | Text("click me")
36 | }
37 | }
38 | }
39 |
40 | onNodeWithTag("t_button").apply {
41 | repeat(3) { performClick() }
42 | }
43 | onNodeWithTag("t_text").assertTextEquals("Go...")
44 | }
45 | }
--------------------------------------------------------------------------------
/simple/composeApp/src/commonMain/kotlin/io/github/sample/LocationViewModel.kt:
--------------------------------------------------------------------------------
1 | package io.github.sample
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import io.github.tbib.klocation.KLocationService
9 | import io.github.tbib.klocation.Location
10 | import kotlinx.coroutines.launch
11 |
12 | class LocationViewModel : ViewModel() {
13 | private val locationService = KLocationService()
14 |
15 | var userLocation by mutableStateOf(null)
16 | var isGPSOpen by mutableStateOf(false)
17 |
18 | var requestPermission by mutableStateOf(false)
19 |
20 |
21 |
22 |
23 | fun init() {
24 | requestPermission = true
25 |
26 | viewModelScope.launch {
27 |
28 |
29 | // Request location permission if not already granted
30 | locationService.gpsStateFlow().collect { isGps ->
31 | println("gps is $isGps")
32 | isGPSOpen = isGps
33 | addListenerLocation()
34 | }
35 |
36 | }
37 | }
38 |
39 | private fun addListenerLocation() {
40 | println("addListenerLocation has called $isGPSOpen")
41 | if (isGPSOpen) {
42 | viewModelScope.launch {
43 | locationService.startLocationUpdates().collect { newLocation ->
44 | userLocation = newLocation
45 | }
46 | }
47 | }
48 | }
49 |
50 | suspend fun getLocation(): Location {
51 | return locationService.getCurrentLocation()
52 | }
53 |
54 | fun enableGPSAndLocation() {
55 | requestPermission = true
56 | }
57 |
58 | @Deprecated("use enableGPSAndLocation")
59 | fun enableLocation() {
60 | locationService.enableLocation()
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin = "2.1.20"
3 | compose = "1.7.3"
4 | agp = "8.9.0"
5 | androidx-activityCompose = "1.10.1"
6 | androidx-uiTest = "1.7.8"
7 | kotlinxCoroutinesCore = "1.10.1"
8 | lifecycleViewmodelCompose = "2.8.4"
9 | maven-publish = "0.31.0"
10 | permissions = "0.18.1"
11 | playServicesLocation = "21.3.0"
12 | lifecycleLivedataCoreKtx = "2.8.7"
13 | accompanistPermissions = "0.37.2"
14 |
15 |
16 |
17 |
18 | [libraries]
19 |
20 | androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
21 | androidx-uitest-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uiTest" }
22 | androidx-uitest-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uiTest" }
23 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
24 | kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutinesCore" }
25 | lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
26 | permissions = { module = "dev.icerock.moko:permissions", version.ref = "permissions" }
27 | permissions-compose = { module = "dev.icerock.moko:permissions-compose", version.ref = "permissions" }
28 | permissions-test = { module = "dev.icerock.moko:permissions-test", version.ref = "permissions" }
29 | play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
30 | androidx-lifecycle-livedata-core-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-core-ktx", version.ref = "lifecycleLivedataCoreKtx" }
31 | accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
32 |
33 | [plugins]
34 |
35 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
36 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
37 | compose = { id = "org.jetbrains.compose", version.ref = "compose" }
38 | android-application = { id = "com.android.application", version.ref = "agp" }
39 | android-library = { id = "com.android.library", version.ref = "agp" }
40 | maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" }
41 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/simple/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
56 |
57 |
58 |
64 |
66 |
72 |
73 |
74 |
75 |
77 |
78 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/simple/composeApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalKotlinGradlePluginApi::class)
2 |
3 | import org.jetbrains.compose.ExperimentalComposeLibrary
4 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
5 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
6 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
7 |
8 | plugins {
9 | alias(libs.plugins.multiplatform)
10 | alias(libs.plugins.compose.compiler)
11 | alias(libs.plugins.compose)
12 | alias(libs.plugins.android.application)
13 | }
14 |
15 | kotlin {
16 | jvmToolchain(17)
17 | androidTarget {
18 | //https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html
19 | instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test)
20 | }
21 |
22 |
23 |
24 | listOf(
25 | iosX64(),
26 | iosArm64(),
27 | iosSimulatorArm64()
28 | ).forEach {
29 | it.binaries.framework {
30 | baseName = "ComposeApp"
31 | isStatic = true
32 | }
33 | }
34 |
35 | sourceSets {
36 | commonMain.dependencies {
37 | implementation(compose.runtime)
38 | implementation(compose.foundation)
39 | implementation(compose.material3)
40 | implementation(compose.components.resources)
41 | implementation(compose.components.uiToolingPreview)
42 | implementation(libs.lifecycle.viewmodel.compose)
43 |
44 | implementation(project(":klocation"))
45 | }
46 |
47 | commonTest.dependencies {
48 | implementation(kotlin("test"))
49 | @OptIn(ExperimentalComposeLibrary::class)
50 | implementation(compose.uiTest)
51 | }
52 |
53 | androidMain.dependencies {
54 | implementation(compose.uiTooling)
55 | implementation(libs.androidx.activityCompose)
56 | }
57 |
58 | // jvmMain.dependencies {
59 | // implementation(compose.desktop.currentOs)
60 | // }
61 |
62 | iosMain.dependencies {
63 | }
64 |
65 | }
66 | }
67 |
68 |
69 | android {
70 | namespace = "io.github.sample"
71 | compileSdk = 35
72 |
73 | defaultConfig {
74 | minSdk = 21
75 | targetSdk = 35
76 |
77 | applicationId = "org.company.app.androidApp"
78 | versionCode = 1
79 | versionName = "1.0.0"
80 |
81 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
82 | }
83 | }
84 |
85 | //https://developer.android.com/develop/ui/compose/testing#setup
86 | dependencies {
87 | androidTestImplementation(libs.androidx.uitest.junit4)
88 | debugImplementation(libs.androidx.uitest.testManifest)
89 | //temporary fix: https://youtrack.jetbrains.com/issue/CMP-5864
90 | androidTestImplementation("androidx.test:monitor") {
91 | version { strictly("1.6.1") }
92 | }
93 |
94 | commonMainApi(libs.permissions)
95 |
96 |
97 | }
98 |
99 | compose.desktop {
100 | application {
101 | mainClass = "MainKt"
102 |
103 | nativeDistributions {
104 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
105 | packageName = "ComposeApp"
106 | packageVersion = "1.0.0"
107 |
108 | linux {
109 | iconFile.set(project.file("desktopAppIcons/LinuxIcon.png"))
110 | }
111 | windows {
112 | iconFile.set(project.file("desktopAppIcons/WindowsIcon.ico"))
113 | }
114 | macOS {
115 | iconFile.set(project.file("desktopAppIcons/MacosIcon.icns"))
116 | bundleID = "org.company.app.desktopApp"
117 | }
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/simple/composeApp/src/commonMain/kotlin/io/github/sample/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package io.github.sample.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | //generated by https://m3.material.io/theme-builder#/custom
6 | //Color palette was taken here: https://colorhunt.co/palettes/popular
7 |
8 | internal val md_theme_light_primary = Color(0xFF00687A)
9 | internal val md_theme_light_onPrimary = Color(0xFFFFFFFF)
10 | internal val md_theme_light_primaryContainer = Color(0xFFABEDFF)
11 | internal val md_theme_light_onPrimaryContainer = Color(0xFF001F26)
12 | internal val md_theme_light_secondary = Color(0xFF00696E)
13 | internal val md_theme_light_onSecondary = Color(0xFFFFFFFF)
14 | internal val md_theme_light_secondaryContainer = Color(0xFF6FF6FE)
15 | internal val md_theme_light_onSecondaryContainer = Color(0xFF002022)
16 | internal val md_theme_light_tertiary = Color(0xFF904D00)
17 | internal val md_theme_light_onTertiary = Color(0xFFFFFFFF)
18 | internal val md_theme_light_tertiaryContainer = Color(0xFFFFDCC2)
19 | internal val md_theme_light_onTertiaryContainer = Color(0xFF2E1500)
20 | internal val md_theme_light_error = Color(0xFFBA1A1A)
21 | internal val md_theme_light_errorContainer = Color(0xFFFFDAD6)
22 | internal val md_theme_light_onError = Color(0xFFFFFFFF)
23 | internal val md_theme_light_onErrorContainer = Color(0xFF410002)
24 | internal val md_theme_light_background = Color(0xFFFFFBFF)
25 | internal val md_theme_light_onBackground = Color(0xFF221B00)
26 | internal val md_theme_light_surface = Color(0xFFFFFBFF)
27 | internal val md_theme_light_onSurface = Color(0xFF221B00)
28 | internal val md_theme_light_surfaceVariant = Color(0xFFDBE4E7)
29 | internal val md_theme_light_onSurfaceVariant = Color(0xFF3F484B)
30 | internal val md_theme_light_outline = Color(0xFF70797B)
31 | internal val md_theme_light_inverseOnSurface = Color(0xFFFFF0C0)
32 | internal val md_theme_light_inverseSurface = Color(0xFF3A3000)
33 | internal val md_theme_light_inversePrimary = Color(0xFF55D6F4)
34 | internal val md_theme_light_shadow = Color(0xFF000000)
35 | internal val md_theme_light_surfaceTint = Color(0xFF00687A)
36 | internal val md_theme_light_outlineVariant = Color(0xFFBFC8CB)
37 | internal val md_theme_light_scrim = Color(0xFF000000)
38 |
39 | internal val md_theme_dark_primary = Color(0xFF55D6F4)
40 | internal val md_theme_dark_onPrimary = Color(0xFF003640)
41 | internal val md_theme_dark_primaryContainer = Color(0xFF004E5C)
42 | internal val md_theme_dark_onPrimaryContainer = Color(0xFFABEDFF)
43 | internal val md_theme_dark_secondary = Color(0xFF4CD9E2)
44 | internal val md_theme_dark_onSecondary = Color(0xFF00373A)
45 | internal val md_theme_dark_secondaryContainer = Color(0xFF004F53)
46 | internal val md_theme_dark_onSecondaryContainer = Color(0xFF6FF6FE)
47 | internal val md_theme_dark_tertiary = Color(0xFFFFB77C)
48 | internal val md_theme_dark_onTertiary = Color(0xFF4D2700)
49 | internal val md_theme_dark_tertiaryContainer = Color(0xFF6D3900)
50 | internal val md_theme_dark_onTertiaryContainer = Color(0xFFFFDCC2)
51 | internal val md_theme_dark_error = Color(0xFFFFB4AB)
52 | internal val md_theme_dark_errorContainer = Color(0xFF93000A)
53 | internal val md_theme_dark_onError = Color(0xFF690005)
54 | internal val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
55 | internal val md_theme_dark_background = Color(0xFF221B00)
56 | internal val md_theme_dark_onBackground = Color(0xFFFFE264)
57 | internal val md_theme_dark_surface = Color(0xFF221B00)
58 | internal val md_theme_dark_onSurface = Color(0xFFFFE264)
59 | internal val md_theme_dark_surfaceVariant = Color(0xFF3F484B)
60 | internal val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CB)
61 | internal val md_theme_dark_outline = Color(0xFF899295)
62 | internal val md_theme_dark_inverseOnSurface = Color(0xFF221B00)
63 | internal val md_theme_dark_inverseSurface = Color(0xFFFFE264)
64 | internal val md_theme_dark_inversePrimary = Color(0xFF00687A)
65 | internal val md_theme_dark_shadow = Color(0xFF000000)
66 | internal val md_theme_dark_surfaceTint = Color(0xFF55D6F4)
67 | internal val md_theme_dark_outlineVariant = Color(0xFF3F484B)
68 | internal val md_theme_dark_scrim = Color(0xFF000000)
69 |
70 |
71 | internal val seed = Color(0xFF2C3639)
72 |
--------------------------------------------------------------------------------
/simple/composeApp/src/commonMain/kotlin/io/github/sample/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package io.github.sample.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Surface
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.lightColorScheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.CompositionLocalProvider
10 | import androidx.compose.runtime.compositionLocalOf
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 |
15 | private val LightColorScheme = lightColorScheme(
16 | primary = md_theme_light_primary,
17 | onPrimary = md_theme_light_onPrimary,
18 | primaryContainer = md_theme_light_primaryContainer,
19 | onPrimaryContainer = md_theme_light_onPrimaryContainer,
20 | secondary = md_theme_light_secondary,
21 | onSecondary = md_theme_light_onSecondary,
22 | secondaryContainer = md_theme_light_secondaryContainer,
23 | onSecondaryContainer = md_theme_light_onSecondaryContainer,
24 | tertiary = md_theme_light_tertiary,
25 | onTertiary = md_theme_light_onTertiary,
26 | tertiaryContainer = md_theme_light_tertiaryContainer,
27 | onTertiaryContainer = md_theme_light_onTertiaryContainer,
28 | error = md_theme_light_error,
29 | errorContainer = md_theme_light_errorContainer,
30 | onError = md_theme_light_onError,
31 | onErrorContainer = md_theme_light_onErrorContainer,
32 | background = md_theme_light_background,
33 | onBackground = md_theme_light_onBackground,
34 | surface = md_theme_light_surface,
35 | onSurface = md_theme_light_onSurface,
36 | surfaceVariant = md_theme_light_surfaceVariant,
37 | onSurfaceVariant = md_theme_light_onSurfaceVariant,
38 | outline = md_theme_light_outline,
39 | inverseOnSurface = md_theme_light_inverseOnSurface,
40 | inverseSurface = md_theme_light_inverseSurface,
41 | inversePrimary = md_theme_light_inversePrimary,
42 | surfaceTint = md_theme_light_surfaceTint,
43 | outlineVariant = md_theme_light_outlineVariant,
44 | scrim = md_theme_light_scrim,
45 | )
46 |
47 | private val DarkColorScheme = darkColorScheme(
48 | primary = md_theme_dark_primary,
49 | onPrimary = md_theme_dark_onPrimary,
50 | primaryContainer = md_theme_dark_primaryContainer,
51 | onPrimaryContainer = md_theme_dark_onPrimaryContainer,
52 | secondary = md_theme_dark_secondary,
53 | onSecondary = md_theme_dark_onSecondary,
54 | secondaryContainer = md_theme_dark_secondaryContainer,
55 | onSecondaryContainer = md_theme_dark_onSecondaryContainer,
56 | tertiary = md_theme_dark_tertiary,
57 | onTertiary = md_theme_dark_onTertiary,
58 | tertiaryContainer = md_theme_dark_tertiaryContainer,
59 | onTertiaryContainer = md_theme_dark_onTertiaryContainer,
60 | error = md_theme_dark_error,
61 | errorContainer = md_theme_dark_errorContainer,
62 | onError = md_theme_dark_onError,
63 | onErrorContainer = md_theme_dark_onErrorContainer,
64 | background = md_theme_dark_background,
65 | onBackground = md_theme_dark_onBackground,
66 | surface = md_theme_dark_surface,
67 | onSurface = md_theme_dark_onSurface,
68 | surfaceVariant = md_theme_dark_surfaceVariant,
69 | onSurfaceVariant = md_theme_dark_onSurfaceVariant,
70 | outline = md_theme_dark_outline,
71 | inverseOnSurface = md_theme_dark_inverseOnSurface,
72 | inverseSurface = md_theme_dark_inverseSurface,
73 | inversePrimary = md_theme_dark_inversePrimary,
74 | surfaceTint = md_theme_dark_surfaceTint,
75 | outlineVariant = md_theme_dark_outlineVariant,
76 | scrim = md_theme_dark_scrim,
77 | )
78 |
79 | internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) }
80 |
81 | @Composable
82 | internal fun AppTheme(
83 | content: @Composable () -> Unit
84 | ) {
85 | val systemIsDark = isSystemInDarkTheme()
86 | val isDarkState = remember { mutableStateOf(systemIsDark) }
87 | CompositionLocalProvider(
88 | LocalThemeIsDark provides isDarkState
89 | ) {
90 | val isDark by isDarkState
91 | SystemAppearance(!isDark)
92 | MaterialTheme(
93 | colorScheme = if (isDark) DarkColorScheme else LightColorScheme,
94 | content = { Surface(content = content) }
95 | )
96 | }
97 | }
98 |
99 | @Composable
100 | internal expect fun SystemAppearance(isDark: Boolean)
101 |
--------------------------------------------------------------------------------
/simple/composeApp/src/commonMain/kotlin/io/github/sample/App.kt:
--------------------------------------------------------------------------------
1 | package io.github.sample
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.WindowInsets
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.safeDrawing
13 | import androidx.compose.foundation.layout.windowInsetsPadding
14 | import androidx.compose.material3.ElevatedButton
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.LaunchedEffect
19 | import androidx.compose.runtime.derivedStateOf
20 | import androidx.compose.runtime.getValue
21 | import androidx.compose.runtime.mutableStateOf
22 | import androidx.compose.runtime.remember
23 | import androidx.compose.runtime.rememberCoroutineScope
24 | import androidx.compose.runtime.setValue
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.unit.dp
28 | import io.github.sample.theme.AppTheme
29 | import io.github.tbib.klocation.KLocationService
30 | import kotlinx.coroutines.launch
31 |
32 | @Composable
33 | internal fun App() = AppTheme {
34 | var isInit by remember { mutableStateOf(false) }
35 |
36 |
37 | val viewModel by remember { mutableStateOf(LocationViewModel()) }
38 |
39 | val scope = rememberCoroutineScope()
40 | // LaunchedEffect for requesting permission
41 | LaunchedEffect(isInit) {
42 | if (!isInit) {
43 | viewModel.init()
44 | isInit = true
45 |
46 | }
47 |
48 | }
49 | val userLocation by remember { derivedStateOf { viewModel.userLocation } }
50 | val isGPSOpen by remember { derivedStateOf { viewModel.isGPSOpen } }
51 |
52 | var isLoading by remember { mutableStateOf(true) }
53 |
54 | LaunchedEffect(userLocation) {
55 | isLoading = userLocation == null
56 | }
57 | if (viewModel.requestPermission) {
58 | KLocationService().EnableLocation()
59 | viewModel.requestPermission = false
60 | }
61 |
62 |
63 | Column(
64 | modifier = Modifier
65 | .fillMaxSize()
66 | .windowInsetsPadding(WindowInsets.safeDrawing)
67 | .padding(16.dp),
68 | horizontalAlignment = Alignment.CenterHorizontally,
69 | verticalArrangement = Arrangement.Center
70 | ) {
71 | if (isLoading) {
72 | Text(
73 | text = "Fetching location...",
74 | style = MaterialTheme.typography.bodyMedium,
75 | color = MaterialTheme.colorScheme.primary
76 | )
77 | } else {
78 | Text(
79 | text = "User Location:",
80 | style = MaterialTheme.typography.headlineSmall,
81 | color = MaterialTheme.colorScheme.primary
82 | )
83 | Text(
84 | text = "Latitude: ${userLocation?.latitude}\nLongitude: ${userLocation?.longitude}",
85 | style = MaterialTheme.typography.bodyLarge,
86 | color = MaterialTheme.colorScheme.onSurface,
87 | modifier = Modifier.padding(top = 8.dp)
88 | )
89 |
90 | ElevatedButton(onClick = {
91 | scope.launch {
92 | println("location ${viewModel.getLocation()}")
93 | }
94 | }) {
95 | Text("print location")
96 | }
97 | }
98 |
99 | TopToast(
100 | isVisible = !isGPSOpen,
101 | message = "GPS is not active. Tap to enable.",
102 | onClick = {
103 | viewModel.enableGPSAndLocation()
104 | }
105 | )
106 | }
107 | }
108 |
109 | @Composable
110 | fun TopToast(
111 | message: String,
112 | isVisible: Boolean,
113 | onClick: () -> Unit
114 | ) {
115 | if (isVisible) {
116 | Box(
117 | modifier = Modifier
118 | .fillMaxWidth()
119 | .padding(horizontal = 16.dp, vertical = 8.dp)
120 | .background(
121 | color = MaterialTheme.colorScheme.error,
122 | shape = MaterialTheme.shapes.medium
123 | )
124 | .clickable { onClick() }
125 | .padding(12.dp),
126 | contentAlignment = Alignment.Center
127 | ) {
128 | Text(
129 | text = message,
130 | color = MaterialTheme.colorScheme.onError,
131 | style = MaterialTheme.typography.bodyLarge
132 | )
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/klocation/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalKotlinGradlePluginApi::class)
2 |
3 | import com.vanniktech.maven.publish.SonatypeHost
4 | import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.getCurrentOperatingSystem
5 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
6 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
7 |
8 | plugins {
9 | alias(libs.plugins.multiplatform)
10 | alias(libs.plugins.compose.compiler)
11 | alias(libs.plugins.compose)
12 | alias(libs.plugins.android.library)
13 | id("maven-publish")
14 | id("signing")
15 | alias(libs.plugins.maven.publish)
16 | }
17 |
18 |
19 |
20 |
21 | apply(plugin = "maven-publish")
22 | apply(plugin = "signing")
23 |
24 |
25 | tasks.withType {
26 | val isMac = getCurrentOperatingSystem().isMacOsX
27 | onlyIf {
28 | isMac.also {
29 | if (!isMac) logger.error(
30 | """
31 | Publishing the library requires macOS to be able to generate iOS artifacts.
32 | Run the task on a mac or use the project GitHub workflows for publication and release.
33 | """
34 | )
35 | }
36 | }
37 | }
38 |
39 |
40 | extra["packageNameSpace"] = "io.github.klocation"
41 | extra["groupId"] = "io.github.the-best-is-best"
42 | extra["artifactId"] = "klocation"
43 | extra["version"] = "1.0.7"
44 | extra["packageName"] = "KLocation"
45 | extra["packageUrl"] = "https://github.com/the-best-is-best/klocation"
46 | extra["packageDescription"] =
47 | "KLocation is a Kotlin Multiplatform (KMP) or Compose Multiplatform (CMP) library designed to simplify retrieving the current location and handling location updates in Android and iOS. It provides a unified API for accessing location services across platforms, enabling developers to focus on building location-aware features without worrying about platform-specific implementations."
48 | extra["system"] = "GITHUB"
49 | extra["issueUrl"] = "https://github.com/the-best-is-best/klocation"
50 | extra["connectionGit"] = "https://github.com/the-best-is-best/klocation.git"
51 |
52 | extra["developerName"] = "Michelle Raouf"
53 | extra["developerNameId"] = "MichelleRaouf"
54 | extra["developerEmail"] = "eng.michelle.raouf@gmail.com"
55 |
56 |
57 | mavenPublishing {
58 | coordinates(
59 | extra["groupId"].toString(),
60 | extra["artifactId"].toString(),
61 | extra["version"].toString()
62 | )
63 |
64 | publishToMavenCentral(SonatypeHost.S01, true)
65 | signAllPublications()
66 |
67 | pom {
68 | name.set(extra["packageName"].toString())
69 | description.set(extra["packageDescription"].toString())
70 | url.set(extra["packageUrl"].toString())
71 | licenses {
72 | license {
73 | name.set("Apache-2.0")
74 | url.set("https://opensource.org/licenses/Apache-2.0")
75 | }
76 | }
77 | issueManagement {
78 | system.set(extra["system"].toString())
79 | url.set(extra["issueUrl"].toString())
80 | }
81 | scm {
82 | connection.set(extra["connectionGit"].toString())
83 | url.set(extra["packageUrl"].toString())
84 | }
85 | developers {
86 | developer {
87 | id.set(extra["developerNameId"].toString())
88 | name.set(extra["developerName"].toString())
89 | email.set(extra["developerEmail"].toString())
90 | }
91 | }
92 | }
93 |
94 | }
95 |
96 |
97 | signing {
98 | useGpgCmd()
99 | sign(publishing.publications)
100 | }
101 |
102 | val packageNameSpace = extra["packageNameSpace"].toString()
103 |
104 | kotlin {
105 | jvmToolchain(17)
106 | androidTarget {
107 | //https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html
108 | instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test)
109 | }
110 |
111 | listOf(
112 | iosX64(),
113 | iosArm64(),
114 | iosSimulatorArm64()
115 | ).forEach {
116 | it.binaries.framework {
117 | baseName = packageNameSpace
118 | isStatic = true
119 | }
120 | }
121 |
122 | sourceSets {
123 | commonMain.dependencies {
124 | implementation(compose.runtime)
125 | // implementation(compose.foundation)
126 | // implementation(compose.material3)
127 | // implementation(compose.components.resources)
128 | // implementation(compose.components.uiToolingPreview)
129 |
130 | implementation(libs.kotlinx.coroutines.core)
131 | }
132 |
133 | commonTest.dependencies {
134 | implementation(kotlin("test"))
135 | // @OptIn(ExperimentalComposeLibrary::class)
136 | // implementation(compose.uiTest)
137 | }
138 |
139 | androidMain.dependencies {
140 | // implementation(compose.uiTooling)
141 | implementation(libs.androidx.activityCompose)
142 | implementation(libs.play.services.location)
143 | implementation(libs.kotlinx.coroutines.play.services)
144 | implementation(libs.accompanist.permissions)
145 |
146 |
147 | }
148 |
149 |
150 |
151 | iosMain.dependencies {
152 | }
153 |
154 | }
155 | }
156 |
157 |
158 | android {
159 | namespace = extra["packageNameSpace"].toString()
160 | compileSdk = 35
161 |
162 | defaultConfig {
163 | minSdk = 21
164 | compileOptions {
165 | sourceCompatibility = JavaVersion.VERSION_17
166 | targetCompatibility = JavaVersion.VERSION_17
167 | }
168 |
169 | }
170 | buildFeatures {
171 | compose = true
172 | }
173 | }
174 |
175 | //https://developer.android.com/develop/ui/compose/testing#setup
176 | dependencies {
177 | androidTestImplementation(libs.androidx.uitest.junit4)
178 | debugImplementation(libs.androidx.uitest.testManifest)
179 | //temporary fix: https://youtrack.jetbrains.com/issue/CMP-5864
180 | androidTestImplementation("androidx.test:monitor") {
181 | version { strictly("1.6.1") }
182 | }
183 | }
184 |
185 | //
186 | //compose.desktop {
187 | // application {
188 | // mainClass = "MainKt"
189 | //
190 | // nativeDistributions {
191 | // targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
192 | // packageName = packageNameSpace
193 | // packageVersion = "1.0.0"
194 | //
195 | // linux {
196 | // iconFile.set(project.file("desktopAppIcons/LinuxIcon.png"))
197 | // }
198 | // windows {
199 | // iconFile.set(project.file("desktopAppIcons/WindowsIcon.ico"))
200 | // }
201 | // macOS {
202 | // iconFile.set(project.file("desktopAppIcons/MacosIcon.icns"))
203 | // bundleID = "org.company.app.desktopApp"
204 | // }
205 | // }
206 | // }
207 | //}
--------------------------------------------------------------------------------
/klocation/src/iosMain/kotlin/io/github/tbib/klocation/KLocationService.ios.kt:
--------------------------------------------------------------------------------
1 | package io.github.tbib.klocation
2 |
3 | import androidx.compose.runtime.Composable
4 | import kotlinx.cinterop.ExperimentalForeignApi
5 | import kotlinx.cinterop.memScoped
6 | import kotlinx.cinterop.pointed
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.GlobalScope
9 | import kotlinx.coroutines.IO
10 | import kotlinx.coroutines.MainScope
11 | import kotlinx.coroutines.delay
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.MutableSharedFlow
14 | import kotlinx.coroutines.flow.MutableStateFlow
15 | import kotlinx.coroutines.flow.channelFlow
16 | import kotlinx.coroutines.flow.first
17 | import kotlinx.coroutines.launch
18 | import kotlinx.coroutines.withContext
19 | import platform.CoreLocation.CLLocation
20 | import platform.CoreLocation.CLLocationManager
21 | import platform.CoreLocation.CLLocationManagerDelegateProtocol
22 | import platform.CoreLocation.kCLAuthorizationStatusAuthorizedAlways
23 | import platform.CoreLocation.kCLAuthorizationStatusAuthorizedWhenInUse
24 | import platform.CoreLocation.kCLAuthorizationStatusNotDetermined
25 | import platform.CoreLocation.kCLLocationAccuracyNearestTenMeters
26 | import platform.Foundation.NSError
27 | import platform.Foundation.NSLog
28 | import platform.Foundation.NSURL
29 | import platform.UIKit.UIAlertAction
30 | import platform.UIKit.UIAlertActionStyleCancel
31 | import platform.UIKit.UIAlertActionStyleDefault
32 | import platform.UIKit.UIAlertController
33 | import platform.UIKit.UIAlertControllerStyleAlert
34 | import platform.UIKit.UIApplication
35 | import platform.UIKit.UIApplicationOpenSettingsURLString
36 | import platform.darwin.NSObject
37 |
38 | actual class KLocationService : NSObject(), CLLocationManagerDelegateProtocol {
39 | private val locationManager = CLLocationManager()
40 | private val locationUpdateChannel = MutableSharedFlow(replay = 1)
41 |
42 | private val _gpsStateFlow = MutableSharedFlow(replay = 1)
43 | private val locationPermissionState = MutableStateFlow(false)
44 |
45 | init {
46 | locationManager.delegate = this
47 | locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
48 | }
49 |
50 | @OptIn(ExperimentalForeignApi::class)
51 | override fun locationManager(manager: CLLocationManager, didUpdateLocations: List<*>) {
52 | val location = (didUpdateLocations.lastOrNull() as? CLLocation)?.let {
53 | memScoped {
54 | val coordinatePtr = it.coordinate.ptr
55 | val latitude = coordinatePtr.pointed.latitude
56 | val longitude = coordinatePtr.pointed.longitude
57 | Location(latitude, longitude)
58 | }
59 | }
60 |
61 | location?.let { locationUpdateChannel.tryEmit(it) }
62 | }
63 |
64 | override fun locationManagerDidChangeAuthorization(manager: CLLocationManager) {
65 | updateLocationPermissionState(locationManager)
66 | MainScope().launch {
67 | emitGpsState(manager)
68 | }
69 | }
70 |
71 | override fun locationManager(manager: CLLocationManager, didFailWithError: NSError) {
72 | NSLog("Failed to retrieve location: ${didFailWithError.localizedDescription}")
73 | }
74 |
75 |
76 | actual suspend fun getCurrentLocation(): Location {
77 | locationManager.startUpdatingLocation()
78 |
79 | return try {
80 | locationUpdateChannel.first().also {
81 | locationManager.stopUpdatingLocation()
82 | }
83 | } catch (e: Exception) {
84 | NSLog("Error fetching location: ${e.message}")
85 | throw e
86 | }
87 | }
88 |
89 | actual fun startLocationUpdates(intervalMillis: Long): Flow = channelFlow {
90 | locationManager.startUpdatingLocation()
91 | try {
92 | locationUpdateChannel.collect { location ->
93 | send(location)
94 | delay(intervalMillis)
95 | }
96 | } catch (e: Exception) {
97 | NSLog("Error during location update collection: ${e.message}")
98 | } finally {
99 | locationManager.stopUpdatingLocation()
100 | }
101 | }
102 |
103 | actual fun isLocationEnabled(): Boolean {
104 | return CLLocationManager.locationServicesEnabled()
105 | }
106 |
107 | actual fun enableLocation() {
108 | when (locationManager.authorizationStatus) {
109 | kCLAuthorizationStatusNotDetermined -> {
110 | locationManager.requestWhenInUseAuthorization()
111 |
112 | }
113 |
114 | kCLAuthorizationStatusAuthorizedWhenInUse,
115 | kCLAuthorizationStatusAuthorizedAlways -> {
116 | NSLog("Location permission already granted.")
117 | updateLocationPermissionState(locationManager)
118 | }
119 |
120 | else -> {
121 | println("open gps location")
122 | showAlertToPromptSettings()
123 | }
124 | }
125 | }
126 |
127 | @Composable
128 | actual fun EnableLocation() {
129 | enableLocation()
130 | }
131 | private suspend fun emitGpsState(manager: CLLocationManager) {
132 | withContext(Dispatchers.Default) {
133 | // Check if location services are enabled and the authorization status
134 | val isEnabled = CLLocationManager.locationServicesEnabled() &&
135 | (manager.authorizationStatus == kCLAuthorizationStatusAuthorizedAlways ||
136 | manager.authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse)
137 |
138 | // Emit the updated state asynchronously
139 | updateLocationPermissionState(manager)
140 | _gpsStateFlow.tryEmit(isEnabled)
141 | }
142 | }
143 |
144 |
145 | override fun locationManager(manager: CLLocationManager, didChangeAuthorizationStatus: Int) {
146 | updateLocationPermissionState(manager)
147 | MainScope().launch {
148 | emitGpsState(manager)
149 | }
150 | }
151 |
152 | actual suspend fun gpsStateFlow(): Flow = channelFlow {
153 | // Use withContext to shift to background thread for initial state check
154 | val initialState = withContext(Dispatchers.IO) {
155 | CLLocationManager.locationServicesEnabled() &&
156 | (locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedAlways ||
157 | locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse)
158 | }
159 |
160 | send(initialState) // Use 'send' instead of 'emit'
161 |
162 | // Collect updates from _gpsStateFlow
163 | _gpsStateFlow.collect { isEnabled ->
164 | send(isEnabled) // Send updates through the flow
165 | }
166 | }
167 |
168 | // Method to update the permission state
169 | private fun updateLocationPermissionState(locationManager: CLLocationManager) {
170 | val status = locationManager.authorizationStatus
171 | locationPermissionState.value = status == kCLAuthorizationStatusAuthorizedWhenInUse ||
172 | status == kCLAuthorizationStatusAuthorizedAlways
173 | }
174 |
175 |
176 | }
177 |
178 | fun showAlertToPromptSettings() {
179 | val topViewController = getTopViewController()
180 | if (topViewController != null) {
181 | val alert = UIAlertController.alertControllerWithTitle(
182 | "Location Services Disabled",
183 | "Location services are turned off. Please enable them in your device settings to use this feature.",
184 | UIAlertControllerStyleAlert
185 | )
186 |
187 | val goToSettingsAction = UIAlertAction.actionWithTitle(
188 | "Go to Settings",
189 | UIAlertActionStyleDefault
190 | ) { _ ->
191 | // Open the app-specific settings page
192 | val appSettingsURL = NSURL(string = UIApplicationOpenSettingsURLString)
193 | UIApplication.sharedApplication.openURL(
194 | appSettingsURL,
195 | options = emptyMap()
196 | ) {}
197 |
198 | }
199 |
200 | alert.addAction(goToSettingsAction)
201 | alert.addAction(UIAlertAction.actionWithTitle("Cancel", UIAlertActionStyleCancel, null))
202 |
203 | topViewController.presentViewController(alert, animated = true, completion = null)
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/klocation/src/androidMain/kotlin/io/github/tbib/klocation/KLocationService.android.kt:
--------------------------------------------------------------------------------
1 | package io.github.tbib.klocation
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.IntentFilter
7 | import android.location.LocationManager
8 | import android.net.Uri
9 | import android.os.Looper
10 | import android.provider.Settings
11 | import android.util.Log
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.LaunchedEffect
14 | import androidx.compose.runtime.derivedStateOf
15 | import androidx.compose.runtime.getValue
16 | import androidx.compose.runtime.remember
17 | import com.google.accompanist.permissions.ExperimentalPermissionsApi
18 | import com.google.accompanist.permissions.isGranted
19 | import com.google.accompanist.permissions.rememberPermissionState
20 | import com.google.accompanist.permissions.shouldShowRationale
21 | import com.google.android.gms.common.api.ApiException
22 | import com.google.android.gms.common.api.CommonStatusCodes
23 | import com.google.android.gms.common.api.ResolvableApiException
24 | import com.google.android.gms.location.FusedLocationProviderClient
25 | import com.google.android.gms.location.LocationCallback
26 | import com.google.android.gms.location.LocationRequest
27 | import com.google.android.gms.location.LocationResult
28 | import com.google.android.gms.location.LocationServices
29 | import com.google.android.gms.location.LocationSettingsRequest
30 | import com.google.android.gms.location.LocationSettingsResponse
31 | import com.google.android.gms.location.SettingsClient
32 | import com.google.android.gms.tasks.Task
33 | import kotlinx.coroutines.flow.Flow
34 | import kotlinx.coroutines.flow.MutableSharedFlow
35 | import kotlinx.coroutines.flow.asSharedFlow
36 | import kotlinx.coroutines.flow.combine
37 | import kotlinx.coroutines.runBlocking
38 | import kotlinx.coroutines.tasks.await
39 |
40 |
41 | actual class KLocationService {
42 | companion object {
43 | private val _isPermissionGranted = MutableSharedFlow(replay = 1)
44 |
45 | }
46 |
47 | private val context = AndroidKLocationService.getActivity()
48 |
49 | private val fusedLocationClient: FusedLocationProviderClient =
50 | LocationServices.getFusedLocationProviderClient(context)
51 |
52 | private val _locationFlow = MutableSharedFlow(replay = 1)
53 | private val _isGPSEnabledFlow = MutableSharedFlow(replay = 1)
54 |
55 | init {
56 | runBlocking {
57 | _isGPSEnabledFlow.tryEmit(isLocationEnabled())
58 | }
59 | val filter = IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)
60 | context.registerReceiver(object : BroadcastReceiver() {
61 | override fun onReceive(context: Context?, intent: Intent?) {
62 | val isEnabled = isLocationEnabled()
63 |
64 | if (_isGPSEnabledFlow.replayCache.first() != isEnabled) {
65 | _isGPSEnabledFlow.tryEmit(isEnabled)
66 | }
67 | }
68 | }, filter)
69 | }
70 |
71 | actual suspend fun getCurrentLocation(): Location {
72 | return try {
73 | val location = fusedLocationClient.lastLocation.await()
74 | if (location != null) {
75 | Location(location.latitude, location.longitude)
76 | } else {
77 | throw Exception("Failed to get current location")
78 | }
79 | } catch (e: Exception) {
80 | Log.e("LocationService", "Error getting current location: ${e.message}")
81 | throw e
82 | }
83 | }
84 |
85 | actual fun startLocationUpdates(intervalMillis: Long): Flow {
86 | val locationRequest = LocationRequest.Builder(
87 | AndroidKLocationService.getAccuracyPriority().value,
88 | intervalMillis
89 | ).build()
90 | val locationCallback = object : LocationCallback() {
91 | override fun onLocationResult(locationResult: LocationResult) {
92 | locationResult.lastLocation?.let { location ->
93 | _locationFlow.tryEmit(Location(location.latitude, location.longitude))
94 | }
95 | }
96 | }
97 | fusedLocationClient.requestLocationUpdates(
98 | locationRequest,
99 | locationCallback,
100 | Looper.getMainLooper()
101 | )
102 | return _locationFlow
103 | }
104 |
105 |
106 | actual fun isLocationEnabled(): Boolean {
107 | val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
108 | return try {
109 | val gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
110 | val networkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
111 | gpsEnabled || networkEnabled
112 | } catch (e: SecurityException) {
113 | Log.e("LocationService", "SecurityException: ${e.message}")
114 | false
115 | }
116 | }
117 | actual fun enableLocation() {
118 | if (!isLocationEnabled()) {
119 | val locationRequest = LocationRequest.Builder(
120 | AndroidKLocationService.getAccuracyPriority().value,
121 | 10 * 1000 // Interval in milliseconds
122 | ).build()
123 |
124 | val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
125 | val client: SettingsClient = LocationServices.getSettingsClient(context)
126 | val task: Task = client.checkLocationSettings(builder.build())
127 |
128 | task.addOnSuccessListener {
129 | Log.d("LocationService", "Location settings are satisfied")
130 | }.addOnFailureListener { exception ->
131 | if (exception is ResolvableApiException) {
132 | // Start resolution for enabling location
133 | exception.startResolutionForResult(context, 1)
134 | } else {
135 | Log.e("LocationService", "Location settings error: ${exception.message}")
136 |
137 | // Check if the user has denied permission and open the settings if needed
138 | if (exception is ApiException && exception.statusCode == CommonStatusCodes.RESOLUTION_REQUIRED) {
139 | openAppSettings()
140 | }
141 | }
142 | }
143 | }
144 | }
145 |
146 | private fun openAppSettings() {
147 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
148 | data = Uri.fromParts("package", context.packageName, null)
149 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
150 | }
151 | context.startActivity(intent)
152 | }
153 |
154 | @OptIn(ExperimentalPermissionsApi::class)
155 | @Composable
156 | actual fun EnableLocation() {
157 | val locationPermissionState = rememberPermissionState(
158 | android.Manifest.permission.ACCESS_FINE_LOCATION
159 | )
160 |
161 | // Check if the permission is already granted
162 | if (!locationPermissionState.status.isGranted) {
163 | // Check if the permission was denied permanently
164 | if (locationPermissionState.status.shouldShowRationale) {
165 | // Permission denied permanently, prompt user to open app settings
166 | LaunchedEffect(Unit) {
167 | openAppSettings()
168 | }
169 | } else {
170 | // Request permission
171 | LaunchedEffect(Unit) {
172 | locationPermissionState.launchPermissionRequest()
173 | }
174 | }
175 | } else {
176 | enableLocation()
177 | }
178 | }
179 |
180 | actual suspend fun gpsStateFlow(): Flow = combine(
181 | _isGPSEnabledFlow.asSharedFlow(),
182 | _isPermissionGranted.asSharedFlow()
183 | ) { isGPSEnabled, isPermissionGranted ->
184 | isGPSEnabled && isPermissionGranted
185 | }
186 |
187 |
188 | @OptIn(ExperimentalPermissionsApi::class)
189 | @Composable
190 | fun ListenerToPermission() {
191 | val locationPermissionState = rememberPermissionState(
192 | android.Manifest.permission.ACCESS_FINE_LOCATION
193 | )
194 |
195 | val isLocationPermissionEnabled by remember {
196 | derivedStateOf { locationPermissionState.status.isGranted }
197 | }
198 | LaunchedEffect(isLocationPermissionEnabled) {
199 | _isPermissionGranted.tryEmit(isLocationPermissionEnabled)
200 | }
201 | }
202 |
203 |
204 | }
205 |
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 | KLocation
2 |
3 |
4 |

5 |
6 |
7 |
8 |

9 |

10 |
11 |

12 |
13 |
14 | ### KLocation is a Kotlin Multiplatform (KMP) or Compose Multiplatform (CMP) library designed to simplify retrieving the current location and handling location updates in Android and iOS. It provides a unified API for accessing location services across platforms, enabling developers to focus on building location-aware features without worrying about platform-specific implementations
15 |
16 |
17 |
18 | [](https://central.sonatype.com/artifact/io.github.the-best-is-best/klocation)
19 |
20 | KLocation is available on `mavenCentral()`.
21 |
22 | ## Install
23 |
24 | ```kotlin
25 | implementation("io.github.the-best-is-best:klocation:1.0.7")
26 | ```
27 |
28 | ### First in iosMain
29 |
30 | ```kotlin
31 | fun MainViewController(): UIViewController {
32 | IOSKLocationServices().requestPermission()
33 | return ComposeUIViewController { App() }
34 | }
35 |
36 | ```
37 |
38 | ### Second in android Main
39 |
40 | ```kotlin
41 | AndroidKLocationService.initialization(this, AccuracyPriority.BALANCED_POWER_ACCURACY)
42 | setContent {
43 | // add this
44 | KLocationService().ListenerToPermission()
45 | App()
46 | }
47 | }
48 | ```
49 |
50 | ### in commonMain
51 |
52 | ```kotlin
53 | package io.github.sample
54 |
55 | import androidx.compose.foundation.background
56 | import androidx.compose.foundation.clickable
57 | import androidx.compose.foundation.layout.Arrangement
58 | import androidx.compose.foundation.layout.Box
59 | import androidx.compose.foundation.layout.Column
60 | import androidx.compose.foundation.layout.WindowInsets
61 | import androidx.compose.foundation.layout.fillMaxSize
62 | import androidx.compose.foundation.layout.fillMaxWidth
63 | import androidx.compose.foundation.layout.padding
64 | import androidx.compose.foundation.layout.safeDrawing
65 | import androidx.compose.foundation.layout.windowInsetsPadding
66 | import androidx.compose.material3.ElevatedButton
67 | import androidx.compose.material3.MaterialTheme
68 | import androidx.compose.material3.Text
69 | import androidx.compose.runtime.Composable
70 | import androidx.compose.runtime.LaunchedEffect
71 | import androidx.compose.runtime.derivedStateOf
72 | import androidx.compose.runtime.getValue
73 | import androidx.compose.runtime.mutableStateOf
74 | import androidx.compose.runtime.remember
75 | import androidx.compose.runtime.rememberCoroutineScope
76 | import androidx.compose.runtime.setValue
77 | import androidx.compose.ui.Alignment
78 | import androidx.compose.ui.Modifier
79 | import androidx.compose.ui.unit.dp
80 | import dev.icerock.moko.permissions.compose.BindEffect
81 | import dev.icerock.moko.permissions.compose.rememberPermissionsControllerFactory
82 | import io.github.sample.theme.AppTheme
83 | import kotlinx.coroutines.launch
84 |
85 | @Composable
86 | internal fun App() = AppTheme {
87 | var isInit by remember { mutableStateOf(false) }
88 |
89 |
90 | val viewModel by remember { mutableStateOf(LocationViewModel()) }
91 |
92 | val scope = rememberCoroutineScope()
93 | // LaunchedEffect for requesting permission
94 | LaunchedEffect(isInit) {
95 | if (!isInit) {
96 | viewModel.init()
97 | isInit = true
98 |
99 | }
100 |
101 | }
102 | val userLocation by remember { derivedStateOf { viewModel.userLocation } }
103 | val isGPSOpen by remember { derivedStateOf { viewModel.isGPSOpen } }
104 |
105 | var isLoading by remember { mutableStateOf(true) }
106 |
107 | LaunchedEffect(userLocation) {
108 | isLoading = userLocation == null
109 | }
110 | if (viewModel.requestPermission) {
111 | KLocationService().EnableLocation()
112 | viewModel.requestPermission = false
113 | }
114 |
115 |
116 | Column(
117 | modifier = Modifier
118 | .fillMaxSize()
119 | .windowInsetsPadding(WindowInsets.safeDrawing)
120 | .padding(16.dp),
121 | horizontalAlignment = Alignment.CenterHorizontally,
122 | verticalArrangement = Arrangement.Center
123 | ) {
124 | if (isLoading) {
125 | Text(
126 | text = "Fetching location...",
127 | style = MaterialTheme.typography.bodyMedium,
128 | color = MaterialTheme.colorScheme.primary
129 | )
130 | } else {
131 | Text(
132 | text = "User Location:",
133 | style = MaterialTheme.typography.headlineSmall,
134 | color = MaterialTheme.colorScheme.primary
135 | )
136 | Text(
137 | text = "Latitude: ${userLocation?.latitude}\nLongitude: ${userLocation?.longitude}",
138 | style = MaterialTheme.typography.bodyLarge,
139 | color = MaterialTheme.colorScheme.onSurface,
140 | modifier = Modifier.padding(top = 8.dp)
141 | )
142 |
143 | ElevatedButton(onClick = {
144 | scope.launch {
145 | println("location ${viewModel.getLocation()}")
146 | }
147 | }) {
148 | Text("print location")
149 | }
150 | }
151 |
152 | TopToast(
153 | isVisible = !isGPSOpen,
154 | message = "GPS is not active. Tap to enable.",
155 | onClick = {
156 | viewModel.enableGPSAndLocation()
157 | }
158 | )
159 | }
160 | }
161 |
162 | @Composable
163 | fun TopToast(
164 | message: String,
165 | isVisible: Boolean,
166 | onClick: () -> Unit
167 | ) {
168 | if (isVisible) {
169 | Box(
170 | modifier = Modifier
171 | .fillMaxWidth()
172 | .padding(horizontal = 16.dp, vertical = 8.dp)
173 | .background(
174 | color = MaterialTheme.colorScheme.error,
175 | shape = MaterialTheme.shapes.medium
176 | )
177 | .clickable { onClick() }
178 | .padding(12.dp),
179 | contentAlignment = Alignment.Center
180 | ) {
181 | Text(
182 | text = message,
183 | color = MaterialTheme.colorScheme.onError,
184 | style = MaterialTheme.typography.bodyLarge
185 | )
186 | }
187 | }
188 | }
189 |
190 | class LocationViewModel : ViewModel() {
191 | private val locationService = KLocationService()
192 |
193 | var userLocation by mutableStateOf(null)
194 | var isGPSOpen by mutableStateOf(false)
195 |
196 | var requestPermission by mutableStateOf(false)
197 |
198 |
199 | fun init() {
200 | requestPermission = true
201 | // viewModelScope.launch {
202 | //
203 | // try {
204 | // controller.providePermission(Permission.LOCATION)
205 | // } catch (e: Exception) {
206 | //
207 | // }
208 | //
209 | // }
210 |
211 | viewModelScope.launch {
212 |
213 |
214 | // Request location permission if not already granted
215 | locationService.gpsStateFlow().collect { isGps ->
216 | println("gps is $isGps")
217 | isGPSOpen = isGps
218 | addListenerLocation()
219 | }
220 |
221 | }
222 | }
223 |
224 | private fun addListenerLocation() {
225 | println("addListenerLocation has called $isGPSOpen")
226 | if (isGPSOpen) {
227 | viewModelScope.launch {
228 | locationService.startLocationUpdates().collect { newLocation ->
229 | userLocation = newLocation
230 | }
231 | }
232 | }
233 | }
234 |
235 | suspend fun getLocation(): Location {
236 | return locationService.getCurrentLocation()
237 | }
238 |
239 | fun enableGPSAndLocation() {
240 | requestPermission = true
241 | }
242 |
243 | @Deprecated("use enableGPSAndLocation")
244 | fun enableLocation() {
245 | locationService.enableLocation()
246 | }
247 | }
248 |
249 |
250 |
251 | ```
252 |
253 | ### Note can check GPS and enable it in v 1.0.2
254 |
255 | ```kotlin
256 | val locationService = KLocationService()
257 | location.gpsStateFlow() // listener for gps
258 | location.enableLocation()
259 | location.isLocationEnabled() //for check location is enabled or not one time
260 | ```
261 |
262 | ### Note can check for is location enabled in v 1.0.1
263 |
264 | ```kotlin
265 | val locationService = KLocationService()
266 | val isEnabledLocation = locationService.isLocationEnabled()
267 | ```
268 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/simple/iosApp/iosApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93A953A29CC810C00F8E227 /* iosApp.swift */; };
11 | A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A953E29CC810D00F8E227 /* Assets.xcassets */; };
12 | A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A954129CC810D00F8E227 /* Preview Assets.xcassets */; };
13 | /* End PBXBuildFile section */
14 |
15 | /* Begin PBXFileReference section */
16 | 412BC6A02D04B0A6009CAEAE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
17 | A93A953729CC810C00F8E227 /* lib.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = lib.app; sourceTree = BUILT_PRODUCTS_DIR; };
18 | A93A953A29CC810C00F8E227 /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = ""; };
19 | A93A953E29CC810D00F8E227 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
20 | A93A954129CC810D00F8E227 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
21 | /* End PBXFileReference section */
22 |
23 | /* Begin PBXFrameworksBuildPhase section */
24 | A93A953429CC810C00F8E227 /* Frameworks */ = {
25 | isa = PBXFrameworksBuildPhase;
26 | buildActionMask = 2147483647;
27 | files = (
28 | );
29 | runOnlyForDeploymentPostprocessing = 0;
30 | };
31 | /* End PBXFrameworksBuildPhase section */
32 |
33 | /* Begin PBXGroup section */
34 | A93A952E29CC810C00F8E227 = {
35 | isa = PBXGroup;
36 | children = (
37 | A93A953929CC810C00F8E227 /* iosApp */,
38 | A93A953829CC810C00F8E227 /* Products */,
39 | C4127409AE3703430489E7BC /* Frameworks */,
40 | );
41 | sourceTree = "";
42 | };
43 | A93A953829CC810C00F8E227 /* Products */ = {
44 | isa = PBXGroup;
45 | children = (
46 | A93A953729CC810C00F8E227 /* lib.app */,
47 | );
48 | name = Products;
49 | sourceTree = "";
50 | };
51 | A93A953929CC810C00F8E227 /* iosApp */ = {
52 | isa = PBXGroup;
53 | children = (
54 | 412BC6A02D04B0A6009CAEAE /* Info.plist */,
55 | A93A953A29CC810C00F8E227 /* iosApp.swift */,
56 | A93A953E29CC810D00F8E227 /* Assets.xcassets */,
57 | A93A954029CC810D00F8E227 /* Preview Content */,
58 | );
59 | path = iosApp;
60 | sourceTree = "";
61 | };
62 | A93A954029CC810D00F8E227 /* Preview Content */ = {
63 | isa = PBXGroup;
64 | children = (
65 | A93A954129CC810D00F8E227 /* Preview Assets.xcassets */,
66 | );
67 | path = "Preview Content";
68 | sourceTree = "";
69 | };
70 | C4127409AE3703430489E7BC /* Frameworks */ = {
71 | isa = PBXGroup;
72 | children = (
73 | );
74 | name = Frameworks;
75 | sourceTree = "";
76 | };
77 | /* End PBXGroup section */
78 |
79 | /* Begin PBXNativeTarget section */
80 | A93A953629CC810C00F8E227 /* iosApp */ = {
81 | isa = PBXNativeTarget;
82 | buildConfigurationList = A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */;
83 | buildPhases = (
84 | A9D80A052AAB5CDE006C8738 /* ShellScript */,
85 | A93A953329CC810C00F8E227 /* Sources */,
86 | A93A953429CC810C00F8E227 /* Frameworks */,
87 | A93A953529CC810C00F8E227 /* Resources */,
88 | );
89 | buildRules = (
90 | );
91 | dependencies = (
92 | );
93 | name = iosApp;
94 | productName = iosApp;
95 | productReference = A93A953729CC810C00F8E227 /* lib.app */;
96 | productType = "com.apple.product-type.application";
97 | };
98 | /* End PBXNativeTarget section */
99 |
100 | /* Begin PBXProject section */
101 | A93A952F29CC810C00F8E227 /* Project object */ = {
102 | isa = PBXProject;
103 | attributes = {
104 | LastSwiftUpdateCheck = 1420;
105 | LastUpgradeCheck = 1420;
106 | TargetAttributes = {
107 | A93A953629CC810C00F8E227 = {
108 | CreatedOnToolsVersion = 14.2;
109 | };
110 | };
111 | };
112 | buildConfigurationList = A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */;
113 | compatibilityVersion = "Xcode 14.0";
114 | developmentRegion = en;
115 | hasScannedForEncodings = 0;
116 | knownRegions = (
117 | en,
118 | Base,
119 | );
120 | mainGroup = A93A952E29CC810C00F8E227;
121 | productRefGroup = A93A953829CC810C00F8E227 /* Products */;
122 | projectDirPath = "";
123 | projectRoot = "";
124 | targets = (
125 | A93A953629CC810C00F8E227 /* iosApp */,
126 | );
127 | };
128 | /* End PBXProject section */
129 |
130 | /* Begin PBXResourcesBuildPhase section */
131 | A93A953529CC810C00F8E227 /* Resources */ = {
132 | isa = PBXResourcesBuildPhase;
133 | buildActionMask = 2147483647;
134 | files = (
135 | A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */,
136 | A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */,
137 | );
138 | runOnlyForDeploymentPostprocessing = 0;
139 | };
140 | /* End PBXResourcesBuildPhase section */
141 |
142 | /* Begin PBXShellScriptBuildPhase section */
143 | A9D80A052AAB5CDE006C8738 /* ShellScript */ = {
144 | isa = PBXShellScriptBuildPhase;
145 | buildActionMask = 2147483647;
146 | files = (
147 | );
148 | inputFileListPaths = (
149 | );
150 | inputPaths = (
151 | );
152 | outputFileListPaths = (
153 | );
154 | outputPaths = (
155 | );
156 | runOnlyForDeploymentPostprocessing = 0;
157 | shellPath = /bin/sh;
158 | shellScript = "cd \"$SRCROOT/..\"\ncd ../\n./gradlew :simple:composeApp:embedAndSignAppleFrameworkForXcode\n";
159 | };
160 | /* End PBXShellScriptBuildPhase section */
161 |
162 | /* Begin PBXSourcesBuildPhase section */
163 | A93A953329CC810C00F8E227 /* Sources */ = {
164 | isa = PBXSourcesBuildPhase;
165 | buildActionMask = 2147483647;
166 | files = (
167 | A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */,
168 | );
169 | runOnlyForDeploymentPostprocessing = 0;
170 | };
171 | /* End PBXSourcesBuildPhase section */
172 |
173 | /* Begin XCBuildConfiguration section */
174 | A93A954329CC810D00F8E227 /* Debug */ = {
175 | isa = XCBuildConfiguration;
176 | buildSettings = {
177 | ALWAYS_SEARCH_USER_PATHS = NO;
178 | CLANG_ANALYZER_NONNULL = YES;
179 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
180 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
181 | CLANG_ENABLE_MODULES = YES;
182 | CLANG_ENABLE_OBJC_ARC = YES;
183 | CLANG_ENABLE_OBJC_WEAK = YES;
184 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
185 | CLANG_WARN_BOOL_CONVERSION = YES;
186 | CLANG_WARN_COMMA = YES;
187 | CLANG_WARN_CONSTANT_CONVERSION = YES;
188 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
189 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
190 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
191 | CLANG_WARN_EMPTY_BODY = YES;
192 | CLANG_WARN_ENUM_CONVERSION = YES;
193 | CLANG_WARN_INFINITE_RECURSION = YES;
194 | CLANG_WARN_INT_CONVERSION = YES;
195 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
196 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
197 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
198 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
199 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
200 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
201 | CLANG_WARN_STRICT_PROTOTYPES = YES;
202 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
203 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
204 | CLANG_WARN_UNREACHABLE_CODE = YES;
205 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
206 | COPY_PHASE_STRIP = NO;
207 | DEBUG_INFORMATION_FORMAT = dwarf;
208 | ENABLE_STRICT_OBJC_MSGSEND = YES;
209 | ENABLE_TESTABILITY = YES;
210 | GCC_C_LANGUAGE_STANDARD = gnu11;
211 | GCC_DYNAMIC_NO_PIC = NO;
212 | GCC_NO_COMMON_BLOCKS = YES;
213 | GCC_OPTIMIZATION_LEVEL = 0;
214 | GCC_PREPROCESSOR_DEFINITIONS = (
215 | "DEBUG=1",
216 | "$(inherited)",
217 | );
218 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
219 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
220 | GCC_WARN_UNDECLARED_SELECTOR = YES;
221 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
222 | GCC_WARN_UNUSED_FUNCTION = YES;
223 | GCC_WARN_UNUSED_VARIABLE = YES;
224 | IPHONEOS_DEPLOYMENT_TARGET = 17.5;
225 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
226 | MTL_FAST_MATH = YES;
227 | ONLY_ACTIVE_ARCH = YES;
228 | SDKROOT = iphoneos;
229 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
230 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
231 | };
232 | name = Debug;
233 | };
234 | A93A954429CC810D00F8E227 /* Release */ = {
235 | isa = XCBuildConfiguration;
236 | buildSettings = {
237 | ALWAYS_SEARCH_USER_PATHS = NO;
238 | CLANG_ANALYZER_NONNULL = YES;
239 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
240 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
241 | CLANG_ENABLE_MODULES = YES;
242 | CLANG_ENABLE_OBJC_ARC = YES;
243 | CLANG_ENABLE_OBJC_WEAK = YES;
244 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
245 | CLANG_WARN_BOOL_CONVERSION = YES;
246 | CLANG_WARN_COMMA = YES;
247 | CLANG_WARN_CONSTANT_CONVERSION = YES;
248 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
249 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
250 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
251 | CLANG_WARN_EMPTY_BODY = YES;
252 | CLANG_WARN_ENUM_CONVERSION = YES;
253 | CLANG_WARN_INFINITE_RECURSION = YES;
254 | CLANG_WARN_INT_CONVERSION = YES;
255 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
256 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
257 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
258 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
259 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
260 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
261 | CLANG_WARN_STRICT_PROTOTYPES = YES;
262 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
263 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
264 | CLANG_WARN_UNREACHABLE_CODE = YES;
265 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
266 | COPY_PHASE_STRIP = NO;
267 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
268 | ENABLE_NS_ASSERTIONS = NO;
269 | ENABLE_STRICT_OBJC_MSGSEND = YES;
270 | GCC_C_LANGUAGE_STANDARD = gnu11;
271 | GCC_NO_COMMON_BLOCKS = YES;
272 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
273 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
274 | GCC_WARN_UNDECLARED_SELECTOR = YES;
275 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
276 | GCC_WARN_UNUSED_FUNCTION = YES;
277 | GCC_WARN_UNUSED_VARIABLE = YES;
278 | IPHONEOS_DEPLOYMENT_TARGET = 17.5;
279 | MTL_ENABLE_DEBUG_INFO = NO;
280 | MTL_FAST_MATH = YES;
281 | SDKROOT = iphoneos;
282 | SWIFT_COMPILATION_MODE = wholemodule;
283 | SWIFT_OPTIMIZATION_LEVEL = "-O";
284 | VALIDATE_PRODUCT = YES;
285 | };
286 | name = Release;
287 | };
288 | A93A954629CC810D00F8E227 /* Debug */ = {
289 | isa = XCBuildConfiguration;
290 | buildSettings = {
291 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
292 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
293 | CODE_SIGN_STYLE = Automatic;
294 | CURRENT_PROJECT_VERSION = 1;
295 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
296 | ENABLE_PREVIEWS = YES;
297 | GENERATE_INFOPLIST_FILE = YES;
298 | INFOPLIST_FILE = iosApp/Info.plist;
299 | INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "need your location for test";
300 | INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "need your location for test";
301 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
302 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
303 | LD_RUNPATH_SEARCH_PATHS = (
304 | "$(inherited)",
305 | "@executable_path/Frameworks",
306 | );
307 | MARKETING_VERSION = 1.0;
308 | PRODUCT_BUNDLE_IDENTIFIER = io.gitub.io.iosApp;
309 | PRODUCT_NAME = lib;
310 | SWIFT_EMIT_LOC_STRINGS = YES;
311 | SWIFT_VERSION = 5.0;
312 | TARGETED_DEVICE_FAMILY = "1,2";
313 | };
314 | name = Debug;
315 | };
316 | A93A954729CC810D00F8E227 /* Release */ = {
317 | isa = XCBuildConfiguration;
318 | buildSettings = {
319 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
320 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
321 | CODE_SIGN_STYLE = Automatic;
322 | CURRENT_PROJECT_VERSION = 1;
323 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
324 | ENABLE_PREVIEWS = YES;
325 | GENERATE_INFOPLIST_FILE = YES;
326 | INFOPLIST_FILE = iosApp/Info.plist;
327 | INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "need your location for test";
328 | INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "need your location for test";
329 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
330 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
331 | LD_RUNPATH_SEARCH_PATHS = (
332 | "$(inherited)",
333 | "@executable_path/Frameworks",
334 | );
335 | MARKETING_VERSION = 1.0;
336 | PRODUCT_BUNDLE_IDENTIFIER = io.gitub.io.iosApp;
337 | PRODUCT_NAME = lib;
338 | SWIFT_EMIT_LOC_STRINGS = YES;
339 | SWIFT_VERSION = 5.0;
340 | TARGETED_DEVICE_FAMILY = "1,2";
341 | };
342 | name = Release;
343 | };
344 | /* End XCBuildConfiguration section */
345 |
346 | /* Begin XCConfigurationList section */
347 | A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */ = {
348 | isa = XCConfigurationList;
349 | buildConfigurations = (
350 | A93A954329CC810D00F8E227 /* Debug */,
351 | A93A954429CC810D00F8E227 /* Release */,
352 | );
353 | defaultConfigurationIsVisible = 0;
354 | defaultConfigurationName = Release;
355 | };
356 | A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
357 | isa = XCConfigurationList;
358 | buildConfigurations = (
359 | A93A954629CC810D00F8E227 /* Debug */,
360 | A93A954729CC810D00F8E227 /* Release */,
361 | );
362 | defaultConfigurationIsVisible = 0;
363 | defaultConfigurationName = Release;
364 | };
365 | /* End XCConfigurationList section */
366 | };
367 | rootObject = A93A952F29CC810C00F8E227 /* Project object */;
368 | }
369 |
--------------------------------------------------------------------------------