Skip to content
This repository has been archived by the owner on Jul 13, 2020. It is now read-only.

Commit

Permalink
Add the first implementation of anko-coroutines (#327)
Browse files Browse the repository at this point in the history
  • Loading branch information
yanex committed Mar 1, 2017
1 parent 9080535 commit 3e4fddb
Show file tree
Hide file tree
Showing 13 changed files with 311 additions and 6 deletions.
6 changes: 6 additions & 0 deletions anko/library/generated/coroutines/AndroidManifest.xml
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.jetbrains.anko.generated.coroutines">
<uses-sdk android:minSdkVersion="15" android:targetSdkVersion="23"/>
<application/>
</manifest>
16 changes: 16 additions & 0 deletions anko/library/generated/coroutines/build.gradle
@@ -0,0 +1,16 @@
apply from: '../generated.gradle'

dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.12"
}

kotlin {
experimental {
coroutines "enable"
}
}

task androidReleaseSources(type: Jar, dependsOn: assembleRelease) {
from("src", "../../static/common/src")
}
35 changes: 35 additions & 0 deletions anko/library/generated/coroutines/src/AnkoCoroutine.kt
@@ -0,0 +1,35 @@
/*
* Copyright 2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jetbrains.anko.async

import kotlinx.coroutines.experimental.*
import kotlin.coroutines.experimental.*

internal open class AnkoCoroutine<out C : Any>(
override val parentContext: CoroutineContext,
active: Boolean = true
) : AbstractCoroutine<Unit>(active), AnkoCoroutineScope<C> {
override fun afterCompletion(state: Any?, mode: Int) {
if (state is CompletedExceptionally) {
handleCoroutineException(parentContext, state.exception)
}
}

@Suppress("UNCHECKED_CAST")
override val caller: C
get() = this[AsyncCaller]!!.ref as C
}
49 changes: 49 additions & 0 deletions anko/library/generated/coroutines/src/AnkoCoroutineDispatcher.kt
@@ -0,0 +1,49 @@
/*
* Copyright 2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jetbrains.anko.async

import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.experimental.*
import java.util.concurrent.Executors
import kotlin.coroutines.experimental.*

internal object AnkoCoroutineDispatcher : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
if (CoroutineAndroidContextHelper.mainThread == Thread.currentThread()) {
context[AsyncCaller]?.withHardReference(
onExisting = { block.run() },
onDisposed = { context[Job]!!.cancel(CallerDisposedException()) })
} else {
CoroutineAndroidContextHelper.handler.post {
context[AsyncCaller]?.withHardReference(
onExisting = { block.run() },
onDisposed = { context[Job]!!.cancel(CallerDisposedException()) })
}
}
}

override fun toString() = "AnkoCoroutineDispatcher"
}

internal object CoroutineAndroidContextHelper {
val handler = Handler(Looper.getMainLooper())
val mainThread: Thread = Looper.getMainLooper().thread
}

internal val DEFAULT_EXECUTOR = Executors.newScheduledThreadPool(
2 * Runtime.getRuntime().availableProcessors())
45 changes: 45 additions & 0 deletions anko/library/generated/coroutines/src/AsyncCaller.kt
@@ -0,0 +1,45 @@
/*
* Copyright 2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jetbrains.anko.async

import java.lang.ref.WeakReference
import kotlin.coroutines.experimental.*

internal class AsyncCaller<C : Any>(ctx: C) : AbstractCoroutineContextElement(AsyncCaller) {
companion object Key : CoroutineContext.Key<AsyncCaller<*>>

internal val weakRef = WeakReference(ctx)
private var hardRef: C? = null

internal val ref: C
get() = hardRef!!

// This function is not thread-safe, must be called from the UI thread.
internal inline fun withHardReference(onExisting: () -> Unit, onDisposed: () -> Unit) {
val hardRef = this[AsyncCaller]?.weakRef?.get() ?: run {
onDisposed()
return
}

@Suppress("UNCHECKED_CAST")
this.hardRef = hardRef as C

onExisting()

this.hardRef = null
}
}
24 changes: 24 additions & 0 deletions anko/library/generated/coroutines/src/AsyncExecutor.kt
@@ -0,0 +1,24 @@
/*
* Copyright 2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jetbrains.anko.async

import java.util.concurrent.Executor
import kotlin.coroutines.experimental.*

internal class AsyncExecutor(val executor: Executor) : AbstractCoroutineContextElement(AsyncExecutor) {
companion object Key : CoroutineContext.Key<AsyncExecutor>
}
59 changes: 59 additions & 0 deletions anko/library/generated/coroutines/src/async.kt
@@ -0,0 +1,59 @@
/*
* Copyright 2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jetbrains.anko.async

import kotlinx.coroutines.experimental.*
import java.util.concurrent.Executor
import kotlin.coroutines.experimental.*

fun <C : Any> C.async(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend AnkoCoroutineScope<C>.() -> Unit
): Job {
return async(DEFAULT_EXECUTOR, context, block)
}

@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
fun <C : Any> C.async(
executor: Executor,
context: CoroutineContext = EmptyCoroutineContext,
block: suspend AnkoCoroutineScope<C>.() -> Unit
): Job {
val newContext = newCoroutineContext(context) +
AsyncCaller(this) + AsyncExecutor(executor) + AnkoCoroutineDispatcher

val coroutine = AnkoCoroutine<C>(newContext)
coroutine.initParentJob(context[Job])
block.startCoroutine(coroutine, coroutine)

return coroutine
}

interface AnkoCoroutineScope<out C : Any> : CoroutineScope {
val caller: C
}

suspend fun <R> AnkoCoroutineScope<*>.bg(f: () -> R): R {
val executor = context[AsyncExecutor]?.executor ?: DEFAULT_EXECUTOR
return suspendCoroutine { c ->
executor.execute {
c.resume(f())
}
}
}

class CallerDisposedException : RuntimeException()
3 changes: 2 additions & 1 deletion anko/library/robolectricTests/build.gradle
Expand Up @@ -2,7 +2,7 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdkVersion 21
compileSdkVersion COMPILE_SDK_VERSION
buildToolsVersion BUILD_TOOLS_VERSION
defaultConfig {
minSdkVersion MIN_SDK_VERSION
Expand All @@ -16,6 +16,7 @@ dependencies {
compile project(':generated:anko-common')
compile project(':generated:anko-sqlite')
compile project(':generated:anko-sdk15')
compile project(':generated:anko-coroutines')
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
testCompile "org.robolectric:robolectric:3.0"
}
58 changes: 58 additions & 0 deletions anko/library/robolectricTests/src/test/java/AsyncTest.kt
@@ -0,0 +1,58 @@
package test

import android.app.Activity
import android.os.Looper
import org.jetbrains.anko.async.async
import org.jetbrains.anko.async.bg
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricGradleTestRunner
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowLooper

class AsyncTestActivity : Activity() {
val result = ThreadLocal<String>()

fun loadData() = async {
assertSame(MAIN_THREAD, Thread.currentThread())
val data = bg {
assertNotSame(MAIN_THREAD, Thread.currentThread())
loadDataSync()
}
assertSame(MAIN_THREAD, Thread.currentThread())
assertEquals(HELLO_WORLD, data)

result.set(data)
}
}

private val DELAY = 500L
private val MAIN_THREAD = Looper.getMainLooper().thread

private val HELLO_WORLD = "Hello, world!"

private fun loadDataSync(): String {
Thread.sleep(DELAY)
return HELLO_WORLD
}

@RunWith(RobolectricGradleTestRunner::class)
@Config(constants = BuildConfig::class)
class AsyncTest {
@Test
fun test() {
val activity = Robolectric.buildActivity(AsyncTestActivity::class.java).create().get()

activity.loadData()

val time = System.currentTimeMillis() + DELAY * 2 + 1000
while (System.currentTimeMillis() < time) {
ShadowLooper.idleMainLooper()
Thread.yield()
}

assertEquals(HELLO_WORLD, activity.result.get())
}
}
6 changes: 6 additions & 0 deletions anko/library/testUtils/build.gradle
Expand Up @@ -14,4 +14,10 @@ sourceSets {
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile 'junit:junit:4.12'
}

kotlin {
experimental {
coroutines "enable"
}
}
11 changes: 7 additions & 4 deletions build.gradle
Expand Up @@ -2,7 +2,7 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream

buildscript {
ext.kotlin_version = '1.1.0-rc-84'
ext.kotlin_version = '1.1.0'

ext.ANKO_VERSION = '0.10.0'
ext.ANKO_VERSION_CODE = 100
Expand All @@ -27,8 +27,9 @@ buildscript {
ext.POM_ARTIFACT_GROUP = 'org.jetbrains.anko'

repositories {
mavenCentral()
jcenter()
maven { url "http://dl.bintray.com/kotlin/kotlin-dev" }
maven { url "http://dl.bintray.com/kotlin/kotlin-eap-1.1" }
}

dependencies {
Expand All @@ -42,9 +43,10 @@ buildscript {

allprojects {
repositories {
mavenCentral()
jcenter()
maven { url "http://dl.bintray.com/kotlin/kotlinx.dom" }
maven { url "http://dl.bintray.com/kotlin/kotlin-dev" }
maven { url "http://dl.bintray.com/kotlin/kotlin-eap-1.1" }
}
}

Expand Down Expand Up @@ -80,7 +82,8 @@ def generatedArtifacts = [
':generated:anko-sdk21',
':generated:anko-sdk23',
':generated:anko-sqlite',
':generated:anko-support-v4'
':generated:anko-support-v4',
':generated:anko-coroutines'
]

task dist()
Expand Down
3 changes: 2 additions & 1 deletion local.properties
@@ -1 +1,2 @@
sdk.dir=dependencies/android-sdk
sdk.dir=dependencies/android-sdk
kotlin.coroutines=enable

0 comments on commit 3e4fddb

Please sign in to comment.