Skip to content

Commit ae67f95

Browse files
authored
Release 8.0.0
*Breaking changes* - Location flow, permission flow and settings flow are now *SharedFlow*. Using *StateFlow* is conceptually wrong because it does not necessarily mean the *current* state. If you used `.value` on the previous flows, you can now use `replayCache.last()`. - `LocationFetcher.location` is now a `SharedFlow<Either<Nel<Error>, Location>>`. It now reports the errors in the left side of the `Either` value in case it failed to obtain a location. - `LocationFetcher.permissionStatus` and `LocationFetcher.settingsStatus` are now `SharedFlow<Boolean>`. The old enums `PermissionStatus` and `SettingsStatus` are now deprecated. - Removed `LocationFetcher.requestLocationPermissionOnLifecycle` and `LocationFetcher.requestEnableLocationSettingsOnLifecycle` configs from `LocationFetcher.Config`. Instead of requesting permissions and setting enablement on their own, it's now requested automatically once the location flows is subscribed to. - Removed the possibility to ask for location indefinitely. We now use (and require) a rationale for asking for location. If user denies the permission once, the rationale is shown and we ask the permission one more time. If the user denies it, we respect the user decision and don't ask again. This is in accordance with Google's best practices and policies on location fetching. The rationale is a `String` passed to the `LocationFetcher` builders. *Other changes* - Added `LocationFetcher.shouldShowRationale(): Boolean`. Should return true after user denied location permission once. It's used internally to decide whether to show a rationale to the user before asking for permission again, but it's also exposed as an API.
1 parent 430911c commit ae67f95

24 files changed

+605
-494
lines changed

.idea/kotlinScripting.xml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 79 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# LocationFetcher
22

3-
[![](https://jitpack.io/v/psteiger/LocationFetcher.svg)](https://jitpack.io/#psteiger/LocationFetcher)
3+
[![Download](https://img.shields.io/maven-central/v/app.freel/locationfetcher)](https://search.maven.org/artifact/app.freel/locationfetcher)
44

55
Simple location fetcher for Android Apps built with Kotlin and Coroutines.
66

@@ -10,25 +10,34 @@ Building location-aware Android apps can be a bit tricky. This library makes it
1010
class MyActivity : ComponentActivity() {
1111

1212
private val locationFetcher by lazy {
13-
locationFetcher() // extension on ComponentActivity
13+
locationFetcher(getString(R.string.location_rationale)) // extension on ComponentActivity
1414
}
1515

16-
init {
16+
override fun onCreate(savedInstanceState: Bundle?) {
17+
super.onCreate(savedInstanceState)
1718
lifecycleScope.launch {
18-
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
19-
with (locationFetcher) {
20-
location
21-
.onEach { /* Location received */ }
22-
.launchIn(lifecycleScope)
23-
24-
settingsStatus
25-
.onEach { /* Location got enabled or disabled in device settings */ }
26-
.launchIn(lifecycleScope)
27-
28-
permissionStatus
29-
.onEach { /* App allowed or disallowed to access the device's location. */ }
30-
.launchIn(lifecycleScope)
31-
}
19+
repeatOnLifecycle(Lifecycle.State.STARTED) {
20+
locationFetcher.location
21+
.onEach { errorsOrLocation ->
22+
errorsOrLocation.map { location ->
23+
// Got location
24+
}.handleError { errors ->
25+
// Optional. Handle errors. This is optional because errors
26+
// (no location permission, or setting disabled), will try to be
27+
// automatically handled by lib.
28+
}
29+
}
30+
.launchIn(this)
31+
32+
// Optional, redundant as erros are already reported to 'location' flow.
33+
locationFetcher.settingsStatus
34+
.onEach { /* Location got enabled or disabled in device settings */ }
35+
.launchIn(this)
36+
37+
// Optional, redundant as erros are already reported to 'location' flow.
38+
locationFetcher.permissionStatus
39+
.onEach { /* App allowed or disallowed to access the device's location. */ }
40+
.launchIn(this)
3241
}
3342
}
3443
}
@@ -37,49 +46,68 @@ class MyActivity : ComponentActivity() {
3746

3847
This library provides a simple location component, `LocationFetcher`, requiring only either an `ComponentActivity` instance or a `Context` instance, to make your Android app location-aware.
3948

40-
The service uses GPS and network as location providers by default and thus the app needs to declare use of the `ACCESS_FINE_LOCATION` and `ACCESS_COARSE_LOCATION` permissions on its `AndroidManifest.xml`.
49+
If the device's location services are disabled, or if your app is not allowed location permissions by the user, this library will automatically ask the user to enable location services in settings or to allow the necessary permissions as soon as you start collecting the `LocationFetcher.location` flow.
50+
51+
The service uses GPS and network as location providers by default and thus the app needs to declare use of the `ACCESS_FINE_LOCATION` and `ACCESS_COARSE_LOCATION` permissions on its `AndroidManifest.xml`. Those permissions are already declared in this library, so manifest merging takes care of it.
4152

4253
You can personalize your `LocationRequest` to suit your needs.
4354

44-
If the device's location services are disabled, or if your app is not allowed location permissions by the user, this library can (optionally) automatically ask the user to enable location services in settings or to allow the necessary permissions.
55+
## Installation with Gradle
4556

46-
## Installation
57+
### Setup Maven Central on project-level build.gradle
4758

48-
### Using Gradle
59+
This library is hosted in Maven Central, so you must set it up for your project before adding the module-level dependency.
4960

50-
On project-level `build.gradle`, add [Jitpack](https://jitpack.io/) repository:
61+
#### New way
5162

52-
```groovy
63+
The new way to install dependencies repositories is through the `dependencyResolutionManagement` DSL in `settings.gradle(.kts)`.
64+
65+
Kotlin or Groovy:
66+
```kotlin
67+
dependencyResolutionManagement {
68+
repositories {
69+
mavenCentral()
70+
}
71+
}
72+
```
73+
74+
OR
75+
76+
#### Old way
77+
78+
On project-level `build.gradle`:
79+
80+
Kotlin or Groovy:
81+
```kotlin
5382
allprojects {
5483
repositories {
55-
maven { url 'https://jitpack.io' }
84+
mavenCentral()
5685
}
5786
}
5887
```
5988

89+
### Add dependency
90+
6091
On app-level `build.gradle`, add dependency:
6192

93+
Groovy:
6294
```groovy
6395
dependencies {
64-
implementation 'com.github.psteiger:locationfetcher:7.0.0'
96+
implementation 'app.freel:locationfetcher:8.0.0'
6597
}
6698
```
6799

68-
### On Manifest
69-
70-
On root level, allow permission:
71-
72-
```xml
73-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
74-
package="com.yourapp">
75-
76-
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
77-
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
78-
</manifest>
100+
Kotlin:
101+
```kotlin
102+
dependencies {
103+
implementation("app.freel:locationfetcher:8.0.0")
104+
}
79105
```
80106

81107
## Usage
82108

109+
### Instantiating
110+
83111
On any `ComponentActivity` or `Context` class, you can instantiate a `LocationFetcher` by calling the extension functions on `ComponentActivity` or `Context`:
84112

85113
```kotlin
@@ -94,12 +122,20 @@ LocationFetcher(this)
94122

95123
If `LocationFetcher` is created with a `ComponentActivity`, it will be able to show dialogs to request the user to enable permission in Android settings and to allow the app to obtain the device's location. If `LocationFetcher` is created with a non-`ComponentActivity` `Context`, it won't be able to show dialogs.
96124

97-
Once instantiated, the component gives you three `Flow`s to collect: one for new locations, one for settings status, and one for location permissions status:
125+
#### Permission rationale
126+
127+
In accordance with Google's best practices and policies, if user denies location permission, we must tell the user the rationale for the need of the user location, then we can ask permission a last time. If denied again, we must respect the user's decision.
128+
129+
The rationale must be passed to `LocationFetcher` builders. It will be shown to the user as an `AlertDialog`.
130+
131+
### Collecting location
132+
133+
Once instantiated, the component gives you three `Flow`s to collect: one for new locations, one for settings status, and one for location permissions status. Usually, you only need to collect the location flow, as errors also flow through it already.
98134

99135
```kotlin
100-
LocationFetcher.location: StateFlow<Location?>
101-
LocationFetcher.permissionStatus: StateFlow<PermissionStatus>
102-
LocationFetcher.settingsStatus: StateFlow<SettingsStatus>
136+
LocationFetcher.location: SharedFlow<Either<Nel<Error>, Location>> // Nel stands for non-empty list.
137+
LocationFetcher.permissionStatus: SharedFlow<Boolean>
138+
LocationFetcher.settingsStatus: SharedFlow<Boolean>
103139
```
104140

105141
To manually request location permissions or location settings enablement, you can call the following APIs:
@@ -118,7 +154,7 @@ Results will be delivered on the aforementioned flows.
118154
(Note: for GPS and Network providers, only `interval` and `smallestDisplacement` are used. If you want to use all options, limit providers to `LocationRequest.Provider.Fused`)
119155

120156
```kotlin
121-
locationFetcher {
157+
locationFetcher("We need your permission to use your location for showing nearby items") {
122158
fastestInterval = 5000
123159
interval = 15000
124160
maxWaitTime = 100000
@@ -131,8 +167,6 @@ locationFetcher {
131167
LocationRequest.Provider.Fused
132168
)
133169
numUpdates = Int.MAX_VALUE
134-
requestLocationPermissionOnLifecycle: Lifecycle.State? // no effect if built with Context
135-
requestEnableLocationSettingsOnLifecycle: Lifecycle.State? // no effect if built with Context
136170
debug = false
137171
}
138172
```
@@ -141,6 +175,7 @@ Alternatively, you might prefer to create a standalone configuration instance. I
141175

142176
```kotlin
143177
val config = LocationFetcher.config(
178+
rationale = "We need your permission to use your location for showing nearby items",
144179
fastestInterval = 5000,
145180
interval = 15000,
146181
maxWaitTime = 100000,
@@ -153,9 +188,7 @@ val config = LocationFetcher.config(
153188
LocationRequest.Provider.Fused
154189
),
155190
numUpdates = Int.MAX_VALUE,
156-
requestLocationPermissions = true, // no effect if built with Context
157-
requestEnableLocationSettings = true, // no effect if built with Context
158191
debug = true
159192
)
160-
locationFetcher(config)
193+
val locationFetcher = locationFetcher(config)
161194
```

build.gradle.kts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
1-
buildscript {
2-
repositories {
3-
google()
4-
mavenCentral()
5-
}
6-
dependencies {
7-
val kotlinVersion = "1.5.31"
8-
classpath("com.android.tools.build:gradle:7.1.0-alpha13")
9-
classpath(kotlin("gradle-plugin", version = kotlinVersion))
10-
}
1+
plugins {
2+
id("com.android.library") version "7.1.0-beta02" apply false
3+
kotlin("android") version "1.6.0-RC2" apply false
4+
id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
115
}
126

13-
tasks.register("clean", Delete::class) {
7+
tasks.register<Delete>("clean") {
148
delete(rootProject.buildDir)
159
}
10+
11+
apply(from = "$rootDir/scripts/publish-root.gradle.kts")
12+
13+
// Set up Sonatype repository
14+
nexusPublishing {
15+
repositories {
16+
sonatype {
17+
val ossrhUsername: String by extra
18+
val ossrhPassword: String by extra
19+
val sonatypeStagingProfileId: String by extra
20+
stagingProfileId.set(sonatypeStagingProfileId)
21+
username.set(ossrhUsername)
22+
password.set(ossrhPassword)
23+
// Add these lines if using new Sonatype infra
24+
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
25+
snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
26+
}
27+
}
28+
}

locationfetcher/build.gradle.kts

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ plugins {
22
id("com.android.library")
33
kotlin("android")
44
`maven-publish`
5+
signing
56
}
67

8+
apply(from = "$rootDir/scripts/publish-root.gradle.kts")
9+
10+
group = "app.freel"
11+
version = "8.0.0"
12+
713
android {
814
compileSdk = 31
915
defaultConfig {
@@ -15,46 +21,97 @@ android {
1521
targetCompatibility = JavaVersion.VERSION_1_8
1622
}
1723
kotlinOptions {
24+
freeCompilerArgs += listOf(
25+
"-Xexplicit-api=strict",
26+
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
27+
)
1828
jvmTarget = "1.8"
1929
languageVersion = "1.5"
2030
}
2131
}
2232

23-
dependencies {
24-
coroutines()
25-
jetpack()
26-
implementation("com.google.android.gms:play-services-location:18.0.0")
27-
implementation("javax.inject:javax.inject:1")
33+
val sourcesJar = task<Jar>("androidSourcesJar") {
34+
archiveClassifier.set("sources")
35+
from(android.sourceSets["main"].java.srcDirs)
2836
}
2937

3038
afterEvaluate {
3139
publishing {
3240
publications {
3341
// Creates a Maven publication called "release".
34-
register("release", MavenPublication::class) {
42+
register<MavenPublication>("release") {
3543
from(components["release"])
44+
groupId = "app.freel"
45+
version = "8.0.0"
3646
artifactId = project.name
47+
artifact(sourcesJar).apply {
48+
classifier = "sources"
49+
}
50+
pom {
51+
name.set(project.name)
52+
description.set("Easy Location fetching for Android apps.")
53+
url.set("https://github.com/psteiger/LocationFetcher")
54+
licenses {
55+
license {
56+
name.set("MIT License")
57+
url.set("https://github.com/psteiger/LocationFetcher/blob/master/LICENSE")
58+
}
59+
}
60+
developers {
61+
developer {
62+
id.set("psteiger")
63+
name.set("Patrick Steiger")
64+
email.set("[email protected]")
65+
}
66+
}
67+
scm {
68+
connection.set("scm:git:github.com/psteiger/LocationFetcher/LocationFetcher.git")
69+
developerConnection.set("scm:git:ssh://github.com/psteiger/LocationFetcher/LocationFetcher.git")
70+
url.set("https://github.com/LocationFetcher/psteiger/tree/master")
71+
}
72+
}
3773
}
3874
}
3975
}
4076
}
4177

78+
signing {
79+
val signingKeyId: String by extra
80+
val signingPassword: String by extra
81+
val signingKey: String by extra
82+
useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
83+
sign(publishing.publications)
84+
}
85+
86+
dependencies {
87+
coroutines()
88+
jetpack()
89+
arrow()
90+
implementation("com.google.android.gms:play-services-location:18.0.0")
91+
implementation("javax.inject:javax.inject:1")
92+
}
93+
94+
fun DependencyHandlerScope.arrow() {
95+
val version = "1.0.1"
96+
api("io.arrow-kt:arrow-core:$version")
97+
}
98+
4299
fun DependencyHandlerScope.coroutines() {
43100
val version = "1.5.2"
44-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$version")
101+
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$version")
45102
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$version")
46103
}
47104

48105
fun DependencyHandlerScope.jetpack() {
49-
implementation("androidx.activity:activity-ktx:1.4.0-beta01")
50-
implementation("androidx.fragment:fragment-ktx:1.4.0-alpha10")
51-
implementation("androidx.appcompat:appcompat:1.4.0-beta01")
52-
implementation("androidx.core:core-ktx:1.6.0")
106+
implementation("androidx.activity:activity-ktx:1.4.0")
107+
implementation("androidx.fragment:fragment-ktx:1.4.0-rc01")
108+
implementation("androidx.appcompat:appcompat:1.4.0-rc01")
109+
implementation("androidx.core:core-ktx:1.7.0")
53110
androidxLifecycle()
54111
}
55112

56113
fun DependencyHandlerScope.androidxLifecycle() {
57-
val lifecycleVersion = "2.4.0-rc01"
114+
val lifecycleVersion = "2.4.0"
58115
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
59116
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
60117
}

0 commit comments

Comments
 (0)