-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New integration: AndroidX Lifecycle #760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
3cbe424
New integration: AndroidX Lifecycle
LouisCAD 5b59a93
Add public api binary compatibility reference
LouisCAD 5df9041
Replace Job() by SupervisorJob()
LouisCAD 82af7d6
Remove implicit cache access in createScope(…)
LouisCAD 39c5df3
Move AndroidX lifecycle coroutineScope extensions
LouisCAD 5ae0603
Add cached into the jobs and scopes cache maps
LouisCAD 428b78c
Replace ConcurrentHashMap by mutableMapOf
LouisCAD 6d4261a
Update public api reference (binary compatibility)
LouisCAD a4a6621
Add Lifecycle observer only on next event loop
LouisCAD 25e26ff
Fix possible concurrency issues
LouisCAD File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
...ry-compatibility-validator/reference-public-api/kotlinx-coroutines-androidx-lifecycle.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
public final class kotlinx/coroutines/androidx/lifecycle/LifecycleDefaultScopesKt { | ||
public static final fun getCoroutineScope (Landroidx/lifecycle/Lifecycle;)Lkotlinx/coroutines/CoroutineScope; | ||
public static final fun getCoroutineScope (Landroidx/lifecycle/LifecycleOwner;)Lkotlinx/coroutines/CoroutineScope; | ||
public static final fun getJob (Landroidx/lifecycle/Lifecycle;)Lkotlinx/coroutines/Job; | ||
} | ||
|
||
public final class kotlinx/coroutines/androidx/lifecycle/LifecycleKt { | ||
public static final fun createJob (Landroidx/lifecycle/Lifecycle;Landroidx/lifecycle/Lifecycle$State;)Lkotlinx/coroutines/Job; | ||
public static synthetic fun createJob$default (Landroidx/lifecycle/Lifecycle;Landroidx/lifecycle/Lifecycle$State;ILjava/lang/Object;)Lkotlinx/coroutines/Job; | ||
public static final fun createScope (Landroidx/lifecycle/Lifecycle;Landroidx/lifecycle/Lifecycle$State;)Lkotlinx/coroutines/CoroutineScope; | ||
} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
59 changes: 59 additions & 0 deletions
59
integration/kotlinx-coroutines-androidx-lifecycle/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# Module kotlinx-coroutines-androidx-lifecycle | ||
|
||
Integration with AndroidX [Lifecycle]( | ||
https://developer.android.com/reference/kotlin/androidx/lifecycle/Lifecycle | ||
) and [LifecycleOwner]( | ||
https://developer.android.com/reference/kotlin/androidx/lifecycle/LifecycleOwner | ||
). | ||
|
||
Extension properties: | ||
|
||
| **Name** | **Description** | ||
| -------- | --------------- | ||
| [Lifecycle.coroutineScope][lifecycleScope] | A scope that dispatches on Android Main thread and is active until the Lifecycle is destroyed. | ||
| [LifecycleOwner.coroutineScope][lifecycleOwnerScope] | A scope that dispatches on Android Main thread and is active until the LifecycleOwner is destroyed. | ||
| [Lifecycle.job][lifecycleJob] | A job that is cancelled when the Lifecycle is destroyed. | ||
|
||
Extension functions: | ||
|
||
| **Name** | **Description** | ||
| -------- | --------------- | ||
| [Lifecycle.createJob][lifecycleCreateJob] | A job that is active while the state is at least the passed one. | ||
| [Lifecycle.createScope][lifecycleCreateScope] | A scope that dispatches on Android Main thread and is active while the state is at least the passed one. | ||
|
||
## Example | ||
|
||
```kotlin | ||
class MainActivity : AppCompatActivity() { | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
lifecycle.coroutineScope.launch { | ||
someSuspendFunction() | ||
someOtherSuspendFunction() | ||
someCancellableSuspendFunction() | ||
} | ||
} | ||
|
||
override fun onStart() { | ||
super.onStart() | ||
val startedScope = lifecycle.createScope(activeWhile = Lifecycle.State.STARTED) | ||
startedScope.launch { | ||
aCancellableSuspendFunction() | ||
yetAnotherCancellableSuspendFunction() | ||
} | ||
startedScope.aMethodThatWillLaunchSomeCoroutines() | ||
} | ||
} | ||
``` | ||
|
||
# Package kotlinx.coroutines.androidx.lifecycle | ||
|
||
Integration with AndroidX [Lifecycle](https://developer.android.com/reference/kotlin/androidx/lifecycle/Lifecycle) | ||
and [LifecycleOwner](https://developer.android.com/reference/kotlin/androidx/lifecycle/LifecycleOwner). | ||
|
||
<!--- MODULE kotlinx-coroutines-core --> | ||
<!--- INDEX kotlinx.coroutines --> | ||
<!--- MODULE kotlinx-coroutines-androidx-lifecycle --> | ||
<!--- INDEX kotlinx.coroutines.androidx.lifecycle --> | ||
<!--- END --> |
92 changes: 92 additions & 0 deletions
92
integration/kotlinx-coroutines-androidx-lifecycle/build.gradle
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import java.nio.file.Files | ||
import java.nio.file.NoSuchFileException | ||
import java.util.zip.ZipEntry | ||
import java.util.zip.ZipFile | ||
|
||
/* | ||
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
ext.androidx_lifecycle_version = '2.0.0' | ||
|
||
repositories { | ||
google() | ||
} | ||
|
||
def attr = Attribute.of("artifactType", String.class) | ||
configurations { | ||
aar { | ||
attributes { attribute(attr, ArtifactTypeDefinition.JAR_TYPE) } | ||
sourceSets.main.compileClasspath += it | ||
sourceSets.test.compileClasspath += it | ||
sourceSets.test.runtimeClasspath += it | ||
} | ||
aarTest { | ||
attributes { attribute(attr, ArtifactTypeDefinition.JAR_TYPE) } | ||
sourceSets.test.compileClasspath += it | ||
sourceSets.test.runtimeClasspath += it | ||
} | ||
} | ||
|
||
sourceSets { | ||
test { | ||
resources.srcDirs = ["test/resources"] | ||
} | ||
} | ||
|
||
dependencies { | ||
registerTransform { | ||
from.attribute(attr, "aar") | ||
to.attribute(attr, "jar") | ||
artifactTransform(ExtractJars.class) | ||
} | ||
aar("androidx.lifecycle:lifecycle-common:$androidx_lifecycle_version") | ||
aarTest("androidx.lifecycle:lifecycle-runtime:$androidx_lifecycle_version") | ||
api project(":kotlinx-coroutines-android") | ||
} | ||
|
||
tasks.withType(dokka.getClass()) { | ||
externalDocumentationLink { | ||
url = new URL("https://developer.android.com/reference/androidx/") | ||
} | ||
} | ||
|
||
class ExtractJars extends ArtifactTransform { | ||
@Override | ||
List<File> transform(File input) { | ||
unzip(input) | ||
|
||
List<File> jars = new ArrayList<>() | ||
outputDirectory.traverse(nameFilter: ~/.*\.jar/) { jars += it } | ||
|
||
return jars | ||
} | ||
|
||
private void unzip(File zipFile) { | ||
ZipFile zip | ||
try { | ||
zip = new ZipFile(zipFile) | ||
for (entry in zip.entries()) { | ||
unzipEntryTo(zip, entry) | ||
} | ||
} finally { | ||
if (zip != null) zip.close() | ||
} | ||
} | ||
|
||
private void unzipEntryTo(ZipFile zip, ZipEntry entry) { | ||
File output = new File(outputDirectory, entry.name) | ||
if (entry.isDirectory()) { | ||
output.mkdirs() | ||
} else { | ||
InputStream stream | ||
try { | ||
stream = zip.getInputStream(entry) | ||
Files.copy(stream, output.toPath()) | ||
} catch (NoSuchFileException ignored) { | ||
} finally { | ||
if (stream != null) stream.close() | ||
} | ||
} | ||
} | ||
} |
49 changes: 49 additions & 0 deletions
49
integration/kotlinx-coroutines-androidx-lifecycle/src/Lifecycle.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package kotlinx.coroutines.androidx.lifecycle | ||
|
||
import androidx.lifecycle.GenericLifecycleObserver | ||
import androidx.lifecycle.Lifecycle | ||
import androidx.lifecycle.Lifecycle.State.INITIALIZED | ||
import androidx.lifecycle.LifecycleOwner | ||
import kotlinx.coroutines.* | ||
|
||
/** | ||
* Returns a [CoroutineScope] that uses [Dispatchers.Main] by default, and that will be cancelled as | ||
* soon as this [Lifecycle] [currentState][Lifecycle.getCurrentState] is no longer | ||
* [at least][Lifecycle.State.isAtLeast] the passed [activeWhile] state. | ||
* | ||
* **Beware**: if the current state is lower than the passed [activeWhile] state, you'll get an | ||
* already cancelled scope. | ||
*/ | ||
fun Lifecycle.createScope(activeWhile: Lifecycle.State): CoroutineScope { | ||
return CoroutineScope(createJob(activeWhile) + Dispatchers.Main) | ||
} | ||
|
||
/** | ||
* Creates a [SupervisorJob] that will be cancelled as soon as this [Lifecycle] | ||
* [currentState][Lifecycle.getCurrentState] is no longer [at least][Lifecycle.State.isAtLeast] the | ||
* passed [activeWhile] state. | ||
* | ||
* **Beware**: if the current state is lower than the passed [activeWhile] state, you'll get an | ||
* already cancelled job. | ||
*/ | ||
fun Lifecycle.createJob(activeWhile: Lifecycle.State = INITIALIZED): Job { | ||
require(activeWhile != Lifecycle.State.DESTROYED) { | ||
LouisCAD marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"DESTROYED is a terminal state that is forbidden for createJob(…), to avoid leaks." | ||
} | ||
return SupervisorJob().also { job -> | ||
when (currentState) { | ||
Lifecycle.State.DESTROYED -> job.cancel() // Fast path if already destroyed | ||
else -> GlobalScope.launch(Dispatchers.Main) { // State is usually synced on next loop, | ||
// this allows to use STARTED from onStart in Activities for example. | ||
addObserver(object : GenericLifecycleObserver { | ||
override fun onStateChanged(source: LifecycleOwner?, event: Lifecycle.Event) { | ||
if (!currentState.isAtLeast(activeWhile)) { | ||
removeObserver(this) | ||
job.cancel() | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
integration/kotlinx-coroutines-androidx-lifecycle/src/LifecycleDefaultScopes.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package kotlinx.coroutines.androidx.lifecycle | ||
|
||
import androidx.lifecycle.Lifecycle | ||
import androidx.lifecycle.LifecycleOwner | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.Job | ||
import kotlinx.coroutines.SupervisorJob | ||
import java.util.concurrent.ConcurrentHashMap | ||
|
||
/** | ||
* Returns a [CoroutineScope] that uses [Dispatchers.Main] by default, and that is cancelled when | ||
* the [Lifecycle] reaches [Lifecycle.State.DESTROYED] state. | ||
* | ||
* Note that this value is cached until the Lifecycle reaches the destroyed state. | ||
*/ | ||
val Lifecycle.coroutineScope: CoroutineScope | ||
LouisCAD marked this conversation as resolved.
Show resolved
Hide resolved
|
||
get() = cachedLifecycleCoroutineScopes[this] ?: job.let { job -> | ||
val newScope = CoroutineScope(job + Dispatchers.Main) | ||
if (job.isActive) { | ||
cachedLifecycleCoroutineScopes[this] = newScope | ||
job.invokeOnCompletion { _ -> cachedLifecycleCoroutineScopes -= this } | ||
} | ||
newScope | ||
} | ||
|
||
/** | ||
* Calls [Lifecycle.coroutineScope] for the [Lifecycle] of this [LifecycleOwner]. | ||
* | ||
* This is an inline property, just there for convenient usage from any [LifecycleOwner], | ||
* like FragmentActivity, AppCompatActivity, Fragment and LifecycleService. | ||
*/ | ||
inline val LifecycleOwner.coroutineScope get() = lifecycle.coroutineScope | ||
LouisCAD marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Returns a [SupervisorJob] that will be cancelled as soon as the [Lifecycle] reaches | ||
* [Lifecycle.State.DESTROYED] state. | ||
* | ||
* Note that this value is cached until the Lifecycle reaches the destroyed state. | ||
* | ||
* You can use this job for custom [CoroutineScope]s, or as a parent [Job]. | ||
*/ | ||
val Lifecycle.job: Job | ||
LouisCAD marked this conversation as resolved.
Show resolved
Hide resolved
|
||
get() = cachedLifecycleJobs[this] ?: createJob().also { | ||
if (it.isActive) { | ||
cachedLifecycleJobs[this] = it | ||
it.invokeOnCompletion { _ -> cachedLifecycleJobs -= this } | ||
} | ||
} | ||
|
||
private val cachedLifecycleJobs = ConcurrentHashMap<Lifecycle, Job>() | ||
private val cachedLifecycleCoroutineScopes = ConcurrentHashMap<Lifecycle, CoroutineScope>() | ||
LouisCAD marked this conversation as resolved.
Show resolved
Hide resolved
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This API surprises me in two ways:
What do you think a lifecycle aware dispatcher to solve both of those cases as well as handle the onCreate situation discussed previously?
I'm not sure this exactly the right API, but it seems likely to work out for these cases. Basically, it'd operate as an event loop that pauses when it's below the expected state. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to launch a coroutine at instantiation time with a lifecycle aware scope, you can still use
createScope(activeWhile = Lifecycle.State.INITIALIZED)
.Do you think I should change the default for the
coroutineScope
andjob
properties to be active while initialized instead of active while created?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About your second point: the typical use case there is to launch a coroutine inside the
onStart
oronResume
method that is automatically cancelled when the lifecycle ispaused
orresumed
, so a new coroutine is launched each time theonStart
oronResume
method is called.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a
Job
is cancelled, there's no going back, but you can always start a coroutine later when the conditions are met again, as mentioned in my comment just above.About pausable dispatcher, I thought about this a while ago, but this doesn't fit well with coroutines design.
You could think that you could pause dispatching, but this could cause more problems, as you may be relying on a dispatching loop to release resources, creating temporary leaks in suspended coroutines that may resume only much later, if ever.
Suspending work based on the lifecycle has to be opt-in at call site.
This is something that is possible with this
awaitState(…)
extension function forLifecycle
I wrote, and that can work well for the use cases where you want to pause execution until the lifecycle is at least in some state. You can call this before starting expensive work or in a loop tied to the UI component (Activity, Fragment…).I personally already have use cases for this method, like showing
DialogFragment
s safely.Do you think I should include this method in this PR?