From d5d3ed281defba92307b2c7c1c53c338b3889d28 Mon Sep 17 00:00:00 2001 From: Tom Seifert Date: Fri, 25 Jan 2019 11:21:27 +0100 Subject: [PATCH 01/12] Add new Kotlin Coroutines module --- build.gradle | 2 +- settings.gradle | 1 + thirtyinch-kotlin-coroutines/.gitignore | 1 + thirtyinch-kotlin-coroutines/build.gradle | 33 +++++++++++++++++++ .../proguard-rules.pro | 21 ++++++++++++ .../src/main/AndroidManifest.xml | 1 + 6 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 thirtyinch-kotlin-coroutines/.gitignore create mode 100644 thirtyinch-kotlin-coroutines/build.gradle create mode 100644 thirtyinch-kotlin-coroutines/proguard-rules.pro create mode 100644 thirtyinch-kotlin-coroutines/src/main/AndroidManifest.xml diff --git a/build.gradle b/build.gradle index 318b4a06..f245d53a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.2.51" + ext.kotlin_version = "1.3.11" repositories { google() jcenter() diff --git a/settings.gradle b/settings.gradle index edfe46c1..19138ebe 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,6 +4,7 @@ include( ":thirtyinch-rx", ":thirtyinch-rx2", ":thirtyinch-kotlin", + ":thirtyinch-kotlin-coroutines", ":thirtyinch-lint", ":sample", ) \ No newline at end of file diff --git a/thirtyinch-kotlin-coroutines/.gitignore b/thirtyinch-kotlin-coroutines/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/thirtyinch-kotlin-coroutines/.gitignore @@ -0,0 +1 @@ +/build diff --git a/thirtyinch-kotlin-coroutines/build.gradle b/thirtyinch-kotlin-coroutines/build.gradle new file mode 100644 index 00000000..e43f4672 --- /dev/null +++ b/thirtyinch-kotlin-coroutines/build.gradle @@ -0,0 +1,33 @@ +apply plugin: "com.android.library" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: 'net.grandcentrix.gradle.publish' + +android { + compileSdkVersion COMPILE_SDK_VERSION + + defaultConfig { + minSdkVersion MIN_SDK_VERSION + targetSdkVersion TARGET_SDK_VERSION + versionCode VERSION_CODE + versionName VERSION_NAME + } + buildTypes { + release { + minifyEnabled false + } + debug { + } + } + lintOptions { + abortOnError false + } +} + +dependencies { + api project(":thirtyinch-kotlin") + api("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") + api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1") + + testImplementation "junit:junit:$junitVersion" + testImplementation "com.nhaarman:mockito-kotlin:1.5.0" +} \ No newline at end of file diff --git a/thirtyinch-kotlin-coroutines/proguard-rules.pro b/thirtyinch-kotlin-coroutines/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/thirtyinch-kotlin-coroutines/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/thirtyinch-kotlin-coroutines/src/main/AndroidManifest.xml b/thirtyinch-kotlin-coroutines/src/main/AndroidManifest.xml new file mode 100644 index 00000000..46d6cf00 --- /dev/null +++ b/thirtyinch-kotlin-coroutines/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + From 457f720d26cb958a21058e89b5f4aa3479887c19 Mon Sep 17 00:00:00 2001 From: Tom Seifert Date: Fri, 25 Jan 2019 13:07:19 +0100 Subject: [PATCH 02/12] Add TiCoroutineScope and a test --- .../kotlin/coroutines/TiCoroutineScope.kt | 46 ++++++++++++++++ .../kotlin/coroutines/TiCoroutineScopeTest.kt | 54 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt create mode 100644 thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt diff --git a/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt b/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt new file mode 100644 index 00000000..11f19c91 --- /dev/null +++ b/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt @@ -0,0 +1,46 @@ +package net.grandcentrix.thirtyinch.kotlin.coroutines + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import net.grandcentrix.thirtyinch.TiPresenter +import net.grandcentrix.thirtyinch.TiPresenter.State.DESTROYED +import net.grandcentrix.thirtyinch.TiPresenter.State.VIEW_DETACHED +import kotlin.coroutines.CoroutineContext + +/** + * A [CoroutineScope] that is bound to the lifecycle of the given [TiPresenter]. + * + * @param cancelWhenViewDetaches If all started coroutines in this scope should be cancelled when the view detaches. + * By default they're cancelled when `presenter` is destroyed. You should recreate this object when the view attaches, + * if you set this to `true`. + */ +class TiCoroutineScope( + private val presenter: TiPresenter<*>, + context: CoroutineContext, + cancelWhenViewDetaches: Boolean = false +) : CoroutineScope { + + /** + * Parent [Job] for all coroutines that are started in the given presenter instance. When this `job` gets + * cancelled all of its children jobs will get cancelled too. + */ + private val job = Job() + override val coroutineContext: CoroutineContext = context + job + /** + * The current [TiPresenter.State] of [presenter]. Needs to be kept as [VIEW_DETACHED] is observed first + * and would instantly cancel all coroutines of this scope. + */ + private var presenterState: TiPresenter.State? = null + + init { + presenter.addLifecycleObserver { state, hasLifecycleMethodBeenCalled -> + if (!hasLifecycleMethodBeenCalled) return@addLifecycleObserver + + when { + state == DESTROYED && !cancelWhenViewDetaches -> job.cancel() + state == VIEW_DETACHED && cancelWhenViewDetaches && presenterState != null -> job.cancel() + } + presenterState = state + } + } +} \ No newline at end of file diff --git a/thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt b/thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt new file mode 100644 index 00000000..9ce65d20 --- /dev/null +++ b/thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt @@ -0,0 +1,54 @@ +package net.grandcentrix.thirtyinch.kotlin.coroutines + +import kotlinx.coroutines.ObsoleteCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestCoroutineContext +import net.grandcentrix.thirtyinch.TiPresenter +import net.grandcentrix.thirtyinch.TiView +import org.junit.* +import org.junit.Assert.* +import org.junit.runner.* +import org.junit.runners.* + +@RunWith(JUnit4::class) +@ObsoleteCoroutinesApi +class TiCoroutineScopeTest { + + private val presenter = Presenter() + private val testPresenter = presenter.test() + private val coroutineContext = TestCoroutineContext() + private val view = object : TiView {} + + @Test + fun `cancels all jobs when presenter is destroyed`() { + val scope = TiCoroutineScope(presenter, coroutineContext) + testPresenter.attachView(view) + + // starting a job while a view is attached + val newJob = scope.launch { delay(10000) } + + // detaching the view doesn't cancel it + testPresenter.detachView() + assertFalse(newJob.isCancelled) + + // but destroying the presenter does + testPresenter.destroy() + assertTrue(newJob.isCancelled) + } + + @Test + fun `cancels all jobs when view is detached`() { + val scope = TiCoroutineScope(presenter, coroutineContext, true) + testPresenter.attachView(view) + + // starting a job while a view is attached + val newJob = scope.launch { delay(10000) } + + // detaching the view cancels the job + testPresenter.detachView() + assertTrue(newJob.isCancelled) + } +} + +class Presenter : TiPresenter() \ No newline at end of file From 9bdc7663dd7a52de70f5d3dcdeb31aa5843b0968 Mon Sep 17 00:00:00 2001 From: Tom Seifert Date: Fri, 25 Jan 2019 13:20:06 +0100 Subject: [PATCH 03/12] Add new module to README --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 0901f70c..f21b7c41 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ dependencies { implementation "net.grandcentrix.thirtyinch:thirtyinch-rx2:$thirtyinchVersion" implementation "net.grandcentrix.thirtyinch:thirtyinch-logginginterceptor:$thirtyinchVersion" implementation "net.grandcentrix.thirtyinch:thirtyinch-kotlin:$thirtyinchVersion" + implementation "net.grandcentrix.thirtyinch:thirtyinch-kotlin-coroutines:$thirtyinchVersion" // Lagacy dependencies implementation "net.grandcentrix.thirtyinch:thirtyinch-rx:$thirtyinchVersion" @@ -266,6 +267,35 @@ interface HelloWorldView : TiView { ``` Back in the Java days we had to use `it` inside the `sendToView`-lambda. +#### Coroutines +If you're using Kotlin's Coroutines we offer a `CoroutineScope` that scopes to a presenter's lifecycle. + +```kotlin +class HelloWorldPresenter : TiPresenter { + + private val scope = TiCoroutineScope(this, Dispatchers.Default) + + override fun onCreate() { + scope.launch { ... } + } +} +``` +The created `Job` will automatically be cancelled when the presenter is destroyed. + +You can also configure it so it cancels all jobs when the view detaches: +```kotlin +class HelloWorldPresenter : TiPresenter { + + private lateinit var scope: CoroutineScope + + override fun onAttachView(view: HelloWorldView) { + scope = TiCoroutineScope(this, Dispatchers.Default) + scope.launch { ... } + } +} +``` +Here, however, the `TiCoroutineScope` needs to be recreated every time a new view attaches. + ### [RxJava](https://github.com/ReactiveX/RxJava) Using RxJava for networking is very often used. From b1d544332a5f8f903fa2e9d101dc040dc60fd104 Mon Sep 17 00:00:00 2001 From: Tom Seifert <14896745+Syex@users.noreply.github.com> Date: Fri, 25 Jan 2019 18:43:37 +0100 Subject: [PATCH 04/12] Fix copy/paste error in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f21b7c41..4ed734f2 100644 --- a/README.md +++ b/README.md @@ -289,7 +289,7 @@ class HelloWorldPresenter : TiPresenter { private lateinit var scope: CoroutineScope override fun onAttachView(view: HelloWorldView) { - scope = TiCoroutineScope(this, Dispatchers.Default) + scope = TiCoroutineScope(this, Dispatchers.Default, true) scope.launch { ... } } } From 4608cf519c64d09324ffe1c14fe4da1d6758616f Mon Sep 17 00:00:00 2001 From: Tom Seifert <14896745+Syex@users.noreply.github.com> Date: Fri, 25 Jan 2019 18:44:02 +0100 Subject: [PATCH 05/12] Use latest Kotlin version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f245d53a..53934009 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.3.11" + ext.kotlin_version = "1.3.20" repositories { google() jcenter() From 2cb4dc07f9caeef0056791a7135bf1dcef25fadf Mon Sep 17 00:00:00 2001 From: Tom Seifert Date: Fri, 1 Feb 2019 15:15:23 +0100 Subject: [PATCH 06/12] Better support for launching coroutines until the view detaches --- README.md | 9 ++-- .../kotlin/coroutines/TiCoroutineScope.kt | 41 +++++++++++++++++-- .../kotlin/coroutines/TiCoroutineScopeTest.kt | 24 ++++++++++- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4ed734f2..0e43cd37 100644 --- a/README.md +++ b/README.md @@ -282,19 +282,18 @@ class HelloWorldPresenter : TiPresenter { ``` The created `Job` will automatically be cancelled when the presenter is destroyed. -You can also configure it so it cancels all jobs when the view detaches: +Alternatively, you can launch jobs that get cancelled when a `TiView` detaches: ```kotlin class HelloWorldPresenter : TiPresenter { - private lateinit var scope: CoroutineScope + private val scope = TiCoroutineScope(this, Dispatchers.Default) override fun onAttachView(view: HelloWorldView) { - scope = TiCoroutineScope(this, Dispatchers.Default, true) - scope.launch { ... } + scope.launchUntilViewDetaches { ... } } } ``` -Here, however, the `TiCoroutineScope` needs to be recreated every time a new view attaches. +However, be careful that `launchUntilViewDetaches` can only be called when there is a view attached! ### [RxJava](https://github.com/ReactiveX/RxJava) diff --git a/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt b/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt index 11f19c91..be524fd2 100644 --- a/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt +++ b/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt @@ -2,8 +2,11 @@ package net.grandcentrix.thirtyinch.kotlin.coroutines import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import net.grandcentrix.thirtyinch.TiPresenter import net.grandcentrix.thirtyinch.TiPresenter.State.DESTROYED +import net.grandcentrix.thirtyinch.TiPresenter.State.VIEW_ATTACHED import net.grandcentrix.thirtyinch.TiPresenter.State.VIEW_DETACHED import kotlin.coroutines.CoroutineContext @@ -16,8 +19,7 @@ import kotlin.coroutines.CoroutineContext */ class TiCoroutineScope( private val presenter: TiPresenter<*>, - context: CoroutineContext, - cancelWhenViewDetaches: Boolean = false + private val context: CoroutineContext ) : CoroutineScope { /** @@ -26,6 +28,15 @@ class TiCoroutineScope( */ private val job = Job() override val coroutineContext: CoroutineContext = context + job + /** + * Parent [Job] for all coroutines that are started while a `view` attached. When this `job` gets + * cancelled (when the view detaches) all of its children jobs will get cancelled too. + */ + private var onViewDetachJob: Job? = null + /** + * A [CoroutineContext] that will be used when starting any coroutines via [launchUntilViewDetaches]. + */ + private var onViewDetachCoroutineContext: CoroutineContext? = null /** * The current [TiPresenter.State] of [presenter]. Needs to be kept as [VIEW_DETACHED] is observed first * and would instantly cancel all coroutines of this scope. @@ -37,10 +48,32 @@ class TiCoroutineScope( if (!hasLifecycleMethodBeenCalled) return@addLifecycleObserver when { - state == DESTROYED && !cancelWhenViewDetaches -> job.cancel() - state == VIEW_DETACHED && cancelWhenViewDetaches && presenterState != null -> job.cancel() + state == DESTROYED -> job.cancel() + state == VIEW_DETACHED && presenterState != null -> { + onViewDetachCoroutineContext?.cancel() + onViewDetachCoroutineContext = null + onViewDetachJob = null + } + state == VIEW_ATTACHED -> { + onViewDetachJob = Job(job) + onViewDetachCoroutineContext = context + onViewDetachJob!! + } } presenterState = state } } + + /** + * Same as [launch], but the so started [Job] will be cancelled once the `view` detaches. + * + * Calling this method while no `view` is attached will throw a `IllegalStateException`, because no + * [CoroutineContext] is available at this time. + */ + fun launchUntilViewDetaches( + block: suspend CoroutineScope.() -> Unit + ): Job { + val context = onViewDetachCoroutineContext ?: throw IllegalStateException( + "launchUntilViewDetaches can only be called when there is a view attached") + return launch(context = context, block = block) + } } \ No newline at end of file diff --git a/thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt b/thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt index 9ce65d20..b54d4d4f 100644 --- a/thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt +++ b/thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt @@ -39,16 +39,36 @@ class TiCoroutineScopeTest { @Test fun `cancels all jobs when view is detached`() { - val scope = TiCoroutineScope(presenter, coroutineContext, true) + val scope = TiCoroutineScope(presenter, coroutineContext) testPresenter.attachView(view) // starting a job while a view is attached - val newJob = scope.launch { delay(10000) } + val newJob = scope.launchUntilViewDetaches { delay(10000) } // detaching the view cancels the job testPresenter.detachView() assertTrue(newJob.isCancelled) } + + @Test + fun `cancelling a job when view detaches does not cancel a job until presenter is destroyed`() { + val scope = TiCoroutineScope(presenter, coroutineContext) + testPresenter.attachView(view) + + // starting two jobs, one until presenter is destroyed, one until view detaches + val onDestroyJob = scope.launch { delay(10000) } + val onViewDetachJob = scope.launchUntilViewDetaches { delay(10000) } + + // and then view detaches, cancels onViewDetachJob but not onDestroyJob + testPresenter.detachView() + + assertTrue(onViewDetachJob.isCancelled) + assertFalse(onDestroyJob.isCancelled) + + // destroying presenter then cancels the job + testPresenter.destroy() + assertTrue(onDestroyJob.isCancelled) + } } class Presenter : TiPresenter() \ No newline at end of file From 26c39353fb0eba0c129716ced6f7b07b0c9ef291 Mon Sep 17 00:00:00 2001 From: Tom Seifert Date: Thu, 7 Feb 2019 11:44:12 +0100 Subject: [PATCH 07/12] Use latest coroutines version and add consumer ProGuard config --- thirtyinch-kotlin-coroutines/build.gradle | 3 ++- .../proguard-rules.pro | 26 ++++--------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/thirtyinch-kotlin-coroutines/build.gradle b/thirtyinch-kotlin-coroutines/build.gradle index e43f4672..e406e909 100644 --- a/thirtyinch-kotlin-coroutines/build.gradle +++ b/thirtyinch-kotlin-coroutines/build.gradle @@ -10,6 +10,7 @@ android { targetSdkVersion TARGET_SDK_VERSION versionCode VERSION_CODE versionName VERSION_NAME + consumerProguardFiles 'proguard-rules.pro' } buildTypes { release { @@ -26,7 +27,7 @@ android { dependencies { api project(":thirtyinch-kotlin") api("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") - api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1") + api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1") testImplementation "junit:junit:$junitVersion" testImplementation "com.nhaarman:mockito-kotlin:1.5.0" diff --git a/thirtyinch-kotlin-coroutines/proguard-rules.pro b/thirtyinch-kotlin-coroutines/proguard-rules.pro index f1b42451..9631e4a1 100644 --- a/thirtyinch-kotlin-coroutines/proguard-rules.pro +++ b/thirtyinch-kotlin-coroutines/proguard-rules.pro @@ -1,21 +1,5 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} +-keepclassmembernames class kotlinx.** { + volatile ; +} \ No newline at end of file From 8fd35dad5f2ad5380d265991fd265b5319cb1de3 Mon Sep 17 00:00:00 2001 From: Tom Seifert Date: Thu, 7 Feb 2019 11:45:55 +0100 Subject: [PATCH 08/12] Improve KDoc and rename base job --- .../kotlin/coroutines/TiCoroutineScope.kt | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt b/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt index be524fd2..95d68252 100644 --- a/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt +++ b/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt @@ -12,10 +12,6 @@ import kotlin.coroutines.CoroutineContext /** * A [CoroutineScope] that is bound to the lifecycle of the given [TiPresenter]. - * - * @param cancelWhenViewDetaches If all started coroutines in this scope should be cancelled when the view detaches. - * By default they're cancelled when `presenter` is destroyed. You should recreate this object when the view attaches, - * if you set this to `true`. */ class TiCoroutineScope( private val presenter: TiPresenter<*>, @@ -23,13 +19,13 @@ class TiCoroutineScope( ) : CoroutineScope { /** - * Parent [Job] for all coroutines that are started in the given presenter instance. When this `job` gets - * cancelled all of its children jobs will get cancelled too. + * Parent [Job] for all coroutines that are started in the given presenter instance. When this `onPresenterDestroyedJob` gets + * cancelled (when the presenter is destroyed) all of its children jobs will get cancelled too. */ - private val job = Job() - override val coroutineContext: CoroutineContext = context + job + private val onPresenterDestroyedJob = Job() + override val coroutineContext: CoroutineContext = context + onPresenterDestroyedJob /** - * Parent [Job] for all coroutines that are started while a `view` attached. When this `job` gets + * Parent [Job] for all coroutines that are started while a `view` attached. When this `onPresenterDestroyedJob` gets * cancelled (when the view detaches) all of its children jobs will get cancelled too. */ private var onViewDetachJob: Job? = null @@ -48,14 +44,14 @@ class TiCoroutineScope( if (!hasLifecycleMethodBeenCalled) return@addLifecycleObserver when { - state == DESTROYED -> job.cancel() + state == DESTROYED -> onPresenterDestroyedJob.cancel() state == VIEW_DETACHED && presenterState != null -> { onViewDetachCoroutineContext?.cancel() onViewDetachCoroutineContext = null onViewDetachJob = null } state == VIEW_ATTACHED -> { - onViewDetachJob = Job(job) + onViewDetachJob = Job(onPresenterDestroyedJob) onViewDetachCoroutineContext = context + onViewDetachJob!! } } From 842b23fcc4987361a4bf5b919a11e4f60dac144a Mon Sep 17 00:00:00 2001 From: Tom Seifert Date: Thu, 7 Feb 2019 11:47:58 +0100 Subject: [PATCH 09/12] Use apply to avoid using !! --- .../thirtyinch/kotlin/coroutines/TiCoroutineScope.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt b/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt index 95d68252..d3a2e82c 100644 --- a/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt +++ b/thirtyinch-kotlin-coroutines/src/main/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScope.kt @@ -51,8 +51,9 @@ class TiCoroutineScope( onViewDetachJob = null } state == VIEW_ATTACHED -> { - onViewDetachJob = Job(onPresenterDestroyedJob) - onViewDetachCoroutineContext = context + onViewDetachJob!! + onViewDetachJob = Job(onPresenterDestroyedJob).apply { + onViewDetachCoroutineContext = context + this + } } } presenterState = state From 32e36c48b529f110cc160fdd4ea6ff6c1612b317 Mon Sep 17 00:00:00 2001 From: Tom Seifert Date: Thu, 7 Feb 2019 11:58:41 +0100 Subject: [PATCH 10/12] Update couple of versions --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 53934009..2accfc72 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.3.20" + ext.kotlin_version = "1.3.21" repositories { google() jcenter() } dependencies { - classpath "com.android.tools.build:gradle:3.1.3" // if you update this, also update the lintVersion below - classpath "com.vanniktech:gradle-android-junit-jacoco-plugin:0.10.0" + classpath "com.android.tools.build:gradle:3.3.1" // if you update this, also update the lintVersion below + classpath "com.vanniktech:gradle-android-junit-jacoco-plugin:0.11.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'guru.stefma.bintrayrelease:bintrayrelease:1.1.1' } @@ -50,7 +50,7 @@ ext { // According to https://github.com/googlesamples/android-custom-lint-rules/tree/master/android-studio-3 // the lint version should match to the used Android Gradle Plugin by the formula "AGP Version X.Y.Z + 23.0.0" // E.g. "AGP Version 3.1.3 + 23.0.0 = Lint Version 26.1.3" - lintVersion = '26.1.3' + lintVersion = '26.3.1' } allprojects { From 7dc51859792ac82094e28a8836f58e9b41bdadae Mon Sep 17 00:00:00 2001 From: StefMa Date: Wed, 13 Feb 2019 14:54:58 +0100 Subject: [PATCH 11/12] Update travis build tool version --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9937c5cf..1ebdfb1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ android: # The BuildTools version used by your project - build-tools-27.0.3 + - build-tools-28.0.3 # The SDK version used to compile your project - android-22 From b2b07a281f9626edf17ab55fcb419bfbf55c7404 Mon Sep 17 00:00:00 2001 From: StefMa Date: Wed, 13 Feb 2019 15:27:32 +0100 Subject: [PATCH 12/12] Add test for throwing --- .../kotlin/coroutines/TiCoroutineScopeTest.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt b/thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt index b54d4d4f..807fa8a3 100644 --- a/thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt +++ b/thirtyinch-kotlin-coroutines/src/test/java/net/grandcentrix/thirtyinch/kotlin/coroutines/TiCoroutineScopeTest.kt @@ -19,10 +19,10 @@ class TiCoroutineScopeTest { private val testPresenter = presenter.test() private val coroutineContext = TestCoroutineContext() private val view = object : TiView {} + private val scope = TiCoroutineScope(presenter, coroutineContext) @Test fun `cancels all jobs when presenter is destroyed`() { - val scope = TiCoroutineScope(presenter, coroutineContext) testPresenter.attachView(view) // starting a job while a view is attached @@ -39,7 +39,6 @@ class TiCoroutineScopeTest { @Test fun `cancels all jobs when view is detached`() { - val scope = TiCoroutineScope(presenter, coroutineContext) testPresenter.attachView(view) // starting a job while a view is attached @@ -52,7 +51,6 @@ class TiCoroutineScopeTest { @Test fun `cancelling a job when view detaches does not cancel a job until presenter is destroyed`() { - val scope = TiCoroutineScope(presenter, coroutineContext) testPresenter.attachView(view) // starting two jobs, one until presenter is destroyed, one until view detaches @@ -69,6 +67,16 @@ class TiCoroutineScopeTest { testPresenter.destroy() assertTrue(onDestroyJob.isCancelled) } + + @Test + fun `throw when launchUntilViewDetaches got called before view got attached`() { + // don't attach a view + try { + scope.launchUntilViewDetaches { delay(10000) } + } catch (exe: IllegalStateException) { + assertTrue(exe.message == "launchUntilViewDetaches can only be called when there is a view attached") + } + } } class Presenter : TiPresenter() \ No newline at end of file