Skip to content

Commit

Permalink
Release 8.0.0
Browse files Browse the repository at this point in the history
*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.
  • Loading branch information
psteiger authored Nov 7, 2021
1 parent 430911c commit ae67f95
Show file tree
Hide file tree
Showing 24 changed files with 605 additions and 494 deletions.
9 changes: 9 additions & 0 deletions .idea/kotlinScripting.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

125 changes: 79 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# LocationFetcher

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

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

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

private val locationFetcher by lazy {
locationFetcher() // extension on ComponentActivity
locationFetcher(getString(R.string.location_rationale)) // extension on ComponentActivity
}

init {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
with (locationFetcher) {
location
.onEach { /* Location received */ }
.launchIn(lifecycleScope)

settingsStatus
.onEach { /* Location got enabled or disabled in device settings */ }
.launchIn(lifecycleScope)

permissionStatus
.onEach { /* App allowed or disallowed to access the device's location. */ }
.launchIn(lifecycleScope)
}
repeatOnLifecycle(Lifecycle.State.STARTED) {
locationFetcher.location
.onEach { errorsOrLocation ->
errorsOrLocation.map { location ->
// Got location
}.handleError { errors ->
// Optional. Handle errors. This is optional because errors
// (no location permission, or setting disabled), will try to be
// automatically handled by lib.
}
}
.launchIn(this)

// Optional, redundant as erros are already reported to 'location' flow.
locationFetcher.settingsStatus
.onEach { /* Location got enabled or disabled in device settings */ }
.launchIn(this)

// Optional, redundant as erros are already reported to 'location' flow.
locationFetcher.permissionStatus
.onEach { /* App allowed or disallowed to access the device's location. */ }
.launchIn(this)
}
}
}
Expand All @@ -37,49 +46,68 @@ class MyActivity : ComponentActivity() {

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.

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`.
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.

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.

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

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.
## Installation with Gradle

## Installation
### Setup Maven Central on project-level build.gradle

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

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

```groovy
The new way to install dependencies repositories is through the `dependencyResolutionManagement` DSL in `settings.gradle(.kts)`.

Kotlin or Groovy:
```kotlin
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
```

OR

#### Old way

On project-level `build.gradle`:

Kotlin or Groovy:
```kotlin
allprojects {
repositories {
maven { url 'https://jitpack.io' }
mavenCentral()
}
}
```

### Add dependency

On app-level `build.gradle`, add dependency:

Groovy:
```groovy
dependencies {
implementation 'com.github.psteiger:locationfetcher:7.0.0'
implementation 'app.freel:locationfetcher:8.0.0'
}
```

### On Manifest

On root level, allow permission:

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yourapp">

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>
Kotlin:
```kotlin
dependencies {
implementation("app.freel:locationfetcher:8.0.0")
}
```

## Usage

### Instantiating

On any `ComponentActivity` or `Context` class, you can instantiate a `LocationFetcher` by calling the extension functions on `ComponentActivity` or `Context`:

```kotlin
Expand All @@ -94,12 +122,20 @@ LocationFetcher(this)

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.

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:
#### Permission rationale

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.

The rationale must be passed to `LocationFetcher` builders. It will be shown to the user as an `AlertDialog`.

### Collecting location

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.

```kotlin
LocationFetcher.location: StateFlow<Location?>
LocationFetcher.permissionStatus: StateFlow<PermissionStatus>
LocationFetcher.settingsStatus: StateFlow<SettingsStatus>
LocationFetcher.location: SharedFlow<Either<Nel<Error>, Location>> // Nel stands for non-empty list.
LocationFetcher.permissionStatus: SharedFlow<Boolean>
LocationFetcher.settingsStatus: SharedFlow<Boolean>
```

To manually request location permissions or location settings enablement, you can call the following APIs:
Expand All @@ -118,7 +154,7 @@ Results will be delivered on the aforementioned flows.
(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`)

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

```kotlin
val config = LocationFetcher.config(
rationale = "We need your permission to use your location for showing nearby items",
fastestInterval = 5000,
interval = 15000,
maxWaitTime = 100000,
Expand All @@ -153,9 +188,7 @@ val config = LocationFetcher.config(
LocationRequest.Provider.Fused
),
numUpdates = Int.MAX_VALUE,
requestLocationPermissions = true, // no effect if built with Context
requestEnableLocationSettings = true, // no effect if built with Context
debug = true
)
locationFetcher(config)
val locationFetcher = locationFetcher(config)
```
35 changes: 24 additions & 11 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
val kotlinVersion = "1.5.31"
classpath("com.android.tools.build:gradle:7.1.0-alpha13")
classpath(kotlin("gradle-plugin", version = kotlinVersion))
}
plugins {
id("com.android.library") version "7.1.0-beta02" apply false
kotlin("android") version "1.6.0-RC2" apply false
id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
}

tasks.register("clean", Delete::class) {
tasks.register<Delete>("clean") {
delete(rootProject.buildDir)
}

apply(from = "$rootDir/scripts/publish-root.gradle.kts")

// Set up Sonatype repository
nexusPublishing {
repositories {
sonatype {
val ossrhUsername: String by extra
val ossrhPassword: String by extra
val sonatypeStagingProfileId: String by extra
stagingProfileId.set(sonatypeStagingProfileId)
username.set(ossrhUsername)
password.set(ossrhPassword)
// Add these lines if using new Sonatype infra
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
}
}
}
81 changes: 69 additions & 12 deletions locationfetcher/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ plugins {
id("com.android.library")
kotlin("android")
`maven-publish`
signing
}

apply(from = "$rootDir/scripts/publish-root.gradle.kts")

group = "app.freel"
version = "8.0.0"

android {
compileSdk = 31
defaultConfig {
Expand All @@ -15,46 +21,97 @@ android {
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
freeCompilerArgs += listOf(
"-Xexplicit-api=strict",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
)
jvmTarget = "1.8"
languageVersion = "1.5"
}
}

dependencies {
coroutines()
jetpack()
implementation("com.google.android.gms:play-services-location:18.0.0")
implementation("javax.inject:javax.inject:1")
val sourcesJar = task<Jar>("androidSourcesJar") {
archiveClassifier.set("sources")
from(android.sourceSets["main"].java.srcDirs)
}

afterEvaluate {
publishing {
publications {
// Creates a Maven publication called "release".
register("release", MavenPublication::class) {
register<MavenPublication>("release") {
from(components["release"])
groupId = "app.freel"
version = "8.0.0"
artifactId = project.name
artifact(sourcesJar).apply {
classifier = "sources"
}
pom {
name.set(project.name)
description.set("Easy Location fetching for Android apps.")
url.set("https://github.com/psteiger/LocationFetcher")
licenses {
license {
name.set("MIT License")
url.set("https://github.com/psteiger/LocationFetcher/blob/master/LICENSE")
}
}
developers {
developer {
id.set("psteiger")
name.set("Patrick Steiger")
email.set("[email protected]")
}
}
scm {
connection.set("scm:git:github.com/psteiger/LocationFetcher/LocationFetcher.git")
developerConnection.set("scm:git:ssh://github.com/psteiger/LocationFetcher/LocationFetcher.git")
url.set("https://github.com/LocationFetcher/psteiger/tree/master")
}
}
}
}
}
}

signing {
val signingKeyId: String by extra
val signingPassword: String by extra
val signingKey: String by extra
useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
sign(publishing.publications)
}

dependencies {
coroutines()
jetpack()
arrow()
implementation("com.google.android.gms:play-services-location:18.0.0")
implementation("javax.inject:javax.inject:1")
}

fun DependencyHandlerScope.arrow() {
val version = "1.0.1"
api("io.arrow-kt:arrow-core:$version")
}

fun DependencyHandlerScope.coroutines() {
val version = "1.5.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$version")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$version")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$version")
}

fun DependencyHandlerScope.jetpack() {
implementation("androidx.activity:activity-ktx:1.4.0-beta01")
implementation("androidx.fragment:fragment-ktx:1.4.0-alpha10")
implementation("androidx.appcompat:appcompat:1.4.0-beta01")
implementation("androidx.core:core-ktx:1.6.0")
implementation("androidx.activity:activity-ktx:1.4.0")
implementation("androidx.fragment:fragment-ktx:1.4.0-rc01")
implementation("androidx.appcompat:appcompat:1.4.0-rc01")
implementation("androidx.core:core-ktx:1.7.0")
androidxLifecycle()
}

fun DependencyHandlerScope.androidxLifecycle() {
val lifecycleVersion = "2.4.0-rc01"
val lifecycleVersion = "2.4.0"
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
}
Loading

0 comments on commit ae67f95

Please sign in to comment.