Compare commits

...

85 commits

Author SHA1 Message Date
John O'Reilly
c666ae73b5
Merge pull request #115 from yschimke/glance2
Glance upgrades
2022-01-29 13:26:02 +00:00
Yuri Schimke
a1d7cf3f95 Glance upgrades 2022-01-29 13:03:32 +00:00
John O'Reilly
be2cfc9693 agp plugin 7.1.0 and gradle 7.2.0 2022-01-25 18:56:33 +00:00
John O'Reilly
539d27f6a7
Update README.md 2022-01-19 20:24:17 +00:00
John O'Reilly
3f33e636f1
Merge pull request #110 from joreilly/graphql
graphql backend
2022-01-19 20:07:47 +00:00
John O'Reilly
d11367c0ec graphql backend 2022-01-19 19:47:17 +00:00
John O'Reilly
f0a0cb5b52
Merge pull request #109 from joreilly/tests
test updates
2022-01-01 11:39:32 +00:00
John O'Reilly
7924d4ea98 test updates 2022-01-01 11:12:59 +00:00
John O'Reilly
a1afb99d80
Merge pull request #108 from joreilly/kotlinx_serialization_1_3_2
kotlinx serialization 1.3.2
2021-12-23 17:22:56 +00:00
John O'Reilly
d97c45b7aa kotlinx serialization 1.3.2 2021-12-23 17:02:11 +00:00
John O'Reilly
81278bfa6b
Update README.md 2021-12-23 16:18:05 +00:00
John O'Reilly
eeab97d9ea
Merge pull request #107 from joreilly/compose_desktop_1_0_1
Compose for Desktop and Web 1.0.1
2021-12-23 15:25:42 +00:00
John O'Reilly
e3964652df Compose for Desktop and Web 1.0.1 2021-12-23 15:05:46 +00:00
John O'Reilly
d2acc790f8
Merge pull request #106 from joreilly/update_versions
Ktor 2.0.0-beta-1, Coroutines 1.6 and KMP-NativeCoroutines 0.11.1-new-mm, accompanist 0.22.0-rc
2021-12-23 11:36:02 +00:00
John O'Reilly
e03495719c Ktor 2.0.0-beta-1, Coroutines 1.6 and KMP-NativeCoroutines 0.11.1-new-mm 2021-12-23 11:14:30 +00:00
John O'Reilly
6017e2863a accompanist 0.22.0-rc 2021-12-23 09:17:53 +00:00
John O'Reilly
f6fb2385a6
Merge pull request #105 from saryongkang/improve-scroll
Improve rotary scrolling
2021-12-23 09:17:24 +00:00
Saryong Kang
8db96078d2 improve scrolling 2021-12-23 16:15:51 +09:00
John O'Reilly
93aac4c617
Merge pull request #104 from yschimke/simpler_scroll
Simpler implementation of Wear scrolling
2021-12-19 13:02:21 +00:00
Yuri Schimke
4ab4ea1830 Simpler scrolling 2021-12-19 12:24:28 +00:00
Yuri Schimke
ee6408a5b0 Simpler scrolling 2021-12-19 12:16:16 +00:00
John O'Reilly
d35f8e64fa
Merge pull request #103 from joreilly/js_apple_silicon
workaround for node.js version on apple silicon
2021-12-18 18:54:16 +00:00
John O'Reilly
6f622097e6 workaround for node.js version on apple silicon 2021-12-18 18:27:21 +00:00
John O'Reilly
b2129ebff9
Merge pull request #102 from joreilly/backend_ktor_2
update backend module to also use Ktor 2.0
2021-12-18 18:26:52 +00:00
John O'Reilly
6d2e75146f update backend module to also use Ktor 2.0 2021-12-18 13:51:33 +00:00
John O'Reilly
cd990acd39
Merge pull request #101 from joreilly/build_cleanup
build cleanup
2021-12-18 12:49:17 +00:00
John O'Reilly
49237b790b build cleanup 2021-12-18 12:09:44 +00:00
John O'Reilly
6e994a133a
Merge pull request #100 from joreilly/widget
fix iOS Widget code
2021-12-18 00:06:57 +00:00
John O'Reilly
dc4646ee3f fix iOS Widget code 2021-12-17 23:48:20 +00:00
John O'Reilly
6ef52970d4
Update README.md 2021-12-17 23:23:28 +00:00
John O'Reilly
e78d57702c
Merge pull request #99 from joreilly/kotlin_1_6_10
Kotlin 1.6.10, K/N new memory model, use Swift 5.5 concurrency apis
2021-12-17 23:22:18 +00:00
John O'Reilly
08a51bb92d Kotlin 1.6.10, K/N new memory model related changes use new Swift concurrency features 2021-12-17 23:03:00 +00:00
John O'Reilly
3bdaab7c11 android gradle plugin 7.0.4 2021-12-11 16:18:25 +00:00
John O'Reilly
b8598b7072
Merge pull request #98 from joreilly/m1
m1 related changes
2021-12-10 17:28:35 +00:00
John O'Reilly
7a77b35f80 m1 related changes 2021-12-10 17:05:25 +00:00
John O'Reilly
5fc45a8043
Merge pull request #97 from joreilly/kermit
Kermit 1.0
2021-12-09 22:13:23 +00:00
John O'Reilly
c5f5c7ca8b Kermit 1.0 2021-12-09 21:57:11 +00:00
John O'Reilly
5b5196dbf6 people data udpates 2021-12-08 17:14:04 +00:00
John O'Reilly
26575b2e33 Compose for Desktop/Web version 1.0.0 2021-12-02 14:16:14 +01:00
John O'Reilly
ae1264314c
Merge pull request #96 from joreilly/kotlin_1_6
Kotlin 1.6, enable K/N memory model
2021-11-20 15:42:52 +00:00
John O'Reilly
6ad647e7ce Kotlin 1.6, enable K/N memory model 2021-11-20 15:24:14 +00:00
John O'Reilly
50dc587728
Merge pull request #95 from nateridderman/patch-1
Update README.md
2021-11-15 14:17:37 +00:00
Nate Ridderman
2987ba4845
Update README.md
fix typo
2021-11-15 08:45:24 -05:00
John O'Reilly
44a9729b0c
Merge pull request #94 from rickclephas/bugfix/macos-ktor-curl-tls-error
Use Ktor iOS for macOS
2021-11-06 14:26:36 +00:00
Rick Clephas
ed6e6e8bb9 Use Ktor iOS for macOS 2021-11-06 14:56:56 +01:00
John O'Reilly
08c10f67de
Merge pull request #93 from rickclephas/feature/kmp-nativecoroutines
Use KMP-NativeCoroutines on Apple platforms
2021-11-06 13:51:09 +00:00
Rick Clephas
71c17855f2 Update README.md 2021-11-06 14:19:26 +01:00
Rick Clephas
cf8cec951a Update watchOS app to use KMPNativeCoroutinesCombine 2021-11-06 13:39:40 +01:00
Rick Clephas
7765f2526b Update macOS app to use KMPNativeCoroutinesCombine 2021-11-06 13:24:11 +01:00
John O'Reilly
07bfe06bf7
Merge pull request #91 from yschimke/tile
Tile for People in Space
2021-11-06 12:22:31 +00:00
Rick Clephas
3b5c8026e2 Update iOS app to use KMPNativeCoroutinesCombine 2021-11-06 13:18:52 +01:00
Yuri Schimke
7a20e6a876 Cleanup 2021-11-06 11:54:14 +00:00
Rick Clephas
163882c121 Update PeopleInSpaceRepo to use KMP-NativeCoroutines 2021-11-06 12:32:53 +01:00
Rick Clephas
a41f29dcbc Add KMP-NativeCoroutines 2021-11-06 12:04:28 +01:00
Yuri Schimke
19294f4b30 Glance Tile for People in Space 2021-11-06 10:32:57 +00:00
John O'Reilly
547bcb68a4
Update README.md 2021-11-05 20:40:17 +00:00
John O'Reilly
256f890931 update accompanist dependency to be compatible with glance api 2021-11-05 20:34:09 +00:00
John O'Reilly
5303d9531e
Merge pull request #90 from yschimke/glance
[WIP] Glance Widgets
2021-11-05 20:31:42 +00:00
John O'Reilly
a397e4fd46 update jetpack compose dependency 2021-11-05 20:17:23 +00:00
John O'Reilly
3db2b87cf4 use specific build id for glance dependency 2021-11-05 20:06:18 +00:00
John O'Reilly
e64a7a1045 accompanist 0.20.2 2021-11-05 15:58:25 +00:00
Yuri Schimke
91626e4efd Glance Widget for People in Space 2021-11-05 15:48:11 +00:00
Yuri Schimke
4e2f7f9d77 Merge branch 'main' into glance
# Conflicts:
#	buildSrc/src/main/java/Dependencies.kt
2021-11-05 09:50:41 +00:00
John O'Reilly
003ceef2a1 fix web client builds 2021-11-04 18:48:02 +00:00
John O'Reilly
1b6050f9ee Jetpack Compose 1.0.5 + other dependency updates 2021-11-04 18:35:34 +00:00
John O'Reilly
c50980f9ef
Update README.md 2021-11-04 17:53:09 +00:00
John O'Reilly
c3183fdc1d
Merge pull request #89 from joreilly/widget_ios
ios widget implementation
2021-11-04 17:10:05 +00:00
John O'Reilly
31fdac4230 ios widget implementation 2021-11-04 14:54:41 +00:00
John O'Reilly
77798c5ff8 glance wip 2021-10-31 20:49:33 +00:00
John O'Reilly
1dc279c56b
Update README.md 2021-10-29 21:42:02 +01:00
John O'Reilly
564476f6d2 watchos updates 2021-10-29 21:38:30 +01:00
John O'Reilly
22cd1dd4e1 watchos updates 2021-10-29 21:27:29 +01:00
John O'Reilly
a726656487 Compose for Desktop/Web version 1.0.0-beta5 2021-10-29 17:29:46 +01:00
John O'Reilly
318140bc0c
Update README.md 2021-10-29 17:22:18 +01:00
John O'Reilly
26ba82255d
Update README - Compose screenshots 2021-10-29 17:21:38 +01:00
John O'Reilly
ff8a9efb5b
Merge pull request #88 from yschimke/release
Only just working proguard rules for release build
2021-10-29 17:05:09 +01:00
Yuri Schimke
373169293d Only just working proguard rules for release build 2021-10-29 18:49:58 +03:00
John O'Reilly
14f1ca190c
Merge pull request #87 from yschimke/deeplinks
ISS Map and deeplinks
2021-10-29 16:47:19 +01:00
Yuri Schimke
33854565ef Fix tests 2021-10-29 18:22:12 +03:00
Yuri Schimke
72b93622c8 ISS Map and deeplinks 2021-10-29 18:07:56 +03:00
John O'Reilly
23d9cbc382 update people data 2021-10-28 09:45:04 +01:00
John O'Reilly
a3e01c763b watchos related updates 2021-10-25 18:52:31 +01:00
John O'Reilly
4599a84184 sqldelight 1.5.2 2021-10-25 18:08:17 +01:00
John O'Reilly
994c6db69d update people data 2021-10-24 09:23:37 +01:00
John O'Reilly
cfc5d3f431 Compose for Desktop/Web version 1.0.0-beta1 2021-10-22 19:11:57 +02:00
225 changed files with 8278 additions and 907 deletions

View file

@ -59,4 +59,4 @@ jobs:
with:
api-level: 29
target: google_apis
script: ./gradlew app:connectedAndroidTest common:connectedAndroidTest
script: ./gradlew app:connectedAndroidTest

View file

@ -2,8 +2,10 @@
Minimal **Kotlin Multiplatform** project with SwiftUI, Jetpack Compose, Compose for Wear OS, Compose for Desktop, Compose for Web, and Kotlin/JS + React clients along with Ktor backend. Currently running on
* Android (Jetpack Compose)
* Android App Widget (Compose based Glance API - contributed by https://github.com/yschimke)
* Wear OS (Compose for Wear OS - primarily developed by https://github.com/yschimke)
* iOS (SwiftUI)
* iOS App Widget (SwiftUI)
* watchOS (SwiftUI) (contributed by https://github.com/nealsanche)
* macOS (SwiftUI)
* Desktop (Compose for Desktop)
@ -27,11 +29,12 @@ Related posts:
* [Wrapping Kotlin Flow with Swift Combine Publisher in a Kotlin Multiplatform project](https://johnoreilly.dev/posts/kotlinmultiplatform-swift-combine_publisher-flow/)
* [Using Swift Packages in a Kotlin Multiplatform project](https://johnoreilly.dev/posts/kotlinmultiplatform-swift-package/)
* [Using Swift's new async/await when invoking Kotlin Multiplatform code](https://johnoreilly.dev/posts/swift_async_await_kotlin_coroutines/)
* [Exploring new AWS SDK for Kotlin](https://johnoreilly.dev/posts/aws-sdk-kotlin/)
Note that this repository very much errs on the side of minimalism to help more clearly illustrate key moving parts of a Kotlin
Multiplatform project and also to hopefully help someone just starting to explore KMP to get up and running for first time (and is of course
primarily focussed on use of Jetpack Compose and SwiftUI). If you're at stage of moving
primarily focused on use of Jetpack Compose and SwiftUI). If you're at the stage of moving
beyond this then I'd definitely recommend checking out [KaMPKit](https://github.com/touchlab/KaMPKit) from Touchlab.
I also have the following samples that demonstrate the use of a variety of Kotlin Multiplatform libraries (and also use Jetpack Compose and SwiftUI).
@ -44,7 +47,7 @@ I also have the following samples that demonstrate the use of a variety of Kotli
### Building
You need to use Android Studio Arctic Fox (**note: Java 11 is now the minimum version required**). Most recently tested with XCode v12 and v13.
You need to use Android Studio Arctic Fox (**note: Java 11 is now the minimum version required**). Requires XCode 13.2 or later (due to use of new Swift 5.5 concurrnecy APIs).
When opening iOS/watchOS/macOS projects remember to open `.xcworkspace` file (and not `.xcodeproj` one).
@ -64,7 +67,7 @@ invoking `./gradlew :compose-web:jsBrowserDevelopmentRun`
This client is available in `compose-desktop` module. Note that you need to use appropriate version of JVM when running (works for example with Java 11)
### Deploying backend code
### Backend code
Have tested this out in Google App Engine deployment. Using shadowJar plugin to create an "uber" jar and then deploying it as shown below. Should be possible to deploy this jar to other services as well.
@ -73,6 +76,11 @@ Have tested this out in Google App Engine deployment. Using shadowJar plugin to
gcloud app deploy backend/build/libs/backend-all.jar
```
### GraphQL backend
There's a GraphQL module (`graphql-server`) which can be run locally using `./gradlew :graphql-server:bootRun` with "playground" then available at http://localhost:8080/playground
### Screenshots
@ -80,19 +88,30 @@ gcloud app deploy backend/build/libs/backend-all.jar
<br/>
<img width="546" alt="Screenshot 2021-02-27 at 12 09 02" src="https://user-images.githubusercontent.com/6302/109386736-ac1f0700-78f4-11eb-812e-4bf971a8c2a7.png">
**Android (Jetpack Compose)**
<br/>
<img width="555" alt="Screenshot 2021-03-07 at 17 03 46" src="https://user-images.githubusercontent.com/6302/110248059-2ab81c00-7f67-11eb-9b3a-2b04d1be43ef.png">
**watchOS (SwiftUI)**
<br/>
<img width="250" alt="watchOS Screenshot 1" src="https://user-images.githubusercontent.com/6302/139499100-dc5112b0-04b9-4bdc-9c30-9975f3608eb3.png">
<img width="250" alt="watch0S Screenshot 2" src="https://user-images.githubusercontent.com/6302/139499115-944b241d-8e92-428b-b86c-f599b456c4bf.png">
**Wear OS (Wear Compose)**
<br/>
<img width="250" alt="Wear Compose Screenshot 1" src="https://user-images.githubusercontent.com/6302/137623548-ac51ca72-572e-4009-8b34-315defdf93a5.png">
<img width="250" alt="Wear Compose Screenshot 2" src="https://user-images.githubusercontent.com/6302/137640396-851489bb-e41d-47ef-badb-e2d22454eee4.png">
<img width="250" alt="Wear Compose Screenshot 3" src="https://user-images.githubusercontent.com/6302/139468900-16ad4e95-41dc-427f-977c-b893b1751c78.png">
**macOS (SwiftUI)**
<br/>
<img width="937" alt="Screenshot 2021-06-01 at 20 02 31" src="https://user-images.githubusercontent.com/6302/120376983-6ec37e80-c314-11eb-8279-7acc0c2d5206.png">
**Android (Jetpack Compose)**
<br/>
<img width="555" alt="Screenshot 2021-03-07 at 17 03 46" src="https://user-images.githubusercontent.com/6302/110248059-2ab81c00-7f67-11eb-9b3a-2b04d1be43ef.png">
**Wear OS (Jetpack Compose)**
<br/>
<img width="300" alt="Screenshot 2021-07-02 at 18 30 03" src="https://user-images.githubusercontent.com/6302/137623548-ac51ca72-572e-4009-8b34-315defdf93a5.png">
<img width="300" alt="Screenshot 2021-07-02 at 18 30 03" src="https://user-images.githubusercontent.com/6302/137640396-851489bb-e41d-47ef-badb-e2d22454eee4.png">
@ -122,3 +141,4 @@ gcloud app deploy backend/build/libs/backend-all.jar
* [SQLDelight](https://github.com/cashapp/sqldelight)
* [Jetpack Compose](https://developer.android.com/jetpack/compose)
* [SwiftUI](https://developer.apple.com/documentation/swiftui)
* [KMP-NativeCoroutines](https://github.com/rickclephas/KMP-NativeCoroutines)

View file

@ -22,7 +22,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.compose
kotlinCompilerExtensionVersion = Versions.composeCompiler
}
buildTypes {
@ -64,7 +64,12 @@ dependencies {
implementation(activityCompose)
}
with(Deps.Glance) {
implementation(appwidget)
}
with(Deps.Compose) {
implementation(compiler)
implementation(ui)
implementation(uiGraphics)
implementation(foundationLayout)

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.surrus.peopleinspace">
<uses-permission android:name="android.permission.INTERNET" />
<application
@ -24,6 +23,31 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name="com.surrus.peopleinspace.glance.PeopleInSpaceWidgetReceiver"
android:label="People in Space"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
<receiver android:name="com.surrus.peopleinspace.glance.ISSMapWidgetReceiver"
android:label="ISS Map"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/iss_widget_info" />
</receiver>
</application>
</manifest>

View file

@ -1,26 +1,33 @@
package com.surrus.peopleinspace
import android.app.Application
import co.touchlab.kermit.Kermit
import co.touchlab.kermit.Logger
import com.surrus.common.di.initKoin
import com.surrus.peopleinspace.di.appModule
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.logger.Level
import org.osmdroid.config.Configuration
import java.io.File
class PeopleInSpaceApplication : Application(), KoinComponent {
private val logger: Kermit by inject()
class PeopleInSpaceApplication : Application() {
override fun onCreate() {
super.onCreate()
// needed for osmandroid
Configuration.getInstance().userAgentValue = BuildConfig.APPLICATION_ID
Configuration.getInstance().osmdroidTileCache = File(cacheDir, "osm").also {
it.mkdir()
}
initKoin {
androidLogger()
// https://github.com/InsertKoinIO/koin/issues/1188
androidLogger(if (BuildConfig.DEBUG) Level.ERROR else Level.NONE)
androidContext(this@PeopleInSpaceApplication)
modules(appModule)
}
logger.d { "PeopleInSpaceApplication" }
Logger.d { "PeopleInSpaceApplication" }
}
}

View file

@ -0,0 +1,106 @@
package com.surrus.peopleinspace.glance
import android.graphics.Bitmap
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.action.actionStartActivity
import androidx.glance.action.clickable
import androidx.glance.background
import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import com.surrus.common.repository.PeopleInSpaceRepositoryInterface
import com.surrus.peopleinspace.R
import com.surrus.peopleinspace.glance.util.BaseGlanceAppWidget
import com.surrus.peopleinspace.ui.MainActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.inject
import org.osmdroid.tileprovider.MapTileProviderBasic
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.Projection
import org.osmdroid.views.drawing.MapSnapshot
import org.osmdroid.views.overlay.IconOverlay
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ISSMapWidget : BaseGlanceAppWidget<ISSMapWidget.Data>() {
val repository: PeopleInSpaceRepositoryInterface by inject()
data class Data(val bitmap: Bitmap?)
override suspend fun loadData(): Data {
val issPosition = repository.pollISSPosition().first()
val issPositionPoint = GeoPoint(issPosition.latitude, issPosition.longitude)
val stationMarker = IconOverlay(
issPositionPoint,
context.resources.getDrawable(R.drawable.ic_iss, context.theme)
)
val source = TileSourceFactory.DEFAULT_TILE_SOURCE
val projection = Projection(5.0, 480, 240, issPositionPoint, 0f, true, false, 0, 0)
val bitmap = withContext(Dispatchers.Main) {
suspendCoroutine<Bitmap> { cont ->
val mapSnapshot = MapSnapshot(
{
if (it.status == MapSnapshot.Status.CANVAS_OK) {
val bitmap = Bitmap.createBitmap(it.bitmap)
cont.resume(bitmap)
}
},
MapSnapshot.INCLUDE_FLAG_UPTODATE or MapSnapshot.INCLUDE_FLAG_SCALED,
MapTileProviderBasic(context, source, null),
listOf(stationMarker),
projection
)
launch(Dispatchers.IO) {
mapSnapshot.run()
}
}
}
return Data(bitmap)
}
@OptIn(ExperimentalUnitApi::class)
@Composable
override fun Content(data: Data?) {
Box(
modifier = GlanceModifier.background(Color.DarkGray).fillMaxSize().clickable(
actionStartActivity<MainActivity>()
)
) {
val bitmap = data?.bitmap
if (bitmap != null) {
Image(
modifier = GlanceModifier.fillMaxSize(),
provider = ImageProvider(bitmap),
contentDescription = "ISS Location"
)
} else {
Text(
"Loading ISS Map...",
style = TextStyle(
color = ColorProvider(Color.White),
fontSize = TextUnit(20f, TextUnitType.Sp)
)
)
}
}
}
}

View file

@ -0,0 +1,7 @@
package com.surrus.peopleinspace.glance
import com.surrus.peopleinspace.glance.util.BaseGlanceAppWidgetReceiver
class ISSMapWidgetReceiver : BaseGlanceAppWidgetReceiver<ISSMapWidget>() {
override fun createWidget(): ISSMapWidget = ISSMapWidget()
}

View file

@ -0,0 +1,65 @@
package com.surrus.peopleinspace.glance
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.background
import androidx.glance.layout.Row
import androidx.glance.layout.padding
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import com.surrus.common.remote.Assignment
import com.surrus.common.repository.PeopleInSpaceRepositoryInterface
import com.surrus.peopleinspace.glance.util.BaseGlanceAppWidget
import kotlinx.coroutines.flow.first
import org.koin.core.component.inject
class PeopleInSpaceWidget : BaseGlanceAppWidget<PeopleInSpaceWidget.Data>() {
val repository: PeopleInSpaceRepositoryInterface by inject()
data class Data(val people: List<Assignment>)
override suspend fun loadData(): Data {
return Data(repository.fetchPeopleAsFlow().first())
}
@OptIn(ExperimentalUnitApi::class)
@Composable
override fun Content(data: Data?) {
LazyColumn(
modifier = GlanceModifier.background(Color.DarkGray).padding(horizontal = 8.dp)
) {
item {
Text(
modifier = GlanceModifier.padding(bottom = 8.dp),
text = "People in Space",
style = TextStyle(
color = ColorProvider(Color.White),
fontSize = TextUnit(12f, TextUnitType.Sp),
fontWeight = FontWeight.Bold
)
)
}
if (data != null) {
items(data.people.size) {
Row {
Text(
text = data.people[it].name,
style = TextStyle(
color = ColorProvider(Color.White),
fontSize = TextUnit(10f, TextUnitType.Sp)
)
)
}
}
}
}
}
}

View file

@ -0,0 +1,7 @@
package com.surrus.peopleinspace.glance
import com.surrus.peopleinspace.glance.util.BaseGlanceAppWidgetReceiver
class PeopleInSpaceWidgetReceiver : BaseGlanceAppWidgetReceiver<PeopleInSpaceWidget>() {
override fun createWidget(): PeopleInSpaceWidget = PeopleInSpaceWidget()
}

View file

@ -0,0 +1,54 @@
package com.surrus.peopleinspace.glance.util
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.unit.DpSize
import androidx.glance.GlanceId
import androidx.glance.LocalGlanceId
import androidx.glance.LocalSize
import androidx.glance.appwidget.GlanceAppWidget
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
abstract class BaseGlanceAppWidget<T>(initialData: T? = null) : GlanceAppWidget(), KoinComponent {
val context: Context by inject()
var glanceId by mutableStateOf<GlanceId?>(null)
var size by mutableStateOf<DpSize?>(null)
var data by mutableStateOf<T?>(initialData)
private val coroutineScope = MainScope()
abstract suspend fun loadData(): T
fun initiateLoad() {
coroutineScope.launch {
data = loadData()
val currentGlanceId = snapshotFlow { glanceId }.filterNotNull().firstOrNull()
if (currentGlanceId != null) {
update(context, currentGlanceId)
}
}
}
@Composable
override fun Content() {
glanceId = LocalGlanceId.current
size = LocalSize.current
Content(data)
}
@Composable
abstract fun Content(data: T?)
}

View file

@ -0,0 +1,17 @@
package com.surrus.peopleinspace.glance.util
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import org.koin.core.component.KoinComponent
abstract class BaseGlanceAppWidgetReceiver<T : BaseGlanceAppWidget<*>> : GlanceAppWidgetReceiver(),
KoinComponent {
override val glanceAppWidget: GlanceAppWidget
get() {
return createWidget().apply {
this.initiateLoad()
}
}
abstract fun createWidget(): T
}

View file

@ -5,7 +5,11 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.material.*
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Person
@ -14,22 +18,16 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import com.google.accompanist.navigation.animation.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.surrus.common.remote.Assignment
import com.surrus.peopleinspace.BuildConfig
import org.osmdroid.config.Configuration
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// needed for osmandroid
Configuration.getInstance().userAgentValue = BuildConfig.APPLICATION_ID
setContent {
MainLayout()
}
@ -97,11 +95,11 @@ fun MainLayout() {
AnimatedNavHost(navController, startDestination = Screen.PersonList.title) {
composable(
route = Screen.PersonList.title,
exitTransition = { _, _ ->
exitTransition = {
slideOutHorizontally() +
fadeOut(animationSpec = tween(1000))
},
popEnterTransition = { _, _ ->
popEnterTransition = {
slideInHorizontally()
}
) {
@ -114,11 +112,11 @@ fun MainLayout() {
}
composable(
route = Screen.PersonDetails.title + "/{person}",
enterTransition = { _, _ ->
enterTransition = {
slideInHorizontally() +
fadeIn(animationSpec = tween(1000))
},
popExitTransition = { _, _ ->
popExitTransition = {
slideOutHorizontally()
}
) { backStackEntry ->

View file

@ -2,9 +2,7 @@ package com.surrus.peopleinspace.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Kermit
import com.surrus.common.remote.Assignment
import com.surrus.common.repository.PeopleInSpaceRepository
import com.surrus.common.repository.PeopleInSpaceRepositoryInterface
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn

View file

@ -0,0 +1,263 @@
<vector android:height="50.6dp" android:viewportHeight="847"
android:viewportWidth="385" android:width="23dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#BF9586" android:pathData="M88.188,10.5c0,9.583 0,19.167 0,28.75c29.532,-2.404 59.044,-4.063 88.626,-5.874c21.633,-1.324 67.814,-12.872 82.985,9.115c10.227,14.821 9.438,27.326 10.052,45.106c0.222,6.446 1.379,10.841 4.587,16.528c3.953,7.008 3.604,14.493 3.875,22.438c0.521,15.255 1.72,30.486 2.303,45.75c0.208,5.47 -1.598,16.568 0.822,21.563c2.621,5.41 12.739,8.2 17.65,11.33c6.127,3.905 9.348,10.715 13.413,16.671c3.825,5.605 9.62,10.142 13.437,15.999c7.843,12.035 11.888,26.538 18.242,39.403c13.548,27.425 22.34,52.58 16.52,83.551c-3.112,16.566 -10.924,25.477 -25.137,34.796c-5.239,3.435 -11.405,5.852 -17,8.707c-11.009,5.618 -10.466,6.807 -13.011,19.07c-3.189,15.369 -3.989,29.261 -3.989,44.959c0,12.234 8.642,27.777 12.46,39.571c9.394,29.011 16.033,59.139 22.958,88.842c3.271,14.033 3.362,29.792 4.534,44.159c1.112,13.652 7.868,26.241 9.832,39.771c1.001,6.899 3.091,14.62 3.091,21.608c0,8.577 -4.215,14.454 -3.375,22.436c1.776,16.891 9.226,27.403 19.287,40.269c5.157,6.595 11.396,30.684 7.088,38.608c-4.814,8.856 -30.952,14.176 -39.981,14.711c-8.112,0.48 -33.541,5.416 -33.901,-9.797c-0.282,-11.894 1.398,-25.152 -3.806,-36.204c-3.239,-6.878 -7.119,-13.728 -9.812,-20.837c-0.796,-2.102 0.088,-5.656 -1.984,-6.881c-3.258,-1.925 -5.235,-3.384 -7.789,-6.138c-9.567,-10.316 -14.569,-22.602 -19.337,-35.637c-4.508,-12.326 -13.262,-20.086 -13.885,-33.96c-0.559,-12.43 -10.433,-27.2 -15.835,-38.135c-5.704,-11.543 -9.188,-23.077 -11.713,-35.598c-1.412,-7.006 -3.737,-36.459 -11.332,-37.025c-1.829,15.894 -9.9,28.164 -12.611,43.704c-2.472,14.173 -3.737,27.892 -8.165,41.726c-2.421,7.563 0.887,11.288 0.273,18.595c-0.583,6.955 -0.649,14.153 -1.935,21.021c-2.729,14.577 -2.847,34.049 -9.001,47.491c-2.575,5.626 -4.624,14.581 -9.435,18.583c-9.454,7.864 -10.506,7.491 -9.125,19.567c0.862,7.539 1.563,15.033 2.141,22.601c0.697,9.13 -3.316,12.024 -9.445,18.65c-4.756,5.142 -7.461,12.823 -10.742,18.965c-3.082,5.769 -9.683,8.14 -15.578,10.221c-15.008,5.297 -28.782,5.051 -43.969,1.501c-4.272,-0.998 -5.2,-13.176 -5.532,-16.374c-0.886,-8.555 3.324,-13.692 7.392,-20.966c6.636,-11.864 10.248,-26.148 10.784,-39.561c0.146,-3.629 3.242,-15.388 1.947,-17.978c-3.64,-7.282 -5.106,-14.659 -5.747,-22.873c-1.13,-14.474 3,-28.828 3,-43.437c0,-14.442 2.25,-28.59 2.25,-42.97c0,-7.57 1.08,-15.837 -0.792,-23.217c-1.78,-7.019 -3.084,-14.493 -3.084,-21.755c0,-7.729 -0.71,-15.801 0,-23.495c0.62,-6.719 3.626,-13.327 3.626,-20.093c0,-15.694 0.143,-31.326 0.795,-47.008c0.614,-14.763 -6.546,-27.976 -5.171,-43.399c-3.574,4.958 -11.04,11.074 -8.795,17.115c2.02,5.438 -0.339,14.252 -1.64,19.95c-1.521,6.66 -4.761,14.174 -3.457,20.984c1.647,8.603 3.966,17.055 3.767,25.823c-0.166,7.296 -7.125,12.766 -7.125,19.241c0,8.876 -1.774,15.393 -4.259,23.982c-3.871,13.38 -19.129,21.46 -31.678,13.653c-13.762,-8.562 -14.932,-27.48 -18.42,-41.704c-1.693,-6.903 -2.893,-12.319 -2.893,-19.389c0,-6.342 5.089,-13.196 1.981,-19.604c-6.162,-12.703 -4.996,-31.832 -4.763,-45.896c0.1,-6.003 3.176,-11.979 3.97,-18.035c0.935,-7.132 0.442,-14.378 1.375,-21.479c1.939,-14.755 5.937,-29.42 6.063,-44.394c0.128,-15.263 -2.336,-28.45 1.25,-43.688c3.438,-14.61 4.556,-29.692 7.874,-44.312c1.698,-7.481 4.188,-14.859 6.269,-22.245c1.666,-5.911 7.521,-12.521 7.144,-18.688c-1.003,-16.431 -1.937,-32.863 -2.843,-49.299c-0.348,-6.318 2.657,-12.763 4.863,-18.512c2.016,-5.254 7.648,-10.292 7.314,-15.942c-0.982,-16.627 -1.959,-33.251 -3.083,-49.869c-0.932,-13.789 -5.834,-29.325 -3.97,-42.996c1.909,-13.999 10.471,-30.212 22.963,-37.397c3.423,-1.969 5.491,-2.645 9.403,-3.317c4.609,-0.792 2.816,-4.521 2.816,-8.901c0,-6.844 1.874,-16.853 -3.626,-22.209l1.126,-4.75h5L88.188,10.5z"/>
<path android:fillColor="#FFFFFF"
android:pathData="M177.313,498.377c3.891,-8.237 18.606,-4.169 26.527,-5.556c10.382,-1.819 19.325,-4.771 27.657,-11.667c7.175,-5.938 -16.429,-47.24 -19.811,-54.777c6.543,3.951 9.538,8.634 12.523,15.771c2.493,5.961 5.974,20.196 12.352,22.729c0,-10.295 -4.481,-17.948 -8.625,-27.377c3.917,-0.467 7.863,3.155 11.246,2.227c5.679,-1.559 7.76,-3.639 12.191,-7.663c7.106,-6.454 13.804,-13.243 20.563,-20.063c0.441,1.995 1.883,3.798 2.5,5.75c3.133,-1.061 6.125,-3.39 6.125,-6.873c0,4.334 -1.616,9.537 2.75,11.873c1.851,-3.701 3.917,-7.244 5,-11.25c1.517,5.222 -4.055,10.545 0,14.377c5.224,-4.229 5.066,-8.646 6.125,-15c-0.317,3.897 -3.38,15.785 0.125,17.687c5.361,2.907 0.382,15.139 -0.615,19.367c-1.517,6.43 -0.135,14.62 -0.135,21.219c0,6.054 -7.09,6.888 -12.125,8.478c1.385,-0.958 6.356,-6.029 2.5,-7.25c-1.548,-0.49 -7.915,5.095 -9.759,6.148c-3.383,1.933 -14.245,13.37 -4.429,11.163c7.908,-1.778 15.495,-5.342 23.075,-8.179c3.038,-1.137 7.907,13.503 9.362,16.617c-3.059,-1.284 -6.43,-1.805 -9.375,0c4.008,3.644 12.242,7.717 14,12.5c-5.283,-3.123 -14.578,-8.825 -20.75,-8.377c4.056,6.576 10.599,11.533 16.625,16.627c-8.388,-4.934 -23.594,-19.377 -33.188,-19.377c-3.955,0 -2.864,5.656 0.299,6.483c3.513,0.918 5.207,1.913 8.206,3.951c6.023,4.095 12.039,8.201 18.081,12.27c6.495,4.373 11.003,8.115 16.267,13.912c3.728,4.106 4.69,13.689 6.354,19.058c4.104,13.237 6.519,27.179 9.203,40.81c2.797,14.204 3.536,28.693 4.874,43.103c1.258,13.558 5.737,26.809 8.273,40.227c1.354,7.158 -2.989,6.807 -9.259,10.271c-7.018,3.879 -13.942,8.076 -21.111,11.667c-5.182,2.596 -11.945,6.9 -17.648,7.836c-4.267,0.699 -8.534,1.399 -12.802,2.099c-3.108,0.51 -2.841,-2.927 -6.05,-4.185c5.146,-7.459 1.767,-10.418 -6.125,-8c4.462,-1.58 13.732,-2.528 14.5,-7.5c-4.609,0 -9.266,-0.434 -13.765,0.616c-6.292,1.468 -6.918,-1.119 -10.505,-6.446c-6.276,-9.323 -2.648,-20.737 -7.933,-30.986c-9.508,-18.438 -19.429,-37.207 -24.644,-57.264c-2.403,-9.243 -3.474,-18.876 -5.092,-28.293c-0.976,-5.677 -0.207,-10.939 -5.304,-13.852c-2.14,-1.223 -11.521,-1.083 -11.875,-1.685c-5.846,-9.91 -11.865,-19.611 -18.103,-29.28C180.422,514.295 173.869,508.319 177.313,498.377z"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M107.813,452.377c0.662,7.818 7.573,-0.051 8.726,-3.574c0.799,-2.442 1.288,-4.989 2.836,-7.053c2.458,-3.276 4.482,3.172 7.312,2.627c7.851,-1.511 -3.45,-7.495 -3.354,-9.566c0.284,-6.145 0.569,-12.289 0.854,-18.434c2.78,6.672 2.48,12.478 10.5,8.623c-3.242,4.682 -1.119,10.25 0.87,14.983c3.115,7.413 2.651,11.308 11.006,11.644c1.936,0.078 5.023,0.665 6.589,-0.56c1.792,-1.4 4.864,-5.16 7.196,-4.544c4.365,1.153 11.735,7.631 15.839,3.354c2,-2.084 3.791,-14.872 4.785,-17.856c0.68,-2.038 -8.49,-9.621 -10.066,-12.06c-1.281,-1.982 -5.233,-6.089 -5.469,-8.461c-0.143,-1.433 2.902,-5.493 3.313,-7.5c0.985,-4.813 0.791,-11.56 -2.813,-15.25c5.115,-0.759 7.511,5.146 11.219,8.021c1.634,1.267 4.62,0.256 6.583,0.077c2.212,-0.202 4.081,3.232 5.452,4.75c6.738,7.461 10.967,14.387 15.487,23.228c4.245,8.302 29.737,51.044 17.677,55.779c-10.206,4.008 -20.369,6.146 -31.418,6.895c-4.441,0.301 -11.047,-1.76 -13.882,2.82c-2.647,4.276 -4.859,8.164 -5.242,13.243c-0.809,10.721 8.768,21.343 13.965,30.005c1.933,3.221 2.885,8.118 5.118,10.839c2.93,3.568 11.071,9.678 8.541,14.593c-4.434,-5.911 -8.677,-11.821 -12.783,-17.962c-2.179,-3.259 -14.341,-20.88 -10.114,-7.29c2.198,7.066 9.06,14.296 13.09,20.451c3.501,5.346 11.229,10.92 9.308,16.928c-6.502,-9.036 -11.682,-18.474 -21,-24.627c3.358,12.636 14.217,20.889 17.375,33.5c-3.501,-4.587 -6.176,-8.608 -10.499,-12.5c-1.248,6.69 3.76,14.123 7.499,19.377c-1.96,-3.109 -4.616,-5.467 -8.125,-3.25c5.959,13.073 4.768,27.96 0.872,42.041c-1.857,6.712 -5.293,13.104 -6.872,19.832c-1.673,7.131 1.418,13.344 1.155,20.528c-0.247,6.77 -0.717,12.667 -2.028,19.276c-1.235,6.218 -1.24,21.544 -5.251,26.445c-3.897,4.762 -14.587,3.338 -20.116,4.233c-7.723,1.251 -14.852,-0.356 -21.822,-0.356c-7.143,0 -14.781,-1.926 -21.355,-4.609c-8.343,-3.406 -10.384,-2.273 -8.957,-12.268c0.641,-4.491 1.877,-4.549 0.583,-10.379c-0.837,-3.766 -0.972,-5.803 -0.521,-9.621c0.898,-7.59 0.721,-15.146 1.673,-22.76c3.626,-28.988 -3.875,-56.686 -2.924,-85.688c0.253,-7.728 3.439,-15.249 3.439,-22.974c0,-12.982 0,-25.965 0,-38.947c0,-6.902 1.742,-16.386 -1.205,-22.505c-1.918,-3.984 -1.741,-9.742 -2.387,-14.091c-0.631,-4.241 3.594,-7.54 4.092,-11.908C93.697,461.048 103.875,453.231 107.813,452.377z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M132.813,333.126c-1.377,1.042 -3.415,1.537 -5,2.25c-0.477,-4.906 2.776,-7.961 3.874,-12.5c1.239,-5.12 0.422,-11.619 0.613,-16.866c0.174,-4.789 2.954,-26.351 -4.487,-26.26c0,6.742 -0.165,13.177 -0.876,19.876c-3.87,-14.32 -6.795,-35.584 -18,-46.25c-3.251,6.761 3.301,13.887 5.626,20c2.451,6.443 3.513,13.119 4.624,19.874c-1.381,-2.466 -3.023,-7.722 -5.25,-9.374c-3.273,-2.428 -5.79,-2.383 -3.766,2.44c3.456,8.235 7.302,16.302 9.642,24.934c-2.102,-2.208 -3.236,-1.818 -5.876,-2.25c-0.565,5.697 2.9,8.813 6.137,12.991c3.923,5.065 2.678,6.908 0.183,12.579c-4.49,10.2 -10.363,16.436 -12.793,27.579c-2.231,10.23 -7.96,19.427 -9.838,29.664c-2.069,11.28 -3.754,22.756 -6.446,33.963c-2.229,9.274 -6.165,18.1 -9.796,26.93c-2.084,5.068 -2.956,13.282 -8.696,13.544c-0.276,0.013 -12.788,-5.338 -13.25,-5.873c-5.348,-6.194 4.288,-25.591 -13.435,-18.857c-3.059,1.163 0.01,11.859 -2.505,12.31c-6.145,1.099 -12.29,2.198 -18.435,3.298c0,-5.784 0.638,-57.877 6.587,-57.877c9.175,0 18.76,0.362 27.495,3.414c4.817,1.684 9.912,4.216 14.918,5.213c3.249,0.647 8.238,-0.475 10.874,1.874c-3.423,-9.255 -21.051,-11.329 -29.124,-13.75c4.075,0 8.588,1.183 11.374,-2.5c-6.398,-3.075 -11.749,-2.5 -18.819,-2.5c-4.27,0 -14.445,2.682 -15.394,-0.554c-3.928,-13.395 -4.401,-27.219 -1.752,-40.956c5.139,-26.644 9.172,-52.179 17.139,-78.176c3.878,-12.655 14.55,-21.844 23.331,-31.039c4.184,-4.382 8.964,-7.804 13.773,-11.508c8.58,-6.609 9.221,-2.749 17.926,3.738c11.201,8.347 22.465,16.016 34.449,23.163c12.914,7.701 20.818,10.249 36.224,11.083c-12.943,2.275 -24.94,0.499 -33.912,11.121c-3.4,4.025 -1.588,11.855 -1.588,16.885c0,7.691 -0.261,15.433 0,23.12c0.208,6.112 0.973,12.208 1.254,18.319c0.435,9.464 3.937,12.673 9.786,20.248c1.064,1.378 3.858,0.235 4.433,1.983c1.005,3.058 1.729,6.416 3.207,9.282c3.239,6.282 4.753,12.585 7.571,19.041c-4.104,-2.364 -10.85,-11.873 -14.676,-11.873c-7.175,0 -12.04,4.398 -14.418,11.101c-1.937,5.458 -4.214,13.646 -2.613,19.308c1.266,4.478 10.759,14.278 5.457,16.092c-1.305,-4.268 -4.018,-7.016 -6.88,-10.279c-2.933,-3.344 0.063,-8.275 1.13,-12.348c-2.425,4.903 -4.489,10.206 -7.75,14.627c2.404,-14.487 4.463,-28.65 8.874,-42.627C135.533,365.693 148.464,330.283 132.813,333.126z"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M353.313,317.876c1.832,14.859 3.971,28.936 -0.25,43.5c-0.702,-2.944 -4.053,-7.742 -6.875,-4.5c-1.129,1.297 1.151,10.045 1.375,12c-2.174,-0.616 -4.152,0.03 -6.375,-1.126c-2.104,4.608 2.525,7.184 0,12.126c-0.68,-2.45 -2.335,-3.571 -3.625,-5.5c-4.204,5.386 -11.125,11.691 -17.5,14.373c-2.054,0.864 -4.669,-0.852 -6.398,1.311c-2.552,3.192 -1.175,6.221 -2.727,9.816c-6.025,-17.198 -6.989,4.204 -10.5,2.123c-3.62,-2.146 -4.851,-22.551 -5.579,-27.03c-1.564,-9.627 -7.46,-17.153 -12.957,-24.864c-2.509,-3.52 -5.083,-4.657 -2.065,-8.729c4.117,-5.557 -0.709,-8.31 -4.773,-12.375c11.021,-8.733 21.879,-16.624 36.75,-16.624C324.855,312.376 340.307,315.567 353.313,317.876z"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M191.688,268.626c0.824,-4.412 6.249,-3.299 9.875,-3.688c6.561,-0.703 8.555,-0.776 13.602,-4.946c6.745,-5.572 11.088,-13.126 15.472,-20.508c7.49,-12.61 23.45,-32.097 23.677,-46.984c11.12,4.083 21.685,7.713 32.022,13.537c9.611,5.414 13.216,11.935 20.7,19.775c7.4,7.753 13.906,16.94 18.408,26.654c2.926,6.315 -2.733,7.168 -8.166,10.033c-3.432,1.81 1.683,3.314 3.16,3.377c2.066,0.088 6.101,-3.146 8,-4.126c8.592,16.958 18.058,33.171 24.375,51.126c-9.041,-1.292 -18.043,-2.087 -27.125,-3c2.667,-0.542 5.333,-1.084 8,-1.626c-0.65,-5.397 -4.565,-3.559 -8.625,-3.124c3.603,-3.119 11.877,-5.827 8.125,-11.25c-11.156,5.758 -19.748,11.736 -32.002,13.98c-11.9,2.179 -20.854,9.082 -30.623,15.894c-1.335,-5.062 -4.372,-11.1 -1.899,-16.228c2.215,-4.592 9.656,-11.707 7.399,-16.146c-2.416,1.037 -4.528,2.822 -6,5c1.511,-6.764 11.443,-16.543 9.625,-22.5c-3.486,2.988 -6.497,5.697 -9.125,9.5c1.062,-6.274 5.02,-14.374 4.5,-20.5c-3.922,5.566 -10.846,13.542 -10.875,20.5c0,-5.667 0,-11.333 0,-17c-5.916,5.442 -4.375,10.362 -4.375,18.127c0,4.881 -5.006,9.649 -2.269,14.014c4.45,7.097 3.448,11.124 5.269,19.109c2.285,10.025 2.921,8.471 -4.625,16.624c0.096,-4.889 -0.421,-24.648 -3.931,-26.403c-6.937,-3.467 -6.933,-3.38 -7.93,-11.051c-0.732,-5.631 0.687,-9.145 -5.45,-9.651c-7.456,-0.615 -8.826,0.764 -11.097,-6.358c-1.392,-4.367 -2.84,-8.533 -6.843,-10.536C215.637,266.597 199.988,269.058 191.688,268.626z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M183.438,49.5c28.987,10.7 50.989,28.876 56.738,60.335c3.149,17.238 6.86,34.485 7.762,52.041c0.603,11.734 4.246,34.477 -4.75,44c-0.281,-1.839 -1.717,-2.813 -2.25,-4.25c-7.757,8.229 -40.307,33.065 -24.875,44.624c1.245,-10.528 2.029,-12.242 9.759,-19.536c5.454,-5.147 10.141,-11.35 15.116,-16.964c-6.548,11.136 -13.096,22.271 -19.645,33.407c-2.135,3.631 -4.359,6.318 -7.203,9.496c-4.473,4.999 -5.262,3.827 -11.902,3.847c19.361,-22.924 -16.904,-7.617 -23.75,-14.124c23.412,-7.863 39.81,-17.38 54.407,-37.95c1.781,-2.51 9.767,-9.809 10.231,-12.291c1.107,-5.904 0.747,-9.426 0.603,-15.089c-0.319,-12.538 0.029,-24.979 -2.308,-37.308c-2.008,-10.596 -3.277,-23.825 -11.621,-31.613c-7.361,-6.871 -19.631,-11.975 -29.118,-16.362c-2.632,-1.217 -9.318,-2.141 -12.405,-2.576c-7.768,-1.095 -15.536,-2.189 -23.303,-3.285c-12.811,-1.806 -20.455,0.426 -31.61,6.223C145.881,71.442 160.3,57.479 183.438,49.5z"
android:strokeColor="#FFFFFF" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M263.688,100.626c10.691,9.33 8.421,31.461 9.375,44.374c1.189,16.093 2.32,32.37 1.375,48.626c-6.082,-2.493 -19.98,-4.598 -20.262,-11.707c-0.351,-8.869 -0.683,-17.675 -1.499,-26.509c-1.669,-18.071 -3.048,-38.176 -8.989,-55.284C247.552,99.189 262.908,95.396 263.688,100.626z"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#FFFFFF"
android:pathData="M258.188,57.5c6.447,5.976 4.33,20.602 4.649,28.304c0.484,11.693 -12.542,2.919 -19.649,8.822c-6.668,-16.568 -17.521,-28.595 -31.5,-39.876C227.354,53.681 243.08,52.465 258.188,57.5z"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#FF000000"
android:pathData="M257.563,52c-7.697,-4.972 -21.434,-5.167 -30.287,-4.662c-7.394,0.421 -14.788,0.843 -22.182,1.265c-1.857,0.105 -1.174,1.945 -3.306,1.204c-3.408,-1.186 -6.816,-2.371 -10.226,-3.557c-5.022,-1.747 -9.637,-4 -14.956,-4c-3.829,0 -16.521,-2.151 -19.231,0.563c-7.443,7.456 -24.992,2.195 -31.812,11.937c-3.743,-6.065 -34.692,1.03 -42.014,1.695c-5.863,0.533 -9.539,2.182 -14.353,5.444c-2.847,1.928 -8.524,8.728 -9.759,11.987c2.96,-2.054 4.959,-5.319 7.5,-2.5c13.106,-12.117 32.913,-9.998 50,-9.126C106.819,74.13 97.212,84.725 92.369,99.497c-4.368,13.321 -8.306,26.859 -8.306,40.865c0,9.299 0,18.597 0,27.896c0,2.938 -15.077,2.649 -18.346,2.501c-7.472,-0.339 -6.03,-33.322 -6.03,-40.07c0,-29.073 -13.36,-56.9 13.241,-78.785c7.922,-6.518 29.192,-7.702 39.31,-8C127.46,43.453 142.655,42 157.871,42c17.499,0 35.412,-1.459 52.82,-3.215c10.194,-1.028 19.386,-0.614 29.639,0.095C250.125,39.558 252.475,44.219 257.563,52z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#13132D" android:pathData="M181.469,199.969c-3,-14 -19,-21 -24,-33c-8,-20 8,-40 14,-59c1.493,-5.226 -0.358,-10.452 -2.641,-15.678c-27.291,-1.158 -54.426,11.526 -67.488,37.658c-5.551,11.104 -0.515,34.702 0.201,46.712c0.804,13.501 9.675,23.861 19.556,32.79c11.491,10.383 20.909,20.316 35.873,24.058c7.745,1.936 14.34,2.294 20.688,1.397C179.28,223.531 184.563,212.349 181.469,199.969z"/>
<path android:fillColor="#363844" android:pathData="M236.417,154.423c-0.198,-12.696 0.148,-30.628 -10.512,-39.381c-12.791,-10.502 -24.972,-15.782 -40.842,-20.416c-5.345,-1.336 -10.793,-2.105 -16.236,-2.336c2.283,5.226 4.134,10.452 2.641,15.678c-6,19 -22,39 -14,59c5,12 21,19 24,33c3.095,12.38 -2.188,23.563 -3.813,34.937c7.68,-1.084 14.995,-4.011 23.531,-8.279c11.265,-5.632 23.966,-17.232 29.502,-28.776C237.611,183.419 236.664,170.254 236.417,154.423z"/>
<path android:fillColor="#FF000000"
android:pathData="M85.688,183.376c0.359,6.385 0.815,12.622 1.616,18.961c1.215,9.622 4.201,14.072 -3.203,20.291c-10.597,8.901 -19.846,19.709 -30.538,28.248c-1.719,-15.686 -5.664,-36.644 -0.699,-51.879c2.588,-7.943 5.519,-17.118 13.823,-20.871c2.048,-0.926 7.945,-1.376 10.375,-1.376C84.563,176.75 84.43,175.962 85.688,183.376z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M255.063,336.5c-12.07,7.293 -20.179,20.825 -33.509,26.438c-9.586,4.036 -17.755,24.561 -17.616,33.811c-7.695,-11.629 -3.978,-14.751 -0.375,-27.373c2.067,-7.245 0.225,-7.114 7.995,-8.032c7.362,-0.87 7.504,-1.348 12.692,-5.156c6.993,-5.132 7.232,-16.739 6.968,-24.278c-0.153,-4.39 -1.995,-12.023 -0.093,-16.097c1.298,-2.78 9.348,-4.055 12.063,-6.813C244.295,323.301 243.141,328.274 255.063,336.5z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M132.438,338.376c8.165,11.792 -3.332,31.704 -6.647,43.722c-4.535,16.438 -6.791,31.377 -8.853,48.401c0,-0.917 0,-1.833 0,-2.75c-5.878,2.924 -1.167,12.209 -5.435,15.963c-4.354,3.83 -10.554,3.804 -14.439,8.664c3.046,-3.877 12.355,-10.994 6.624,-15.5c-3.233,2.685 -6.736,5.103 -9.124,8.623c-0.542,-5.344 7.121,-12.93 10.048,-17.321c1.284,-1.926 4.568,-5.782 1.077,-7.052c-2.894,-1.052 -6.337,5.749 -8.125,8c-0.058,-4.182 12.552,-20.025 3.374,-17.127c2.589,-3.02 5.354,-5.36 8.063,-8.062c3.381,-3.373 -4.685,-3.078 -6.937,-0.562c1.932,-5.644 9.653,-8.011 11.624,-13.438c2.667,-7.343 -8.53,0.596 -10.5,2.438c2.005,-1.845 9.974,-6.61 10.75,-8.877c1.919,-5.603 -4.753,-4.012 -7.5,-1.123c-0.665,-6.499 8.108,-4.54 11.126,-9.127c5.165,-7.85 -3.121,-3.91 -6.876,-2.5c0.385,-3.74 19.824,-21.497 10.126,-20.499C123.098,347.333 130.067,336.511 132.438,338.376z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M101.813,196.75c10.273,12.271 19.525,22.736 31.922,32.786c9.674,7.842 30.274,15.593 42.702,14.214c-7.783,0.819 -15.567,1.639 -23.35,2.458c-1.069,0.112 -1.095,7.995 -1.209,9.301c-0.166,1.898 4.905,2.894 6.309,3.117c12.667,2.01 29.866,-1.376 42.875,-1.376c-11.194,1.023 -22.192,2.31 -33.427,2.034c-5.077,-0.125 -9.909,0.582 -14.424,-1.769c-2.445,-1.273 -15.292,-6.039 -15.433,-8.66c-0.318,-5.904 1.103,-9.957 -4.026,-13.054c-5.556,-3.354 -10.113,-6.211 -14.767,-10.669C109.949,216.475 101.813,209.615 101.813,196.75z"
android:strokeColor="#FFFFFF" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000" android:pathData="M97.563,190.876c0,6.226 0,12.453 0,18.679c0,3.119 9.819,11.409 11.679,13.47c3.544,3.928 8.143,7.421 12.445,10.476c7.465,5.299 10.569,4.526 11.376,13.876c-10.746,-6.686 -19.382,-12.845 -28.601,-21.472c-3.767,-3.524 -6.993,-6.896 -10.255,-10.891c-2.863,-3.506 -2.014,-13.691 -2.523,-18.145c-2.441,-21.336 -0.747,-42.723 -0.747,-63.674c0,-23.747 10.692,-45.102 25.614,-62.82c15.217,-18.069 38.202,-21.749 60.512,-21.749c-14.345,4.632 -25.385,13.759 -35.847,24.221c-5.155,5.155 -8.588,14.156 -11.946,20.523c-2.347,4.448 -12.167,9.425 -16.225,13.381c-6.28,6.124 -10.519,11.005 -15.108,18.439c-4.029,6.525 -3.124,11.899 -3.124,19.621C94.813,160.476 95.864,175.327 97.563,190.876z"/>
<path android:fillColor="#A89187"
android:pathData="M85.688,247.376c-2.899,3.708 -5.82,-0.186 -9.124,-2.25C79.075,240.626 83.718,244.005 85.688,247.376z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M302.938,246.5c2.284,4.423 4.454,8.922 6.949,13.231c0.517,0.893 9.24,14.781 4.176,13.645c-2.995,-0.672 -15.817,-25.346 -15.25,-29.876C300.852,243.487 301.84,244.675 302.938,246.5z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M78.813,252.376c1.833,2.292 3.667,4.583 5.5,6.874c-0.708,-0.667 -1.417,-1.333 -2.126,-2c-4.151,3.751 -9.45,-4.85 -13.874,-6.874C70.348,245.576 76.249,249.985 78.813,252.376z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M201.938,249.5c-2.667,0.292 -5.333,0.584 -8,0.876C196.566,249.066 199.438,248.129 201.938,249.5z"
android:strokeColor="#FF9100" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M185.313,251.25c-2.837,-0.36 -5.645,-0.811 -8.5,-0.874c4.716,-0.166 9.405,-0.25 14.124,-0.25C189.066,250.5 187.196,250.945 185.313,251.25z"
android:strokeColor="#FF0000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M74.938,258.626c4.221,3.685 9.273,6.971 11.626,12.25c-9.548,-1.025 -15.959,-13.899 -26.626,-15C62.749,249.834 71.068,256.304 74.938,258.626z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M294.063,283.626c-1.86,4.494 -5.976,3.954 -3.433,-0.832c3.575,-6.73 5.245,-7.908 12.058,-11.418C301.957,276.394 297.258,279.967 294.063,283.626z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M206.438,277.25c-6.504,5.153 1.116,12.832 6.875,13c11.788,0.344 5.397,-5.984 7.125,-13.874c7.675,9.903 3.375,36.119 3.375,48.148c0,11.538 -0.197,16.738 -5.379,26.899c-1.858,3.645 -7.438,1.953 -11.219,1.953c-3.793,0 -4.625,-3.119 -6.152,-6.376c3.936,-0.354 7.872,-0.708 11.808,-1.062c2.611,-0.234 3.362,-6.438 0.237,-6.438c-8.761,0 -17.635,-0.435 -26.383,0.018c-8.762,0.453 -17.528,0.85 -26.287,1.358c-2.444,0.142 -8.262,-0.793 -9.374,2.25c-1.53,4.187 3.642,5.674 6.374,4.374c-0.941,7.436 -6.627,2.822 -9.124,-1.624c-3.179,-5.662 -1.998,-14.208 -2.348,-20.46c-0.718,-12.808 -1.881,-25.226 -1.044,-38.017c0.295,-4.513 -0.415,-10.647 6.014,-11.055c5.992,-0.38 12.11,-1.197 18.114,-1.077c14.296,0.287 28.592,0.573 42.889,0.859C210.111,276.5 208.28,276.973 206.438,277.25z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M330.063,281.626c-7.801,2.065 -14.775,6.5 -22.125,8.624C306.098,282.555 328.329,270.151 330.063,281.626z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M187.063,287.75c0.456,9.423 1.028,18.781 1.826,28.181c0.325,3.843 -18.59,5.104 -20.452,3.319c-3.411,-3.269 -1.124,-23.851 -1.124,-29.52C167.313,283.207 182.992,285.344 187.063,287.75z"
android:strokeColor="#A89187" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#00FFFF" android:pathData="M183.938,313.25c-3.667,0.25 -7.333,0.5 -11,0.75c-0.324,-7.449 -0.861,-14.917 -0.874,-22.374c5.086,0.236 10.624,-1.876 10.926,3.801C183.306,301.368 183.621,307.31 183.938,313.25z"/>
<path android:fillColor="#13007C"
android:pathData="M240.438,295.75c-4.033,1.78 -9.745,4.139 -8.875,9.376c-0.438,-1.813 -3.396,-8.88 -0.977,-9.669C233.794,294.412 239.348,291.82 240.438,295.75z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M160.438,305.126c2.341,5.195 1.194,12.482 1.362,18.151c0.188,6.352 4.15,14.599 -5.195,14.599c-6.529,0 -6.112,-30.577 -1.167,-35.25C156.868,304.685 158.271,304.126 160.438,305.126z"
android:strokeColor="#A89187" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M100.938,314.626c4.542,4.25 9.084,8.5 13.626,12.75c-2.396,-2.428 -4.744,-0.617 -7.5,-2c-2.879,-1.445 -5.72,-5.646 -8.016,-7.911c-3.885,-3.834 -16.117,-7.539 -15.234,-13.339C90.469,304.522 95.922,310.814 100.938,314.626z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M240.438,308.75c-0.458,0.375 -0.917,0.75 -1.375,1.126c-0.842,-1.17 -1.16,-1.971 -1.125,-3.376c1.125,0 2.25,0 3.375,0C241.021,307.25 240.729,308 240.438,308.75z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#6CFF00" android:pathData="M157.438,334.25c-5.526,-4.073 -2.617,-19.78 -2.75,-26.25C159.959,307.818 161.703,330.906 157.438,334.25z"/>
<path android:fillColor="#FF000000"
android:pathData="M250.063,322.376c-1.535,-2.114 -1.201,-4.872 -0.5,-7.5C249.899,317.364 250.067,319.87 250.063,322.376z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M313.188,333.126c0.198,1.239 0,2.615 0,3.874c-2.983,-2.688 -17.137,-11.68 -18,-15.25C293.536,314.919 312.031,331.788 313.188,333.126z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M64.438,330.126c-2.18,9.125 -2.329,27.813 -7.75,34.874c-3.957,-3.67 1.789,-21.867 2.607,-27.301c0.565,-3.756 0.688,-11.658 2.893,-14.573C67.064,316.678 64.911,327.942 64.438,330.126z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M189.813,323.75c0.578,5.492 2.899,8.989 -3.057,9.369c-1.688,0.108 -16.451,1.859 -16.735,0.706c-0.962,-3.899 -4.211,-12.347 2.96,-11.983C178.031,322.098 185.606,320.915 189.813,323.75z"
android:strokeColor="#A89187" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#00000000"
android:pathData="M173.438,328.75c3.06,-2.572 8.379,-1.214 12.5,-1.124C181.771,328.001 177.604,328.376 173.438,328.75z"
android:strokeColor="#FF0000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M103.688,337.25c3.458,5.849 -5.57,2.727 -5.25,-1.624C100.19,335.818 102.189,336.276 103.688,337.25z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFF00" android:pathData="M259.563,348.376c7.59,5.087 16.046,14.193 19.531,22.644c1.269,3.077 9.177,34.404 0.094,29.356c-5.037,-2.8 0.027,-21.725 -4.438,-27.534c-7.332,-9.54 -14.344,-15.845 -23.813,-23.342C253.541,344.552 255.862,344.098 259.563,348.376z"/>
<path android:fillColor="#13007C"
android:pathData="M200.563,358.876c-2.454,8.548 -5.058,15.983 -8.875,24c-2.937,-9.811 -17.504,-10.767 -19.624,0c-4.822,-5.033 -5.566,-10.553 -8.565,-16.639c-5.107,-10.366 -2.742,-7.805 1.627,-17.424c1.242,-2.734 16.699,-1.563 20.495,-1.563C196.289,347.25 195.563,349.386 200.563,358.876z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M277.563,336.5c-5.898,6.193 -4.518,6.5 0,13.75c3.227,5.179 7.723,10.085 9.848,15.829c4.428,11.968 8.423,23.756 6.652,36.547c-7.801,1.036 -5.098,-9.271 -5.234,-14.393c-0.167,-6.297 -4.068,-13.798 -5.918,-19.82c-3.323,-10.815 -13.645,-20.853 -22.348,-27.537c3.386,-2.913 6.771,-5.825 10.158,-8.738C273.422,329.814 275.447,334.287 277.563,336.5z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M260.313,367.75c4.569,7.092 8.239,12.441 10.227,20.592c0.796,3.262 3.246,13.1 0.854,15.621c-10.066,10.612 -20.093,26.737 -35.456,26.787c-9.021,0.029 -11.832,-7.586 -17.508,-12.816c-2.075,-1.912 -5.421,0.273 -8.18,-1.934c-3.102,-2.48 -4.067,-4.908 -1.604,-8.466c3.421,-4.941 -2.849,-12.047 2.167,-18.784c1.154,-1.551 16.55,-11.327 5.75,-10.75c2.772,-7.785 13.679,-12.316 20,-16.873C247.326,353.368 250.879,359.859 260.313,367.75z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M187.813,386c-1.686,2.064 -13.877,7.631 -11.274,0.979C178.616,381.671 185.527,377.577 187.813,386z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M335.563,384.376c-0.708,0.333 -1.417,0.667 -2.125,1c0.04,-2.644 -0.199,-2.056 1.625,-3.877C335.396,382.434 335.3,383.484 335.563,384.376z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M153.563,386.75c7.177,4.215 10.385,10.581 8.25,18.627c-0.931,3.508 -4.848,4.997 -2.021,8.81c2.966,4 5.933,8 8.898,12c3.103,4.185 8.78,15.929 -0.504,16.813c-3.04,0.29 -6.964,-3.873 -10.74,-3.873c-3.669,0 -6.358,5.126 -9.76,5.25c-4.806,0.175 -11.27,-9.867 -7.175,-14.499c3.208,-3.629 14.637,-12.92 9.113,-18.439c-3.267,-3.264 -7.083,1.137 -9.688,-4.189c-1.47,-3.005 0.455,-10.817 0.599,-14.234C140.797,386.836 150.813,376.118 153.563,386.75z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M327.063,389.876c-1.093,-0.67 -3.414,0.649 -0.625,-2.25C326.646,388.376 326.854,389.126 327.063,389.876z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M198.938,398.75c1.292,2.375 2.583,4.751 3.875,7.127c-4.046,-4.716 -6.986,-9.549 -10,-14.877C194.854,393.583 196.896,396.166 198.938,398.75z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M235.938,410.877c0.77,1.706 4.544,7.634 3.375,9.75c-2.836,5.135 -6.79,-7.477 -7.135,-8.308c-1.887,-4.545 -8.157,-17.23 -2.49,-19.943C232.261,398.487 234.114,404.493 235.938,410.877z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M208.063,444.877c6.279,10.938 2.696,27.903 -12.523,20.301C186.373,460.599 196.059,434.559 208.063,444.877z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M205.813,452.127c-0.415,4.371 -5.919,13.81 -9.125,5.5C193.979,450.608 201.321,448.549 205.813,452.127z"
android:strokeColor="#FFFFFF" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M145.438,466.5c5.49,1.89 2.685,-2.333 5.626,-4.123c3.564,-2.169 5.859,-2.323 10.859,-2.684c8.859,-0.639 7.837,3.99 6.265,12.307c2.626,-4.195 12.711,-11 16.876,-7.75c4.48,3.495 -8.841,6.465 -10.476,10.39c-3.299,7.92 2.161,15.263 -10.365,14.085c-7.714,-0.726 -8.892,-9.377 -2.409,-12.848c-1.833,-1.626 -3.667,-3.251 -5.5,-4.877c-1.651,5.008 -2.901,15.814 -10.713,11.757c-7.489,-3.891 -15.028,-7.236 -22.794,-10.547c-4.938,-2.104 0.573,-11.096 3.194,-13.021C129.924,456.306 141.905,464.779 145.438,466.5z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M66.688,479.5c5.025,3.038 7.616,4.401 5.464,10.858c-1.75,5.251 -2.041,10.639 -3.838,15.769c-7.489,-3.137 -13.58,-6.435 -21.598,-2.684c-7.499,3.507 -15.022,6.804 -22.652,9.307c-0.806,-5.367 -7.504,-38.609 -2.341,-39.877C37.626,468.968 51.769,474.47 66.688,479.5z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M225.813,493.127c7.112,5.575 -17.545,7.534 -19.528,7.798c-5.713,0.76 -22.345,4.553 -26.471,-1.111c-3.587,-4.925 17.508,-2.536 19.956,-2.817C206.316,496.244 220.97,491.704 225.813,493.127z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M196.438,506.377c-3.442,4.375 -12.568,5.338 -16.124,0.873C183.83,502.563 192.15,503.856 196.438,506.377z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M65.188,512.5c5.698,5.89 10.519,29.75 3.876,35.627c-0.344,-3.045 0.308,-12.207 -1.626,-14.377c-2.802,-3.143 -3.66,0.448 -3.451,2.975c0.83,10.039 0.852,18.942 0.201,28.902c-0.611,9.343 -1.701,19.31 -8.895,26.025c-8.796,8.211 -16.757,-1.5 -21.457,-8.601c-10.636,-16.074 -11.455,-37.119 -8.901,-55.342C26.875,513.867 53.676,504.028 65.188,512.5z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M190.563,516.627c-1.925,0.501 -11.222,-1.199 -6.874,-3.612C187.177,511.078 192.1,511.25 190.563,516.627z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M347.563,706.127c2.827,10.743 -24.355,21.701 -31.46,25.866c-5.481,3.215 -15.369,2.255 -21.613,2.919c-8.831,0.939 -10.492,-2.626 -16.427,-9.162c10.097,-7.609 24.412,-6.044 35.649,-11.23c7.77,-3.586 15.539,-7.172 23.309,-10.758C344.429,700.343 345.8,695.744 347.563,706.127z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF"
android:pathData="M107.063,728c2.812,0.779 5.708,1.041 8.5,0.25c0.254,3.753 23.388,8.174 25.75,5c4.42,5.243 8.703,0.566 13.624,0c1.673,-0.192 2.848,1.589 4.7,1.084c2.86,-0.781 5.351,-1.591 8.3,-1.584c-2.406,1.721 -2.468,2.997 -2.13,5.622c0.274,2.121 -3.735,3.727 -5.37,4.878c1.19,0.577 0.997,0.959 2.5,1.377c-5.047,5.384 -8.459,10.25 -15.844,10.25c-7.569,0 -15.8,-0.318 -23.156,-2.25c-6.461,-1.696 -14.617,-6.329 -19.652,-10.525c-3.844,-3.203 -10.049,-14.414 -7.722,-19.102C100.25,724.44 103.964,725.494 107.063,728z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M344.813,729.127c1.183,14.487 6.975,23.606 16.148,34.457c7.267,8.595 9.12,21.742 10.137,31.998c1.016,10.245 -27.745,13.067 -34.418,14.313c-12.13,2.265 -26.692,8.847 -25.367,-10.768c2.989,5.224 6.408,-1.427 9.41,-4.219c3.716,-3.457 12.509,-4.174 17.283,-5.562c4.608,-1.34 11.135,-4.383 15.932,-4.597c3.506,-0.156 9.207,4.035 11.25,1.127c7.558,-10.758 -27.36,-3.568 -30.514,-2.739c-6.93,1.821 -20.75,6.063 -22.861,14.112c-3.152,-4.81 -1.522,-12.932 -1.599,-18.576c-0.074,-5.439 -4.552,-8.488 -0.026,-13.297c-2.284,-0.462 -3.976,-1.593 -6.375,-1.377c2.567,-1.798 5.897,-1.922 5.5,-6.123c-3.174,-0.105 -6.572,0.672 -8.875,-1.627c2.47,-2.102 8,-0.46 8,-4.562c0,-1.918 -8.538,-1.552 -10.5,-3.438c2.265,-0.387 18.64,-0.854 16.125,-6.123c-3.014,-6.312 -15.192,4.396 -18.875,-1.877c7.186,-0.666 18.631,0.42 25.187,-3.3C325.761,733.895 341.331,720.169 344.813,729.127z"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M160.438,737.127c0.726,-0.258 1.461,-1.174 3,-0.75C162.438,736.627 161.438,736.877 160.438,737.127z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M123.313,761.25c7.675,0.866 15.349,1.731 23.024,2.598c3.334,0.376 4.321,14.507 5.1,18.402c3.142,15.709 -9.428,26.272 -16.621,39.235c-6.638,11.964 -21.567,15.892 -34.894,15.892c-7.624,0 -15.038,0.83 -18.359,-7.5c-2.55,-6.397 -0.152,-16.521 5,-21c-0.157,1.568 0.511,2.913 0.5,4.373c3.155,-5.297 7.952,-2.895 12.905,-3.705c7.833,-1.282 13.483,-3.006 20.438,1.344c2.335,1.46 2.588,-3.633 1.087,-4.952c-2.813,-2.473 -4.873,-3.85 -8.676,-3.751c-7.495,0.194 -14.904,0.518 -22.38,1.064c-0.392,-8.586 9.178,-18.335 9.626,-27.873c1.909,1.315 6.971,1.857 6.75,-2c-0.057,-0.988 -5.069,-4.182 -6.126,-5.25c0.747,-0.223 14.673,2.827 7.75,-3.25c4.56,1.721 10.91,4.372 14.126,-0.877c-5.984,-2.372 -21.022,-4.47 -19.626,-13.873C109.831,754.79 115.474,758.555 123.313,761.25z"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF" android:pathData="M207.813,145.5c13.659,-2.167 10.148,-31.902 -0.939,-28.458C195.919,120.445 195.34,143.716 207.813,145.5z"/>
<path android:fillColor="#FA002A"
android:pathData="M153.813,404c4.07,-9.431 -1.446,-6.081 -4.75,-14.123c-3.146,3.039 -3.12,6.077 -3.406,10.125C145.302,405.048 149.279,403.527 153.813,404z"
android:strokeColor="#FA002A" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FA7800" android:pathData="M153.563,423.877c6.251,0 18.406,4.306 15.124,13c-10.103,-4.718 -12.256,-5.389 -21.5,0.873C142.788,433.468 150.714,427.534 153.563,423.877z"/>
<path android:fillColor="#FFFFFF" android:pathData="M63.04,523.027c0,0 -0.218,-0.135 -0.626,-0.388c-0.396,-0.275 -1.006,-0.624 -1.75,-1.019c-0.752,-0.344 -1.784,-0.754 -2.639,-0.543c-0.399,0.146 -0.739,0.391 -1.023,0.866c-0.273,0.477 -0.493,1.068 -0.642,1.752c-0.314,1.352 -0.434,2.944 -0.481,4.633c-0.041,1.693 -0.014,3.501 0.044,5.378c0.08,1.876 0.176,3.823 0.321,5.804c0.172,1.987 0.347,4.007 0.522,6.025c0.087,1.074 0.061,2.041 0.097,3.063l0.032,1.509c-0.014,0.506 -0.028,1.01 -0.042,1.513c-0.039,1.004 -0.042,2.003 -0.11,2.982c-0.074,0.978 -0.146,1.943 -0.218,2.893c-0.176,1.896 -0.382,3.725 -0.617,5.45c-0.247,1.726 -0.473,3.356 -0.738,4.854c-0.484,3 -0.973,5.482 -1.297,7.224c-0.333,1.74 -0.523,2.733 -0.523,2.733c-0.004,0.02 -0.022,0.032 -0.042,0.028c-0.019,-0.003 -0.031,-0.021 -0.028,-0.04c0,0 0.158,-1 0.434,-2.749c0.266,-1.75 0.673,-4.247 1.058,-7.256c0.215,-1.501 0.387,-3.135 0.576,-4.86c0.178,-1.726 0.322,-3.551 0.435,-5.438c0.041,-0.943 0.081,-1.903 0.122,-2.875c0.035,-0.971 0.006,-1.949 0.012,-2.937c-0.002,-0.493 -0.004,-0.988 -0.007,-1.485l-0.081,-1.506c-0.067,-0.991 -0.081,-2.053 -0.192,-2.993c-0.528,-3.981 -1.245,-8.004 -1.585,-11.82c-0.182,-1.909 -0.274,-3.771 -0.216,-5.555c0.063,-1.784 0.246,-3.49 0.727,-5.071c0.215,-0.79 0.596,-1.547 1.082,-2.221c0.484,-0.689 1.327,-1.19 2.125,-1.292c0.403,-0.029 0.785,-0.035 1.111,0.069c0.167,0.039 0.334,0.077 0.486,0.128c0.141,0.066 0.279,0.132 0.415,0.196c0.559,0.23 0.947,0.575 1.334,0.838c0.715,0.61 1.203,1.115 1.501,1.489c0.308,0.369 0.473,0.565 0.473,0.565c0.013,0.016 0.011,0.038 -0.004,0.051C63.07,523.034 63.053,523.035 63.04,523.027z"/>
<path android:fillColor="#FFFFFF" android:pathData="M248.966,363.022c0,0 -0.143,0.494 -0.503,1.273c-0.359,0.782 -0.916,1.855 -1.723,3.055c-0.405,0.601 -0.889,1.218 -1.399,1.872c-0.509,0.653 -1.099,1.304 -1.738,1.949c-0.647,0.64 -1.287,1.32 -2.011,1.949c-0.716,0.635 -1.425,1.292 -2.122,1.958c-0.734,0.628 -1.469,1.256 -2.191,1.874c-0.702,0.64 -1.497,1.145 -2.209,1.697c-0.724,0.541 -1.42,1.064 -2.129,1.491c-0.695,0.445 -1.352,0.866 -1.958,1.255c-0.596,0.403 -1.192,0.692 -1.693,0.991c-0.508,0.288 -0.95,0.539 -1.314,0.745c-0.729,0.413 -1.145,0.648 -1.145,0.648c-0.017,0.01 -0.039,0.004 -0.049,-0.013c-0.009,-0.017 -0.002,-0.039 0.014,-0.048c0,0 0.4,-0.261 1.102,-0.718c0.351,-0.228 0.775,-0.504 1.264,-0.822c0.48,-0.328 1.055,-0.649 1.616,-1.096c0.572,-0.428 1.192,-0.892 1.848,-1.383c0.669,-0.472 1.319,-1.039 1.993,-1.623c0.662,-0.596 1.402,-1.144 2.047,-1.823c0.663,-0.655 1.337,-1.321 2.01,-1.987c0.629,-0.711 1.245,-1.432 1.835,-2.153c0.6,-0.711 1.109,-1.488 1.658,-2.183c1.099,-1.39 2.111,-2.705 3.1,-3.735c0.503,-0.507 0.945,-1.003 1.384,-1.398c0.422,-0.41 0.821,-0.743 1.152,-1.015c0.664,-0.544 1.106,-0.806 1.106,-0.806c0.019,-0.011 0.042,-0.005 0.053,0.014C248.968,363.001 248.969,363.013 248.966,363.022z"/>
<path android:fillColor="#00000000"
android:pathData="M337.563,739.377c3.495,12.533 8.209,19.954 16.625,29.873"
android:strokeColor="#FFFFFF" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#00000000"
android:pathData="M360.188,792.75c-5.732,-1.196 -9.619,0.341 -15.125,2"
android:strokeColor="#FFFFFF" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#00000000"
android:pathData="M137.438,771.5c-0.549,11.256 -4.222,15.88 -10.5,25.377"
android:strokeColor="#FFFFFF" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#00000000"
android:pathData="M113.688,816.877c-6.847,2.125 -12.803,2.576 -20,3"
android:strokeColor="#FFFFFF" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFFFF" android:pathData="M250.813,403.876c0,0 -0.246,-0.283 -0.62,-0.8c-0.377,-0.515 -0.88,-1.265 -1.434,-2.183c-1.104,-1.838 -2.333,-4.381 -3.33,-7.011c-0.861,-2.711 -1.486,-5.505 -1.76,-7.644c-0.137,-1.07 -0.205,-1.97 -0.226,-2.606c-0.025,-0.635 -0.006,-1.006 -0.006,-1.006s0.232,0.3 0.578,0.838c0.35,0.537 0.813,1.313 1.313,2.254c0.999,1.884 2.071,4.453 2.907,7.105c0.993,2.631 1.755,5.351 2.143,7.459c0.193,1.054 0.313,1.949 0.37,2.584C250.81,403.502 250.813,403.876 250.813,403.876z"/>
<path android:fillColor="#FA002A" android:pathData="M275.813,259.75c2.225,-4.67 23.463,-28.569 21.125,-31C291.535,223.132 265.829,261.555 275.813,259.75z"/>
<path android:fillColor="#FA002A" android:pathData="M279.188,221.626c1.113,-2.504 7.331,-6.185 6.625,-1.376c-0.468,3.186 -6.914,7.801 -8.875,10.5c-3.701,5.094 -8.024,19.159 -14.125,20.5C260.807,241.297 272.867,228.612 279.188,221.626z"/>
<path android:fillColor="#FA002A" android:pathData="M271.688,218c5.358,-5.668 10.277,-1.495 3.375,3.626C270.036,225.356 266.084,221.832 271.688,218z"/>
<path android:fillColor="#FA002A" android:pathData="M262.563,221c0.733,-2.185 11.817,-15.949 13.875,-10.25C277.19,212.836 264.699,221.63 262.563,221z"/>
<path android:fillColor="#FA002A" android:pathData="M257.063,218.5c1.981,-3.302 7.674,-15.482 12.375,-10.75C265.533,210.375 260.92,220.092 257.063,218.5z"/>
<path android:fillColor="#1300C6" android:pathData="M255.938,220.5c5.788,4.081 13.771,2.381 13.25,9.626c-0.026,0.37 -8.12,15.954 -8.625,16.374c-3.659,3.046 -14.066,-2.89 -13.62,-7.8C247.461,232.996 250.542,223.237 255.938,220.5z"/>
<path android:fillColor="#FA002A" android:pathData="M289.938,224.626c0.333,5.91 -7.832,12.249 -11.391,16.565c-3.909,4.741 -5.481,12.765 -11.234,15.309C267.316,249.658 281.768,220.788 289.938,224.626z"/>
<path android:fillColor="#A89187"
android:pathData="M103.438,326.75c3.49,2.737 7.772,4.977 8.25,9.75c-6.138,-1.255 -25.76,-11.779 -24.25,-18.374C93.332,318.604 98.615,323.664 103.438,326.75z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M56.688,385.126c-2.159,1.787 -18.553,5.835 -19.124,2.25C36.463,380.464 55.263,383.531 56.688,385.126z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A60053"
android:pathData="M78.813,394.25c-2.641,0.338 1.361,-0.095 2.25,0C80.313,394.25 79.563,394.25 78.813,394.25z"
android:strokeColor="#A60053" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FFFF00" android:pathData="M53.563,463.25c5.965,4.16 13.117,6.081 18.874,10.5c-5.903,1.747 -17.396,-4.874 -23.671,-6.508c-9.498,-2.473 -17.147,-1.08 -26.453,-0.742c1.038,-5.998 14.309,-4.932 19.44,-4.529c12.097,0.948 3.525,-14.243 11.06,-13.971C53.063,453.083 53.313,458.167 53.563,463.25z"/>
<path android:fillColor="#A89187"
android:pathData="M232.938,503.877c-1.235,9.531 -4.108,19.102 -5.75,28.623c-0.8,4.638 -0.996,10.029 -2.5,14.5c-0.903,2.686 -2.414,7.929 -5.25,1.127c-2.6,-6.236 4.209,-23.522 5.433,-29.439C225.71,514.622 229.626,495.624 232.938,503.877z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M240.188,505.5c0.49,18.999 -8.947,34.421 -10.5,52c0,-9.837 1.354,-19.467 1.851,-29.383C231.729,524.29 234.769,501.56 240.188,505.5z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M133.938,509.127c5.188,1.984 12.112,3.215 16,7.25c-4.437,3.969 -12.418,-0.811 -17.147,-2.577c-6.249,-2.335 -13.143,-3.05 -19.729,-4.385c-6.679,-1.354 -8.324,-4.569 -0.247,-4.165C120.03,505.61 126.891,507.542 133.938,509.127z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M247.313,506.377c0.537,4.927 -3.634,26.399 -7.125,18.5C238.668,521.438 242.461,503.3 247.313,506.377z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M127.188,517.127c0.583,0.541 1.167,1.082 1.75,1.623c-5.068,1.583 -18.704,0.165 -21.624,-4.373C113.616,511.233 121.182,514.888 127.188,517.127z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M119.813,524.877c0.844,6.79 -9.675,3.093 -10,-2.25C113.197,523.043 117.344,522.659 119.813,524.877z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M271.688,572c0.773,8.227 1.547,16.453 2.32,24.68c0.567,6.04 1.542,1.898 3.805,5.447c1.54,2.416 5.701,3.513 2.75,6.873c-0.929,-1.015 -2.388,-1.538 -3.375,-2.5c-0.857,3.351 0.093,5.071 3.375,4.25c-0.667,0.459 -1.333,0.918 -2,1.377c2.004,2.838 7.75,5.175 7.75,8.437c0,2.019 -8.258,4.788 -0.991,7.484c6.181,2.293 2.54,8.87 -2.073,5.052c-3.792,-3.139 -8.391,-7.059 -9.295,-11.989c-1.661,-9.063 -12.095,-62.388 -5.016,-65.983C269.854,560.751 270.771,566.376 271.688,572z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M143.813,612.377c-4.079,5.317 -11.072,6.564 -15.924,10.877c-4.354,3.87 -10.099,12.56 -15.576,8.996c2.428,-6.34 8.99,-11.623 14.68,-15.012c5.541,-3.3 4.569,-11.064 9.32,-12.861c0.277,2.033 1.548,3.96 1.75,6c2.81,-2.87 2.101,-13.25 4.375,-13.25C145.29,597.127 143.813,609.424 143.813,612.377z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M172.313,655c-0.79,4.546 -32.291,7.431 -35.626,4.627c5.006,-5.458 5.655,-5.415 15.571,-6.095C156.253,653.259 170.864,649.868 172.313,655z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M321.563,661c-2.592,3.908 -5.184,7.815 -7.775,11.724c-1.862,2.807 3.057,4.037 2.15,6.026c-2.131,4.678 -18.698,5.782 -23.62,7.209c-4.669,1.353 -2.869,-4.493 -1.093,-6.432c2.537,-2.768 8.138,-2.279 11.449,-4.583c7.477,-5.201 12.119,-11.943 18.014,-18.817C319.493,658.676 321.263,658.806 321.563,661z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M155.438,665.25c-0.743,5.374 -13.75,7.711 -13.75,1.375c0,-6.806 12.909,-2.51 16.25,-1.375C157.104,665.25 156.271,665.25 155.438,665.25z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#A89187"
android:pathData="M309.313,690.127c-1.826,1.37 -12.029,4.19 -10.5,-0.25C299.974,686.507 308.207,687.872 309.313,690.127z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M344.813,692c-11.668,5.957 -22.197,12.215 -34.367,17.402c-4.347,1.853 -34.706,13.177 -37.07,7.036c-1.405,-3.65 14.092,-3.136 16.281,-3.277c5.316,-0.343 14.506,-2.691 19.035,-5.502c11.026,-6.844 23.721,-13.549 35.496,-19.159C344.766,689.629 344.917,690.744 344.813,692z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000"
android:pathData="M102.563,714.5c17.159,-2.251 32.889,15.131 49.874,4.127c-0.667,0.75 -1.333,1.5 -2,2.25c5.008,0.152 14.498,-0.661 19.376,-3.127c-1.728,2.915 0.97,9.614 -1.861,10.357c-4.976,1.305 -9.879,2.143 -15.027,2.143c-4.826,0 -9.651,0 -14.477,0c-4.904,0 -8.795,-2 -13.506,-2c-6.384,0 -15.344,-4.258 -21.505,-7.373c-4.171,-2.109 -9.427,-1.75 -8.754,-7.307C95.183,709.453 100.844,712.602 102.563,714.5z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.126"/>
<path android:fillColor="#FF000000" android:pathData="M88.849,22.891C88.601,17.992 89.917,6.615 83,5.75c-0.421,-0.053 -0.896,0.28 -1.087,0.633c-1.324,2.432 -0.769,4.769 -0.324,7.343c0.67,3.875 0.502,7.938 0.817,11.854c0.632,7.834 2.416,15.559 2.938,23.42c0.226,3.407 5.161,3.444 5.313,0C91.046,40.131 89.292,31.678 88.849,22.891z"/>
<path android:fillColor="#CBFF00" android:pathData="M207.5,283a5.75,5.5 0,1 0,11.5 0a5.75,5.5 0,1 0,-11.5 0z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="64dp" android:viewportHeight="401.294"
android:viewportWidth="401.294" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M115.342,188.015c-7.025,0 -12.741,5.716 -12.741,12.741s5.716,12.741 12.741,12.741c7.026,0 12.742,-5.716 12.742,-12.741S122.368,188.015 115.342,188.015zM115.342,205.497c-2.614,0 -4.741,-2.127 -4.741,-4.741s2.127,-4.741 4.741,-4.741c2.615,0 4.742,2.127 4.742,4.741S117.957,205.497 115.342,205.497z"/>
<path android:fillColor="#FF000000" android:pathData="M397.294,164.141c2.209,0 4,-1.791 4,-4V89.215c0,-2.209 -1.791,-4 -4,-4h-33.219c-2.209,0 -4,1.791 -4,4v70.926c0,2.209 1.791,4 4,4h12.61v8.999h-36.205c-6.008,-4.585 -13.401,-7.094 -20.986,-7.094c-17.762,0 -32.434,13.455 -34.376,30.706h-3.421v-9.79c0,-2.209 -1.791,-4 -4,-4h-28.993l-6.173,-11.125c-0.705,-1.271 -2.044,-2.059 -3.498,-2.059h-39.513v-6.524h16.196c2.209,0 4,-1.791 4,-4V73.281c0,-2.209 -1.791,-4 -4,-4h-40.266c-2.209,0 -4,1.791 -4,4v85.972c0,2.209 1.791,4 4,4h16.069v6.524h-38.468c-2.209,0 -4,1.791 -4,4v22.974h-4.52c-1.97,-15.047 -14.865,-26.705 -30.44,-26.705c-10.313,0 -19.703,5.125 -25.326,13.331c-5.623,-8.206 -15.012,-13.331 -25.326,-13.331H44.653v-5.905h6.324c2.209,0 4,-1.791 4,-4V89.215c0,-2.209 -1.791,-4 -4,-4H17.758c-2.209,0 -4,1.791 -4,4v70.926c0,2.209 1.791,4 4,4h6.325v5.905H4c-2.209,0 -4,1.791 -4,4v53.419c0,2.209 1.791,4 4,4h20.083v5.905h-6.325c-2.209,0 -4,1.791 -4,4v70.927c0,2.209 1.791,4 4,4h33.219c2.209,0 4,-1.791 4,-4v-70.927c0,-2.209 -1.791,-4 -4,-4h-6.325v-5.905h18.789c10.313,0 19.703,-5.125 25.326,-13.331c5.624,8.205 15.013,13.331 25.326,13.331c15.578,0 28.476,-11.663 30.441,-26.714h4.518v22.765c0,2.209 1.791,4 4,4h38.468v6.524h-16.069c-2.209,0 -4,1.791 -4,4v85.972c0,2.209 1.791,4 4,4h40.266c2.209,0 4,-1.791 4,-4V242.04c0,-2.209 -1.791,-4 -4,-4h-16.196v-6.524h39.513c1.454,0 2.792,-0.789 3.498,-2.059l6.173,-11.125h28.993c2.209,0 4,-1.791 4,-4v-9.581h3.447c2.037,17.151 16.66,30.497 34.35,30.497c7.581,0 14.972,-2.507 20.978,-7.087h36.214v9.209h-12.61c-2.209,0 -4,1.791 -4,4v70.927c0,2.209 1.791,4 4,4h33.219c2.209,0 4,-1.791 4,-4v-70.927c0,-2.209 -1.791,-4 -4,-4h-12.609v-9.209h12.435c2.209,0 4,-1.791 4,-4V177.14c0,-2.209 -1.791,-4 -4,-4h-12.435v-8.999H397.294zM199.521,155.253v-13.221h12.196v13.221H199.521zM191.521,112.267h-12.069V98.502h12.069V112.267zM199.521,98.502h12.196v13.765h-12.196V98.502zM191.521,120.267v13.765h-12.069v-13.765H191.521zM199.521,120.267h12.196v13.765h-12.196V120.267zM211.717,90.502h-12.196V77.281h12.196V90.502zM191.521,77.281v13.221h-12.069V77.281H191.521zM179.451,155.253v-13.221h12.069v13.221H179.451zM24.689,223.466v-45.419h22.038v45.419H24.689zM54.727,178.046h6.641v45.419h-6.641V178.046zM46.977,128.678v9.956H21.758v-9.956H46.977zM21.758,120.678v-9.957h25.219v9.957H21.758zM46.977,93.215v9.507H21.758v-9.507H46.977zM21.758,146.634h25.219v9.507H21.758V146.634zM32.083,164.141h4.569v5.905h-4.569V164.141zM8,178.046h8.689v45.419H8V178.046zM21.758,272.834v-9.956h25.219v9.956H21.758zM46.977,280.834v9.956H21.758v-9.956H46.977zM21.758,308.298v-9.507h25.219v9.507H21.758zM46.977,254.878H21.758v-9.507h25.219V254.878zM36.652,237.371h-4.569v-5.905h4.569V237.371zM69.368,222.667v-43.822c6.952,1.876 12.703,6.983 15.31,13.918v15.987C82.071,215.684 76.32,220.791 69.368,222.667zM114.093,223.466c-9.664,0 -18.222,-6.091 -21.415,-15.184v-15.05c3.193,-9.094 11.751,-15.185 21.415,-15.185c12.522,0 22.709,10.188 22.709,22.71C136.802,213.278 126.615,223.466 114.093,223.466zM191.521,246.04v13.221h-12.069V246.04H191.521zM199.521,289.027h12.196v13.765h-12.196V289.027zM191.521,302.792h-12.069v-13.765h12.069V302.792zM199.521,281.027v-13.765h12.196v13.765H199.521zM191.521,281.027h-12.069v-13.765h12.069V281.027zM179.451,310.792h12.069v13.221h-12.069V310.792zM199.521,324.012v-13.221h12.196v13.221H199.521zM211.717,246.04v13.221h-12.196V246.04H211.717zM157.052,177.778h11.046v45.739h-11.046V177.778zM236.678,223.516h-60.58v-45.739h60.58l5.311,9.571v26.596L236.678,223.516zM273.696,210.332h-23.707v-19.371h23.707V210.332zM368.075,272.834v-9.956h25.219v9.956H368.075zM393.294,280.834v9.956h-25.219v-9.956H393.294zM368.075,308.298v-9.507h25.219v9.507H368.075zM393.294,254.878h-25.219v-9.507h25.219V254.878zM292.892,200.647c0,-8.817 4.315,-16.641 10.94,-21.484v42.968C297.207,217.288 292.892,209.463 292.892,200.647zM319.493,227.248c-2.663,0 -5.234,-0.398 -7.661,-1.129v-50.944c2.428,-0.731 4.998,-1.129 7.661,-1.129c6.217,0 12.267,2.193 17.036,6.175c0.248,0.208 0.521,0.368 0.803,0.506v39.849c-0.29,0.141 -0.57,0.304 -0.821,0.513C331.747,225.061 325.703,227.248 319.493,227.248zM393.119,220.161h-47.787V181.14h47.787V220.161zM393.294,128.678v9.956h-25.219v-9.956H393.294zM368.075,120.678v-9.957h25.219v9.957H368.075zM393.294,93.215v9.507h-25.219v-9.507H393.294zM368.075,146.634h25.219v9.507h-25.219V146.634z"/>
</vector>

View file

@ -0,0 +1,6 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="80dp"
android:minHeight="80dp"
android:targetCellWidth="4"
android:targetCellHeight="2"
android:resizeMode="none" />

View file

@ -0,0 +1,10 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="80dp"
android:minHeight="80dp"
android:targetCellWidth="2"
android:targetCellHeight="2"
android:minResizeWidth="40dp"
android:minResizeHeight="40dp"
android:maxResizeWidth="120dp"
android:maxResizeHeight="120dp"
android:resizeMode="horizontal|vertical" />

View file

@ -16,8 +16,9 @@ dependencies {
with(Deps.Ktor) {
implementation(serverCore)
implementation(serverNetty)
implementation(serialization)
implementation(websockets)
implementation(serverContentNegotiation)
implementation(json)
}
with(Deps.Log) {

View file

@ -26,9 +26,16 @@ val personImages = mapOf(
"Anton Shkaplerov" to "https://alchetron.com/cdn/anton-shkaplerov-799e7545-54ae-4145-83d1-18f906d22b1-resize-750.jpeg",
"Klim Shipenko" to "https://upload.wikimedia.org/wikipedia/pt/f/f9/Shipenko_klim.jpg",
"Yulia Pereslid" to "https://metro.co.uk/wp-content/uploads/2021/10/PRI_203475964.jpg?quality=90&strip=all&zoom=1&resize=540%2C810",
"Zhai Zhigang" to "https://lh3.googleusercontent.com/proxy/xk_cyKyl2J0L0evVWqscwKjb2Ypsnr4PuoT2WciVGi_r6ALDFw-1Rlw4cU37z24n3ePXZm5RrRCJIVzi-UbPdEEidf3GJVb0uUhoPlXv67S-1-56yhNxaM-i0Ug4IX8RNYyfTn4zx9drQ_A",
"Zhai Zhigang" to "https://dingyue.ws.126.net/2021/0701/aaa7e1e7j00qvkk13001hc000hs00lzg.jpg",
"Wang Yaping" to "https://alchetron.com/cdn/wang-yaping-2f869150-f66f-4884-b638-62ff678084a-resize-750.jpeg",
"Ye Guangfu" to "https://lh3.googleusercontent.com/proxy/ffO0hJN-y4tkAuta240ninzvANaZnhMtszxxdHIMx0RuUWloi8QSkFSC6VejW8pytRDr11pQ8UfgrAioJNQjBTwthSVlilElwBDanfZH0UgXmjIMfjwmGXQDQIkEiNrDjhIwgVHQdQ",
"Ye Guangfu" to "https://upload.wikimedia.org/wikipedia/commons/a/a2/Ye_Guangfu_in_2021.jpg",
"Raja Chari" to "https://www.spacex.com/static/images/crew-3/portraits/SPACEX_Crew-3_RajaChari.jpg",
"Tom Marshburn" to "https://www.spacex.com/static/images/crew-3/portraits/SPACEX_Crew-3_TomMarshburn.jpg",
"Kayla Barron" to "https://www.spacex.com/static/images/crew-3/portraits/SPACEX_Crew-3_KaylaBarron.jpg",
"Matthias Maurer" to "https://www.spacex.com/static/images/crew-3/portraits/SPACEX_Crew-3_MathiasMaurer.jpg",
"Alexander Misurkin" to "https://spaceflight101.com/iss/wp-content/uploads/sites/37/2017/08/36468940815_70ef48a8b6_o-768x1152.jpg",
"Yusaku Maezawa" to "https://pbs.twimg.com/media/FD599ihaUAAyjC7?format=jpg",
"Yozo Hirano" to "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS1lYvHXPvl-eG3yX5MWAtvTaxkoiISJ5KXrg&usqp=CAU",
)
val personBios = mapOf(
@ -62,4 +69,12 @@ val personBios = mapOf(
"Zhai Zhigang" to "Zhai Zhigang (born October 10, 1966) is a major general of the People's Liberation Army Strategic Support Force (PLASSF) in active service as a People's Liberation Army Astronaut Corps (PLAAC) taikonaut. During the Shenzhou 7 mission in 2008, he became the first Chinese citizen to carry out a spacewalk. He was a People's Liberation Army Air Force (PLAAF) fighter pilot.",
"Wang Yaping" to "Colonel Wang Yaping (born 27 January 1980) is a Chinese military pilot and astronaut. Wang was the second female astronaut selected to the People's Liberation Army Astronaut Corps, and the second Chinese woman in space.",
"Ye Guangfu" to "Colonel Ye Guangfu (Chinese: 叶光富; born 1 September 1980[1]) is a Chinese People's Liberation Army Astronaut Corps (PLAAC) astronaut selected as part of the Shenzhou program.",
"Raja Chari" to "Raja Jon Vurputoor \"Grinder\" Chari (born June 24, 1977; Colonel, United States Air Force) is an American test pilot and NASA astronaut. He is a graduate of the U.S. Air Force Academy, Massachusetts Institute of Technology, and U.S. Naval Test Pilot School, and has over 2,000 flying hours.",
"Tom Marshburn" to "Thomas Henry \"Tom\" Marshburn (born August 29, 1960) is an American physician and a NASA astronaut. He is a veteran of two spaceflights to the International Space Station.",
"Kayla Barron" to "Kayla Jane Barron (born September 19, 1987; LCDR, USN) is an American submarine warfare officer, engineer and NASA astronaut.",
"Matthias Maurer" to "Matthias Josef Maurer (born 18 March 1970 in St. Wendel, Saarland) is a German European Space Agency astronaut and materials scientist, who was selected in 2015 to take part in space training.",
"Alexander Misurkin" to "Alexander Alexanderovich Misurkin (Russian: Aлександрександрович Мисуркин) (born September 23, 1977), a major in the Russian Air Force, is a Russian cosmonaut, selected in 2006. He flew aboard Soyuz TMA-08M on 28 March 2013 as his first space mission, and launched on Soyuz MS-06 as his second flight, in 2017. He was Commander of the International Space Station for Expedition 54.",
"Yusaku Maezawa" to "Yusaku Maezawa (前澤 友作, Maezawa Yūsaku, born 22 November 1975) is a Japanese billionaire entrepreneur and art collector. He founded Start Today in 1998 and launched the online fashion retail website Zozotown in 2004, now Japan's largest. Most recently, Maezawa introduced a custom-fit apparel brand ZOZO and at-home measurement system, the ZOZOSUIT, in 2018. As of July 2021, he is estimated by Forbes to have a net worth of $1.9 billion.",
"Yozo Hirano" to "Yozo Hirano (Japanese: 平野 陽三, 1985- ) born in Imabari, Ehime Prefecture, Japan, is a Japanese spaceflight participant. He is scheduled to fly on Soyuz MS-20.\n\n"
+ "He is foreseen to fly with Yusaku Maezawa, who will pay for both seats; his function will be as production assistant of Maezawa and to document the flight."
)

View file

@ -2,14 +2,13 @@ import com.surrus.common.di.initKoin
import com.surrus.common.remote.Assignment
import com.surrus.common.remote.AstroResult
import com.surrus.common.remote.PeopleInSpaceApi
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.serialization.*
import io.ktor.server.application.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun main() {
val koin = initKoin(enableNetworkLogs = true).koin
@ -22,20 +21,6 @@ fun main() {
json()
}
install(CORS) {
method(HttpMethod.Options)
method(HttpMethod.Put)
method(HttpMethod.Delete)
method(HttpMethod.Patch)
header(HttpHeaders.Authorization)
header(HttpHeaders.ContentType)
header(HttpHeaders.AccessControlAllowOrigin)
// header("any header") if you want to add any header
allowCredentials = true
allowNonSimpleContentTypes = true
anyHost()
}
routing {
get("/astros.json") {

View file

@ -1,4 +1,7 @@
buildscript {
val kotlinVersion: String by project
println(kotlinVersion)
repositories {
google()
mavenCentral()
@ -8,15 +11,16 @@ buildscript {
dependencies {
// keeping this here to allow AS to automatically update
classpath("com.android.tools.build:gradle:7.0.3")
classpath("com.android.tools.build:gradle:7.1.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-serialization:${kotlinVersion}")
with(Deps.Gradle) {
classpath(kotlin)
classpath(kotlinSerialization)
classpath(sqlDelight)
classpath(shadow)
classpath(kotlinter)
classpath(gradleVersionsPlugin)
classpath("com.rickclephas.kmp:kmp-nativecoroutines-gradle-plugin:${Versions.kmpNativeCoroutinesVersion}")
}
}
}
@ -29,5 +33,13 @@ allprojects {
mavenCentral()
maven(url = "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers")
maven(url = "https://jitpack.io")
maven(url = "https://maven.pkg.jetbrains.space/public/p/kotlinx-coroutines/maven")
}
}
// On Apple Silicon we need Node.js 16.0.0
// https://youtrack.jetbrains.com/issue/KT-49109
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class) {
rootProject.the(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension::class).nodeVersion = "16.0.0"
}

View file

@ -3,18 +3,21 @@ object Versions {
const val androidCompileSdk = 31
const val androidTargetSdk = androidCompileSdk
const val kotlin = "1.5.31"
const val kotlinCoroutines = "1.5.2-native-mt"
const val koin = "3.1.2"
const val ktor = "1.6.4"
const val kotlinxSerialization = "1.2.2"
const val kotlinCoroutines = "1.6.0"
const val koin = "3.1.4"
const val ktor = "2.0.0-beta-1"
const val kotlinxSerialization = "1.3.2"
const val kotlinxHtmlJs = "0.7.3"
const val compose = "1.0.4"
const val wearCompose = "1.0.0-alpha08"
const val navCompose = "2.4.0-alpha10"
const val accompanist = "0.19.0"
const val kmpNativeCoroutinesVersion = "0.11.1-new-mm"
const val compose = "1.1.0-rc01"
const val composeCompiler = "1.1.0-rc02"
const val wearCompose = "1.0.0-alpha13"
const val navCompose = "2.4.0-rc01"
const val accompanist = "0.22.0-rc"
const val composeDesktopWeb = "1.0.1"
const val junit = "4.12"
const val androidXTestJUnit = "1.1.3"
@ -22,7 +25,7 @@ object Versions {
const val mockito = "3.11.2"
const val robolectric = "4.6.1"
const val sqlDelight = "1.5.0"
const val sqlDelight = "1.5.3"
const val shadow = "7.0.0"
const val kotlinterGradle = "3.4.5"
@ -40,15 +43,13 @@ object Versions {
const val slf4j = "1.7.30"
const val logback = "1.2.3"
const val kermit = "0.1.9"
const val kermit = "1.0.0"
const val gradleVersionsPlugin = "0.39.0"
}
object Deps {
object Gradle {
const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
const val kotlinSerialization = "org.jetbrains.kotlin:kotlin-serialization:${Versions.kotlin}"
const val kotlinter = "org.jmailen.gradle:kotlinter-gradle:${Versions.kotlinterGradle}"
const val shadow = "gradle.plugin.com.github.jengelman.gradle.plugins:shadow:${Versions.shadow}"
const val sqlDelight = "com.squareup.sqldelight:gradle-plugin:${Versions.sqlDelight}"
@ -58,6 +59,7 @@ object Deps {
object Kotlinx {
const val serializationCore = "org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.kotlinxSerialization}"
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinCoroutines}"
const val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.kotlinCoroutines}"
const val htmlJs = "org.jetbrains.kotlinx:kotlinx-html-js:${Versions.kotlinxHtmlJs}"
}
@ -79,15 +81,13 @@ object Deps {
const val robolectric = "org.robolectric:robolectric:${Versions.robolectric}"
const val testCore = "androidx.test:core:${Versions.testCore}"
const val kotlinTest = "org.jetbrains.kotlin:kotlin-test:${Versions.kotlin}"
const val kotlinTestJUnit = "org.jetbrains.kotlin:kotlin-test-junit:${Versions.kotlin}"
const val composeUiTest = "androidx.compose.ui:ui-test:${Versions.compose}"
const val composeUiTestJUnit = "androidx.compose.ui:ui-test-junit4:${Versions.compose}"
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest:${Versions.compose}"
}
object Compose {
const val compiler = "androidx.compose.compiler:compiler:${Versions.composeCompiler}"
const val ui = "androidx.compose.ui:ui:${Versions.compose}"
const val uiGraphics = "androidx.compose.ui:ui-graphics:${Versions.compose}"
const val uiTooling = "androidx.compose.ui:ui-tooling:${Versions.compose}"
@ -114,18 +114,19 @@ object Deps {
object Ktor {
const val serverCore = "io.ktor:ktor-server-core:${Versions.ktor}"
const val serverNetty = "io.ktor:ktor-server-netty:${Versions.ktor}"
const val serialization = "io.ktor:ktor-serialization:${Versions.ktor}"
const val contentNegotiation = "io.ktor:ktor-client-content-negotiation:${Versions.ktor}"
const val json = "io.ktor:ktor-serialization-kotlinx-json:${Versions.ktor}"
const val serverContentNegotiation = "io.ktor:ktor-server-content-negotiation:${Versions.ktor}"
const val websockets = "io.ktor:ktor-websockets:${Versions.ktor}"
const val clientCore = "io.ktor:ktor-client-core:${Versions.ktor}"
const val clientJson = "io.ktor:ktor-client-json:${Versions.ktor}"
const val clientLogging = "io.ktor:ktor-client-logging:${Versions.ktor}"
const val clientSerialization = "io.ktor:ktor-client-serialization:${Versions.ktor}"
const val clientAndroid = "io.ktor:ktor-client-android:${Versions.ktor}"
const val clientApache = "io.ktor:ktor-client-apache:${Versions.ktor}"
const val clientJava = "io.ktor:ktor-client-java:${Versions.ktor}"
const val clientIos = "io.ktor:ktor-client-ios:${Versions.ktor}"
const val clientCio = "io.ktor:ktor-client-cio:${Versions.ktor}"
const val clientCurl = "io.ktor:ktor-client-curl:${Versions.ktor}"
const val clientJs = "io.ktor:ktor-client-js:${Versions.ktor}"
}
@ -155,4 +156,9 @@ object Deps {
const val logback = "ch.qos.logback:logback-classic:${Versions.logback}"
const val kermit = "co.touchlab:kermit:${Versions.kermit}"
}
object Glance {
const val tiles = "androidx.glance:glance-wear-tiles:1.0.0-alpha02"
const val appwidget = "androidx.glance:glance-appwidget:1.0.0-alpha02"
}
}

View file

@ -1,4 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
kotlin("multiplatform")
@ -6,6 +7,7 @@ plugins {
id("com.android.library")
id("org.jetbrains.kotlin.native.cocoapods")
id("com.squareup.sqldelight")
id("com.rickclephas.kmp.nativecoroutines")
id("com.chromaticnoise.multiplatform-swiftpackage") version "2.0.3"
}
@ -26,33 +28,20 @@ android {
}
}
// Workaround for https://youtrack.jetbrains.com/issue/KT-43944
android {
configurations {
create("androidTestApi")
create("androidTestDebugApi")
create("androidTestReleaseApi")
create("testApi")
create("testDebugApi")
create("testReleaseApi")
}
}
kotlin {
val sdkName: String? = System.getenv("SDK_NAME")
val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
if (isiOSDevice) {
iosArm64("iOS")
} else {
iosX64("iOS")
val iosTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget = when {
System.getenv("SDK_NAME")?.startsWith("iphoneos") == true -> ::iosArm64
System.getenv("NATIVE_ARCH")?.startsWith("arm") == true -> ::iosSimulatorArm64 // available to KT 1.5.30
else -> ::iosX64
}
iosTarget("iOS") {}
val sdkName: String? = System.getenv("SDK_NAME")
val isWatchOSDevice = sdkName.orEmpty().startsWith("watchos")
if (isWatchOSDevice) {
watchosArm64("watch")
} else {
watchosX86("watch")
watchosX64("watch")
}
macosX64("macOS")
@ -72,18 +61,17 @@ kotlin {
sourceSets {
sourceSets["commonMain"].dependencies {
implementation(Deps.Kotlinx.coroutinesCore) {
isForce = true
}
with(Deps.Ktor) {
implementation(clientCore)
implementation(clientJson)
implementation(clientLogging)
implementation(clientSerialization)
implementation(contentNegotiation)
implementation(json)
}
with(Deps.Kotlinx) {
implementation(coroutinesCore)
implementation(serializationCore)
}
@ -102,6 +90,10 @@ kotlin {
}
}
sourceSets["commonTest"].dependencies {
implementation(Deps.Koin.test)
implementation(Deps.Kotlinx.coroutinesTest)
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
sourceSets["androidMain"].dependencies {
@ -109,10 +101,6 @@ kotlin {
implementation(Deps.SqlDelight.androidDriver)
}
sourceSets["androidTest"].dependencies {
// having issue with following after update to Kotlin 1.5.21
// need to investigate further
//implementation(Deps.Test.kotlinTest)
//implementation(Deps.Test.kotlinTestJUnit)
implementation(Deps.Test.junit)
}
@ -135,7 +123,7 @@ kotlin {
}
sourceSets["macOSMain"].dependencies {
implementation(Deps.Ktor.clientCurl)
implementation(Deps.Ktor.clientIos)
implementation(Deps.SqlDelight.nativeDriverMacos)
}

View file

@ -35,10 +35,7 @@ Pod::Spec.new do |spec|
"$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
-Pkotlin.native.cocoapods.archs="$ARCHS" \
-Pkotlin.native.cocoapods.configuration=$CONFIGURATION \
-Pkotlin.native.cocoapods.cflags="$OTHER_CFLAGS" \
-Pkotlin.native.cocoapods.paths.headers="$HEADER_SEARCH_PATHS" \
-Pkotlin.native.cocoapods.paths.frameworks="$FRAMEWORK_SEARCH_PATHS"
-Pkotlin.native.cocoapods.configuration=$CONFIGURATION
SCRIPT
}
]

View file

@ -1,12 +1,13 @@
package com.surrus.common.repository
import co.touchlab.kermit.LogcatLogger
import co.touchlab.kermit.Logger
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.surrus.common.di.PeopleInSpaceDatabaseWrapper
import com.surrus.peopleinspace.db.PeopleInSpaceDatabase
import io.ktor.client.engine.android.*
import org.koin.dsl.module
actual fun platformModule() = module {
single {
val driver =
@ -14,5 +15,5 @@ actual fun platformModule() = module {
PeopleInSpaceDatabaseWrapper(PeopleInSpaceDatabase(driver))
}
single<Logger> { LogcatLogger() }
single { Android.create() }
}

View file

@ -1,18 +0,0 @@
package com.surrus.peopleinspace
import com.surrus.common.di.initKoin
import com.surrus.common.remote.PeopleInSpaceApi
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue
import org.junit.Test
class PeopleInSpaceTest {
@Test
fun testGetPeople() = runBlocking {
val koin = initKoin(enableNetworkLogs = true).koin
val peopleInSpaceApi = koin.get<PeopleInSpaceApi>()
val result = peopleInSpaceApi.fetchPeople()
println(result)
assertTrue(result.people.isNotEmpty())
}
}

View file

@ -1,14 +1,14 @@
package com.surrus.common.di
import co.touchlab.kermit.Kermit
import com.surrus.common.remote.PeopleInSpaceApi
import com.surrus.common.repository.PeopleInSpaceRepository
import com.surrus.common.repository.PeopleInSpaceRepositoryInterface
import com.surrus.common.repository.platformModule
import io.ktor.client.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.features.logging.*
import io.ktor.client.engine.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -28,21 +28,21 @@ fun initKoin() = initKoin(enableNetworkLogs = false) {}
fun commonModule(enableNetworkLogs: Boolean) = module {
single { createJson() }
single { createHttpClient(get(), enableNetworkLogs = enableNetworkLogs) }
single { createHttpClient(get(), get(), enableNetworkLogs = enableNetworkLogs) }
single { CoroutineScope(Dispatchers.Default + SupervisorJob() ) }
single<PeopleInSpaceRepositoryInterface> { PeopleInSpaceRepository() }
single { PeopleInSpaceApi(get()) }
single { Kermit(logger = get()) }
}
fun createJson() = Json { isLenient = true; ignoreUnknownKeys = true }
fun createHttpClient(json: Json, enableNetworkLogs: Boolean) = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(json)
fun createHttpClient(httpClientEngine: HttpClientEngine, json: Json, enableNetworkLogs: Boolean) = HttpClient(httpClientEngine) {
install(ContentNegotiation) {
json(json)
}
if (enableNetworkLogs) {
install(Logging) {

View file

@ -1,6 +1,7 @@
package com.surrus.common.remote
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import kotlinx.serialization.Serializable
import org.koin.core.component.KoinComponent
@ -21,6 +22,6 @@ class PeopleInSpaceApi(
private val client: HttpClient,
var baseUrl: String = "https://people-in-space-proxy.ew.r.appspot.com",
) : KoinComponent {
suspend fun fetchPeople() = client.get<AstroResult>("$baseUrl/astros.json")
suspend fun fetchISSPosition() = client.get<IssResponse>("$baseUrl/iss-now.json")
suspend fun fetchPeople() = client.get("$baseUrl/astros.json").body<AstroResult>()
suspend fun fetchISSPosition() = client.get("$baseUrl/iss-now.json").body<IssResponse>()
}

View file

@ -1,6 +1,7 @@
package com.surrus.common.repository
import co.touchlab.kermit.Kermit
import co.touchlab.kermit.Logger
import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope
import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList
import com.surrus.common.di.PeopleInSpaceDatabaseWrapper
@ -11,7 +12,6 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.coroutines.CoroutineContext
interface PeopleInSpaceRepositoryInterface {
fun fetchPeopleAsFlow(): Flow<List<Assignment>>
@ -22,13 +22,13 @@ interface PeopleInSpaceRepositoryInterface {
class PeopleInSpaceRepository : KoinComponent, PeopleInSpaceRepositoryInterface {
private val peopleInSpaceApi: PeopleInSpaceApi by inject()
private val logger: Kermit by inject()
@NativeCoroutineScope
private val coroutineScope: CoroutineScope = MainScope()
private val peopleInSpaceDatabase: PeopleInSpaceDatabaseWrapper by inject()
private val peopleInSpaceQueries = peopleInSpaceDatabase.instance?.peopleInSpaceQueries
var peopleJob: Job? = null
val logger = Logger.withTag("PeopleInSpaceRepository")
init {
coroutineScope.launch {
@ -71,54 +71,21 @@ class PeopleInSpaceRepository : KoinComponent, PeopleInSpaceRepositoryInterface
}
}
// Used by web client atm
// Used by web and apple clients atm
override suspend fun fetchPeople(): List<Assignment> = peopleInSpaceApi.fetchPeople().people
// called from Kotlin/Native clients
fun startObservingPeopleUpdates(success: (List<Assignment>) -> Unit) {
logger.d { "startObservingPeopleUpdates" }
peopleJob = coroutineScope.launch {
fetchPeopleAsFlow().collect {
success(it)
override fun pollISSPosition(): Flow<IssPosition> {
return flow {
while (true) {
val position = peopleInSpaceApi.fetchISSPosition().iss_position
emit(position)
logger.d { position.toString() }
delay(POLL_INTERVAL)
}
}
}
fun stopObservingPeopleUpdates() {
logger.d { "stopObservingPeopleUpdates, peopleJob = $peopleJob" }
peopleJob?.cancel()
}
override fun pollISSPosition(): Flow<IssPosition> = flow {
while (true) {
val position = peopleInSpaceApi.fetchISSPosition().iss_position
emit(position)
logger.d("PeopleInSpaceRepository") { position.toString() }
delay(POLL_INTERVAL)
}
}
val iosScope: CoroutineScope = object : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = SupervisorJob() + Dispatchers.Main
}
fun iosPollISSPosition() = KotlinNativeFlowWrapper(pollISSPosition())
companion object {
private const val POLL_INTERVAL = 10000L
}
}
class KotlinNativeFlowWrapper<T>(private val flow: Flow<T>) {
fun subscribe(
scope: CoroutineScope,
onEach: (item: T) -> Unit,
onComplete: () -> Unit,
onThrow: (error: Throwable) -> Unit
) = flow
.onEach { onEach(it) }
.catch { onThrow(it) }
.onCompletion { onComplete() }
.launchIn(scope)
}

View file

@ -0,0 +1,43 @@
package com.surrus.peopleinspace
import com.surrus.common.di.PeopleInSpaceDatabaseWrapper
import com.surrus.common.di.commonModule
import com.surrus.common.repository.PeopleInSpaceRepositoryInterface
import com.surrus.common.repository.platformModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.koin.core.context.startKoin
import org.koin.dsl.module
import org.koin.test.KoinTest
import org.koin.test.inject
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertTrue
class PeopleInSpaceTest: KoinTest {
private val repo : PeopleInSpaceRepositoryInterface by inject()
@BeforeTest
fun setUp() {
Dispatchers.setMain(StandardTestDispatcher())
startKoin{
modules(
commonModule(true),
platformModule(),
module {
single { PeopleInSpaceDatabaseWrapper(null) }
}
)
}
}
@Test
fun testGetPeople() = runTest {
val result = repo.fetchPeople()
println(result)
assertTrue(result.isNotEmpty())
}
}

View file

@ -1,16 +1,16 @@
package com.surrus.common.repository
import co.touchlab.kermit.Logger
import co.touchlab.kermit.NSLogLogger
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
import com.surrus.common.di.PeopleInSpaceDatabaseWrapper
import com.surrus.peopleinspace.db.PeopleInSpaceDatabase
import io.ktor.client.engine.ios.*
import org.koin.dsl.module
actual fun platformModule() = module {
single {
val driver = NativeSqliteDriver(PeopleInSpaceDatabase.Schema, "peopleinspace.db")
PeopleInSpaceDatabaseWrapper(PeopleInSpaceDatabase(driver))
}
single<Logger> { NSLogLogger() }
single { Ios.create() }
}

View file

@ -1,18 +0,0 @@
package com.surrus.peopleinspace
import com.surrus.common.di.initKoin
import com.surrus.common.repository.PeopleInSpaceRepositoryInterface
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertTrue
class PeopleInSpaceTest {
@Test
fun testGetPeople() = runBlocking {
val koin = initKoin(enableNetworkLogs = true).koin
val repo = koin.get<PeopleInSpaceRepositoryInterface>()
val result = repo.fetchPeople()
println(result)
assertTrue(result.isNotEmpty())
}
}

View file

@ -1,14 +1,12 @@
package com.surrus.common.repository
import co.touchlab.kermit.CommonLogger
import co.touchlab.kermit.Logger
import com.surrus.common.di.PeopleInSpaceDatabaseWrapper
import io.ktor.client.engine.js.*
import org.koin.dsl.module
actual fun platformModule() = module {
single {
PeopleInSpaceDatabaseWrapper(null)
}
single<Logger> { CommonLogger() }
single { Js.create() }
}

View file

@ -1,10 +1,9 @@
package com.surrus.common.repository
import co.touchlab.kermit.CommonLogger
import co.touchlab.kermit.Logger
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
import com.surrus.common.di.PeopleInSpaceDatabaseWrapper
import com.surrus.peopleinspace.db.PeopleInSpaceDatabase
import io.ktor.client.engine.java.*
import org.koin.dsl.module
actual fun platformModule() = module {
@ -13,6 +12,5 @@ actual fun platformModule() = module {
.also { PeopleInSpaceDatabase.Schema.create(it) }
PeopleInSpaceDatabaseWrapper(PeopleInSpaceDatabase(driver))
}
single<Logger> { CommonLogger() }
single { Java.create() }
}

View file

@ -1,10 +1,9 @@
package com.surrus.common.repository
import co.touchlab.kermit.Logger
import co.touchlab.kermit.NSLogLogger
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
import com.surrus.common.di.PeopleInSpaceDatabaseWrapper
import com.surrus.peopleinspace.db.PeopleInSpaceDatabase
import io.ktor.client.engine.ios.*
import org.koin.dsl.module
actual fun platformModule() = module {
@ -12,5 +11,5 @@ actual fun platformModule() = module {
val driver = NativeSqliteDriver(PeopleInSpaceDatabase.Schema, "peopleinspace.db")
PeopleInSpaceDatabaseWrapper(PeopleInSpaceDatabase(driver))
}
single<Logger> { NSLogLogger() }
single { Ios.create() }
}

View file

@ -1,10 +1,9 @@
package com.surrus.common.repository
import co.touchlab.kermit.Logger
import co.touchlab.kermit.NSLogLogger
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
import com.surrus.common.di.PeopleInSpaceDatabaseWrapper
import com.surrus.peopleinspace.db.PeopleInSpaceDatabase
import io.ktor.client.engine.ios.*
import org.koin.dsl.module
actual fun platformModule() = module {
@ -12,5 +11,5 @@ actual fun platformModule() = module {
val driver = NativeSqliteDriver(PeopleInSpaceDatabase.Schema, "peopleinspace.db")
PeopleInSpaceDatabaseWrapper(PeopleInSpaceDatabase(driver))
}
single<Logger> { NSLogLogger() }
single { Ios.create() }
}

View file

@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
id("org.jetbrains.compose") version "1.0.0-alpha4-build398"
id("org.jetbrains.compose") version Versions.composeDesktopWeb
application
}

View file

@ -1,6 +1,6 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose") version "1.0.0-alpha4-build398"
id("org.jetbrains.compose") version Versions.composeDesktopWeb
}
version = "1.0"
@ -33,6 +33,7 @@ kotlin {
afterEvaluate {
rootProject.extensions.configure<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension> {
versions.webpackDevServer.version = "4.0.0"
versions.webpackCli.version = "4.9.0"
}
}

View file

@ -21,4 +21,10 @@ android.enableJetifier=false
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# XCode
xcodeproj=./ios/PeopleInSpaceSwiftUI
xcodeproj=./ios/PeopleInSpaceSwiftUI
# Kotlin/Native clients can override this through updating common.podspec
kotlinVersion=1.6.10
kotlin.native.binary.memoryModel=experimental
kotlin.native.binary.freezing=disabled

View file

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip

1
graphql-server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
build

View file

@ -0,0 +1,41 @@
plugins {
id("kotlin-platform-jvm")
id("org.jetbrains.kotlin.plugin.spring") version("1.6.10")
id("org.jetbrains.kotlin.plugin.serialization")
id("org.springframework.boot") version("2.5.6")
id("com.google.cloud.tools.appengine") version("2.4.2")
id("com.github.johnrengelman.shadow")
}
dependencies {
implementation("com.expediagroup:graphql-kotlin-spring-server:5.3.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.3.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
testImplementation("com.squareup.okhttp3:okhttp:4.9.3")
with(Deps.Log) {
implementation(logback)
}
implementation(project(":common"))
}
kotlin {
sourceSets.all {
languageSettings {
optIn("kotlin.RequiresOptIn")
}
}
}
appengine {
stage {
setArtifact(tasks.named("bootJar").flatMap { (it as Jar).archiveFile })
}
deploy {
projectId = "peopleinspace-graphql"
version = "GCLOUD_CONFIG"
}
}

View file

@ -0,0 +1,3 @@
runtime: java11
entrypoint: java -Xmx64m -jar graphql-server.jar

View file

@ -0,0 +1,20 @@
package com.surrus.peopleinspace
import com.surrus.common.di.initKoin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.ConfigurableApplicationContext
val koin = initKoin(enableNetworkLogs = true).koin
@SpringBootApplication
class DefaultApplication {
}
fun runServer(): ConfigurableApplicationContext {
return runApplication<DefaultApplication>()
}

View file

@ -0,0 +1,37 @@
package com.surrus.peopleinspace
import com.expediagroup.graphql.server.operations.Subscription
import com.surrus.common.remote.IssPosition
import com.surrus.common.remote.PeopleInSpaceApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.reactive.asPublisher
import org.reactivestreams.Publisher
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class IssPositionSubscription : Subscription {
private val logger: Logger = LoggerFactory.getLogger(IssPositionSubscription::class.java)
private var peopleInSpaceApi: PeopleInSpaceApi = koin.get()
fun issPosition(): Publisher<IssPosition> {
return flow {
while (true) {
val position = peopleInSpaceApi.fetchISSPosition().iss_position
logger.info("ISS position = $position")
emit(position)
delay(POLL_INTERVAL)
}
}.asPublisher()
}
companion object {
private const val POLL_INTERVAL = 10000L
}
}

View file

@ -0,0 +1,15 @@
package com.surrus.peopleinspace
import com.expediagroup.graphql.server.operations.Query
import com.surrus.common.remote.PeopleInSpaceApi
import com.surrus.common.remote.Assignment
import org.springframework.stereotype.Component
data class People(val people: List<Assignment>)
@Component
class RootQuery : Query {
private var peopleInSpaceApi: PeopleInSpaceApi = koin.get()
suspend fun allPeople(): People = People(peopleInSpaceApi.fetchPeople().people)
}

View file

@ -0,0 +1,6 @@
package com.surrus.peopleinspace
fun main(args: Array<String>) {
runServer()
}

View file

@ -0,0 +1,2 @@
graphql:
packages: "com.surrus"

View file

@ -7,6 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
1A2873952733F5C600D7C25E /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A40FE1823DB5C35008428A6 /* libsqlite3.tbd */; };
1A287396273427B300D7C25E /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AA82EEC25052E1E00193051 /* ImageView.swift */; };
1A28791D2733F257008EC881 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A28791C2733F257008EC881 /* WidgetKit.framework */; };
1A28791F2733F257008EC881 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A28791E2733F257008EC881 /* SwiftUI.framework */; };
1A2879222733F257008EC881 /* PeopleInSpaceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2879212733F257008EC881 /* PeopleInSpaceWidget.swift */; };
1A2879242733F259008EC881 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1A2879232733F259008EC881 /* Assets.xcassets */; };
1A2879282733F259008EC881 /* PeopleInSpaceWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 1A28791B2733F257008EC881 /* PeopleInSpaceWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1A40FE1923DB5C35008428A6 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A40FE1823DB5C35008428A6 /* libsqlite3.tbd */; };
1AA82EED25052E1E00193051 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AA82EEC25052E1E00193051 /* ImageView.swift */; };
1ABD44FA23B00008008387E3 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABD44F923B00008008387E3 /* ViewModel.swift */; };
@ -16,12 +23,44 @@
1ABFB8C423AFF5CE003D807E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1ABFB8C323AFF5CE003D807E /* Assets.xcassets */; };
1ABFB8C723AFF5CE003D807E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1ABFB8C623AFF5CE003D807E /* Preview Assets.xcassets */; };
1ABFB8CA23AFF5CE003D807E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1ABFB8C823AFF5CE003D807E /* LaunchScreen.storyboard */; };
1AC2439025A1D57700F17D2F /* IssPositionPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AC2438F25A1D57700F17D2F /* IssPositionPublisher.swift */; };
1AD2EC6A25E9984900CCEE81 /* ISSMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AD2EC6925E9984900CCEE81 /* ISSMapView.swift */; };
1E1255057DE614781855FC02 /* libPods-PeopleInSpaceSwiftUI.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 28681F6577A67864E2C2D9B4 /* libPods-PeopleInSpaceSwiftUI.a */; };
EB26652EEED4D2D4BCFA681D /* libPods-PeopleInSpaceWidgetExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0873B836B9801BC514F9A555 /* libPods-PeopleInSpaceWidgetExtension.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
1A2879262733F259008EC881 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 1ABFB8B223AFF5CC003D807E /* Project object */;
proxyType = 1;
remoteGlobalIDString = 1A28791A2733F257008EC881;
remoteInfo = PeopleInSpaceWidgetExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
1A28792C2733F259008EC881 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
1A2879282733F259008EC881 /* PeopleInSpaceWidgetExtension.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0873B836B9801BC514F9A555 /* libPods-PeopleInSpaceWidgetExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-PeopleInSpaceWidgetExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; };
18A371146E3D470DBF29C172 /* Pods-PeopleInSpaceWidgetExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PeopleInSpaceWidgetExtension.debug.xcconfig"; path = "Target Support Files/Pods-PeopleInSpaceWidgetExtension/Pods-PeopleInSpaceWidgetExtension.debug.xcconfig"; sourceTree = "<group>"; };
1A28791B2733F257008EC881 /* PeopleInSpaceWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PeopleInSpaceWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
1A28791C2733F257008EC881 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
1A28791E2733F257008EC881 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
1A2879212733F257008EC881 /* PeopleInSpaceWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeopleInSpaceWidget.swift; sourceTree = "<group>"; };
1A2879232733F259008EC881 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
1A2879252733F259008EC881 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1A40FE1823DB5C35008428A6 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; };
1AA82EEC25052E1E00193051 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
1ABD44F923B00008008387E3 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
@ -33,14 +72,25 @@
1ABFB8C623AFF5CE003D807E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
1ABFB8C923AFF5CE003D807E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
1ABFB8CB23AFF5CE003D807E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1AC2438F25A1D57700F17D2F /* IssPositionPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssPositionPublisher.swift; sourceTree = "<group>"; };
1AD2EC6925E9984900CCEE81 /* ISSMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ISSMapView.swift; sourceTree = "<group>"; };
28681F6577A67864E2C2D9B4 /* libPods-PeopleInSpaceSwiftUI.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-PeopleInSpaceSwiftUI.a"; sourceTree = BUILT_PRODUCTS_DIR; };
56CD7101BBBC60F561BFB049 /* Pods-PeopleInSpaceSwiftUI.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PeopleInSpaceSwiftUI.release.xcconfig"; path = "Target Support Files/Pods-PeopleInSpaceSwiftUI/Pods-PeopleInSpaceSwiftUI.release.xcconfig"; sourceTree = "<group>"; };
BD4D317D12DFE46788F27B26 /* Pods-PeopleInSpaceWidgetExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PeopleInSpaceWidgetExtension.release.xcconfig"; path = "Target Support Files/Pods-PeopleInSpaceWidgetExtension/Pods-PeopleInSpaceWidgetExtension.release.xcconfig"; sourceTree = "<group>"; };
E9A29AFE5FFFB564C509840A /* Pods-PeopleInSpaceSwiftUI.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PeopleInSpaceSwiftUI.debug.xcconfig"; path = "Target Support Files/Pods-PeopleInSpaceSwiftUI/Pods-PeopleInSpaceSwiftUI.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
1A2879182733F257008EC881 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1A28791F2733F257008EC881 /* SwiftUI.framework in Frameworks */,
1A28791D2733F257008EC881 /* WidgetKit.framework in Frameworks */,
1A2873952733F5C600D7C25E /* libsqlite3.tbd in Frameworks */,
EB26652EEED4D2D4BCFA681D /* libPods-PeopleInSpaceWidgetExtension.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
1ABFB8B723AFF5CC003D807E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -58,14 +108,27 @@
children = (
E9A29AFE5FFFB564C509840A /* Pods-PeopleInSpaceSwiftUI.debug.xcconfig */,
56CD7101BBBC60F561BFB049 /* Pods-PeopleInSpaceSwiftUI.release.xcconfig */,
18A371146E3D470DBF29C172 /* Pods-PeopleInSpaceWidgetExtension.debug.xcconfig */,
BD4D317D12DFE46788F27B26 /* Pods-PeopleInSpaceWidgetExtension.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
1A2879202733F257008EC881 /* PeopleInSpaceWidget */ = {
isa = PBXGroup;
children = (
1A2879212733F257008EC881 /* PeopleInSpaceWidget.swift */,
1A2879232733F259008EC881 /* Assets.xcassets */,
1A2879252733F259008EC881 /* Info.plist */,
);
path = PeopleInSpaceWidget;
sourceTree = "<group>";
};
1ABFB8B123AFF5CC003D807E = {
isa = PBXGroup;
children = (
1ABFB8BC23AFF5CC003D807E /* PeopleInSpaceSwiftUI */,
1A2879202733F257008EC881 /* PeopleInSpaceWidget */,
1ABFB8BB23AFF5CC003D807E /* Products */,
0682AAA6A83414CA1CC104C4 /* Pods */,
D0F1ED5C992B526EA33236F9 /* Frameworks */,
@ -76,6 +139,7 @@
isa = PBXGroup;
children = (
1ABFB8BA23AFF5CC003D807E /* PeopleInSpaceSwiftUI.app */,
1A28791B2733F257008EC881 /* PeopleInSpaceWidgetExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -92,7 +156,6 @@
1ABFB8C523AFF5CE003D807E /* Preview Content */,
1ABD44F923B00008008387E3 /* ViewModel.swift */,
1AA82EEC25052E1E00193051 /* ImageView.swift */,
1AC2438F25A1D57700F17D2F /* IssPositionPublisher.swift */,
1AD2EC6925E9984900CCEE81 /* ISSMapView.swift */,
);
path = PeopleInSpaceSwiftUI;
@ -111,6 +174,9 @@
children = (
1A40FE1823DB5C35008428A6 /* libsqlite3.tbd */,
28681F6577A67864E2C2D9B4 /* libPods-PeopleInSpaceSwiftUI.a */,
1A28791C2733F257008EC881 /* WidgetKit.framework */,
1A28791E2733F257008EC881 /* SwiftUI.framework */,
0873B836B9801BC514F9A555 /* libPods-PeopleInSpaceWidgetExtension.a */,
);
name = Frameworks;
sourceTree = "<group>";
@ -118,6 +184,24 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
1A28791A2733F257008EC881 /* PeopleInSpaceWidgetExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1A2879292733F259008EC881 /* Build configuration list for PBXNativeTarget "PeopleInSpaceWidgetExtension" */;
buildPhases = (
CB56E28CF58AE58B52E5412F /* [CP] Check Pods Manifest.lock */,
1A2879172733F257008EC881 /* Sources */,
1A2879182733F257008EC881 /* Frameworks */,
1A2879192733F257008EC881 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = PeopleInSpaceWidgetExtension;
productName = PeopleInSpaceWidgetExtension;
productReference = 1A28791B2733F257008EC881 /* PeopleInSpaceWidgetExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
1ABFB8B923AFF5CC003D807E /* PeopleInSpaceSwiftUI */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1ABFB8CE23AFF5CE003D807E /* Build configuration list for PBXNativeTarget "PeopleInSpaceSwiftUI" */;
@ -126,10 +210,12 @@
1ABFB8B623AFF5CC003D807E /* Sources */,
1ABFB8B723AFF5CC003D807E /* Frameworks */,
1ABFB8B823AFF5CC003D807E /* Resources */,
1A28792C2733F259008EC881 /* Embed App Extensions */,
);
buildRules = (
);
dependencies = (
1A2879272733F259008EC881 /* PBXTargetDependency */,
);
name = PeopleInSpaceSwiftUI;
productName = PeopleInSpaceSwiftUI;
@ -142,10 +228,13 @@
1ABFB8B223AFF5CC003D807E /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1130;
LastSwiftUpdateCheck = 1310;
LastUpgradeCheck = 1130;
ORGANIZATIONNAME = "John O'Reilly";
TargetAttributes = {
1A28791A2733F257008EC881 = {
CreatedOnToolsVersion = 13.1;
};
1ABFB8B923AFF5CC003D807E = {
CreatedOnToolsVersion = 11.3;
};
@ -165,11 +254,20 @@
projectRoot = "";
targets = (
1ABFB8B923AFF5CC003D807E /* PeopleInSpaceSwiftUI */,
1A28791A2733F257008EC881 /* PeopleInSpaceWidgetExtension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
1A2879192733F257008EC881 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1A2879242733F259008EC881 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
1ABFB8B823AFF5CC003D807E /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -205,15 +303,45 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
CB56E28CF58AE58B52E5412F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-PeopleInSpaceWidgetExtension-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
1A2879172733F257008EC881 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1A287396273427B300D7C25E /* ImageView.swift in Sources */,
1A2879222733F257008EC881 /* PeopleInSpaceWidget.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
1ABFB8B623AFF5CC003D807E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1AA82EED25052E1E00193051 /* ImageView.swift in Sources */,
1AC2439025A1D57700F17D2F /* IssPositionPublisher.swift in Sources */,
1ABFB8BE23AFF5CC003D807E /* AppDelegate.swift in Sources */,
1ABFB8C023AFF5CC003D807E /* SceneDelegate.swift in Sources */,
1AD2EC6A25E9984900CCEE81 /* ISSMapView.swift in Sources */,
@ -224,6 +352,14 @@
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
1A2879272733F259008EC881 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 1A28791A2733F257008EC881 /* PeopleInSpaceWidgetExtension */;
targetProxy = 1A2879262733F259008EC881 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
1ABFB8C823AFF5CE003D807E /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
@ -236,6 +372,66 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
1A28792A2733F259008EC881 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 18A371146E3D470DBF29C172 /* Pods-PeopleInSpaceWidgetExtension.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PeopleInSpaceWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = PeopleInSpaceWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 John O'Reilly. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.surrus.PeopleInSpaceSwiftUI.PeopleInSpaceWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
1A28792B2733F259008EC881 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BD4D317D12DFE46788F27B26 /* Pods-PeopleInSpaceWidgetExtension.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PeopleInSpaceWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = PeopleInSpaceWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 John O'Reilly. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.surrus.PeopleInSpaceSwiftUI.PeopleInSpaceWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
1ABFB8CC23AFF5CE003D807E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -286,7 +482,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -340,7 +536,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@ -354,6 +550,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = E9A29AFE5FFFB564C509840A /* Pods-PeopleInSpaceSwiftUI.debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"PeopleInSpaceSwiftUI/Preview Content\"";
@ -375,6 +572,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 56CD7101BBBC60F561BFB049 /* Pods-PeopleInSpaceSwiftUI.release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"PeopleInSpaceSwiftUI/Preview Content\"";
@ -395,6 +593,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
1A2879292733F259008EC881 /* Build configuration list for PBXNativeTarget "PeopleInSpaceWidgetExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1A28792A2733F259008EC881 /* Debug */,
1A28792B2733F259008EC881 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
1ABFB8B523AFF5CC003D807E /* Build configuration list for PBXProject "PeopleInSpaceSwiftUI" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View file

@ -7,7 +7,12 @@
<key>PeopleInSpaceSwiftUI.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
<integer>7</integer>
</dict>
<key>PeopleInSpaceWidgetExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>6</integer>
</dict>
</dict>
</dict>

View file

@ -1,46 +0,0 @@
import Combine
import common
public struct IssPositionPublisher: Publisher {
public typealias Output = IssPosition
public typealias Failure = Never
private let repository: PeopleInSpaceRepository
public init(repository: PeopleInSpaceRepository) {
self.repository = repository
}
public func receive<S: Subscriber>(subscriber: S) where S.Input == IssPosition, S.Failure == Failure {
let subscription = IssPositionSubscription(repository: repository, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
final class IssPositionSubscription<S: Subscriber>: Subscription where S.Input == IssPosition, S.Failure == Failure {
private var subscriber: S?
private var job: Kotlinx_coroutines_coreJob? = nil
private let repository: PeopleInSpaceRepository
init(repository: PeopleInSpaceRepository, subscriber: S) {
self.repository = repository
self.subscriber = subscriber
job = repository.iosPollISSPosition().subscribe(
scope: repository.iosScope,
onEach: { position in
subscriber.receive(position!)
},
onComplete: { subscriber.receive(completion: .finished) },
onThrow: { error in debugPrint(error) }
)
}
func cancel() {
subscriber = nil
job?.cancel(cause: nil)
}
func request(_ demand: Subscribers.Demand) {}
}
}

View file

@ -1,31 +1,48 @@
import Foundation
import Combine
import common
import KMPNativeCoroutinesAsync
@MainActor
class PeopleInSpaceViewModel: ObservableObject {
@Published var people = [Assignment]()
@Published var issPosition = IssPosition(latitude: 0, longitude: 0)
private var subscription: AnyCancellable?
private var fetchPeopleTask: Task<(), Never>? = nil
private var pollISSPositionTask: Task<(), Never>? = nil
private let repository: PeopleInSpaceRepository
init(repository: PeopleInSpaceRepository) {
self.repository = repository
subscription = IssPositionPublisher(repository: repository)
//.map { position in String(format: "ISS Position = (%f, %f)", position.latitude, position.longitude ) }
.assign(to: \.issPosition, on: self)
pollISSPositionTask = Task {
do {
let stream = asyncStream(for: repository.pollISSPositionNative())
for try await data in stream {
self.issPosition = data
}
} catch {
print("Failed with error: \(error)")
}
}
}
func startObservingPeopleUpdates() {
repository.startObservingPeopleUpdates(success: { data in
self.people = data
})
fetchPeopleTask = Task {
do {
let stream = asyncStream(for: repository.fetchPeopleAsFlowNative())
for try await data in stream {
self.people = data
}
} catch {
print("Failed with error: \(error)")
}
}
}
func stopObservingPeopleUpdates() {
repository.stopObservingPeopleUpdates()
fetchPeopleTask?.cancel()
}
}

View file

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,73 @@
import WidgetKit
import SwiftUI
import Combine
import common
import KMPNativeCoroutinesCombine
final class Provider: TimelineProvider {
private let repository: PeopleInSpaceRepository
private var timelineCancellable: AnyCancellable?
init() {
KoinKt.doInitKoin()
repository = PeopleInSpaceRepository()
}
func placeholder(in context: Context) -> PeopleInSpaceListEntry {
PeopleInSpaceListEntry(date: Date(), peopleList: [])
}
func getSnapshot(in context: Context, completion: @escaping (PeopleInSpaceListEntry) -> ()) {
let entry = PeopleInSpaceListEntry(date: Date(), peopleList: [])
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
timelineCancellable = createFuture(for: repository.fetchPeopleNative())
.map { data in
let entry = PeopleInSpaceListEntry(date: Date(), peopleList: data)
return Timeline(entries: [entry], policy: .atEnd)
}
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
debugPrint(completion)
}, receiveValue: completion)
}
}
struct PeopleInSpaceListEntry: TimelineEntry {
let date: Date
let peopleList: [Assignment]?
}
struct PeopleInSpaceWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
if let peopleList = entry.peopleList {
ForEach(peopleList, id:\.name) { person in
Text("\(person.name) (\(person.craft))").font(.body)
}
}
}
}
}
@main
struct PeopleInSpaceWidget: Widget {
let kind: String = "PeopleInSpaceWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
PeopleInSpaceWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.systemLarge])
}
}

View file

@ -1,5 +1,10 @@
target 'PeopleInSpaceSwiftUI' do
pod 'common', :path => '../../common'
pod 'KMPNativeCoroutinesAsync', '0.11.1'
end
target 'PeopleInSpaceWidgetExtension' do
pod 'common', :path => '../../common'
pod 'KMPNativeCoroutinesCombine', '0.11.1'
end

View file

@ -1,16 +1,32 @@
PODS:
- common (1.0)
- KMPNativeCoroutinesAsync (0.11.1):
- KMPNativeCoroutinesCore (= 0.11.1)
- KMPNativeCoroutinesCombine (0.11.1):
- KMPNativeCoroutinesCore (= 0.11.1)
- KMPNativeCoroutinesCore (0.11.1)
DEPENDENCIES:
- common (from `../../common`)
- KMPNativeCoroutinesAsync (= 0.11.1)
- KMPNativeCoroutinesCombine (= 0.11.1)
SPEC REPOS:
trunk:
- KMPNativeCoroutinesAsync
- KMPNativeCoroutinesCombine
- KMPNativeCoroutinesCore
EXTERNAL SOURCES:
common:
:path: "../../common"
SPEC CHECKSUMS:
common: ed5a58383c5a02f46882243487c7aac341a3064f
common: 5def32d6e7131f79a49997cca9bbcc9cbd11e08a
KMPNativeCoroutinesAsync: 1e6e09efe1fb04a9412483680829dbd35b55ef18
KMPNativeCoroutinesCombine: 43a442a5e50ee8fcbb8633a361d12907fe933920
KMPNativeCoroutinesCore: ed98a12d8337f861088f6b79636ffb29ff9bb2ad
PODFILE CHECKSUM: f7c2bab6802efae18b223d30cb88e54a074a5f69
PODFILE CHECKSUM: d51c9a2ba0fb9109f719acf8878fb54882154ace
COCOAPODS: 1.9.3
COCOAPODS: 1.11.2

View file

@ -0,0 +1 @@
../../../Target Support Files/KMPNativeCoroutinesAsync/KMPNativeCoroutinesAsync-umbrella.h

View file

@ -0,0 +1 @@
../../../Target Support Files/KMPNativeCoroutinesAsync/KMPNativeCoroutinesAsync.modulemap

View file

@ -0,0 +1 @@
../../../Target Support Files/KMPNativeCoroutinesCombine/KMPNativeCoroutinesCombine-umbrella.h

View file

@ -0,0 +1 @@
../../../Target Support Files/KMPNativeCoroutinesCombine/KMPNativeCoroutinesCombine.modulemap

View file

@ -0,0 +1 @@
../../../Target Support Files/KMPNativeCoroutinesCore/KMPNativeCoroutinesCore-umbrella.h

View file

@ -0,0 +1 @@
../../../Target Support Files/KMPNativeCoroutinesCore/KMPNativeCoroutinesCore.modulemap

View file

@ -0,0 +1,69 @@
//
// AsyncFunction.swift
// KMPNativeCoroutinesAsync
//
// Created by Rick Clephas on 13/06/2021.
//
import KMPNativeCoroutinesCore
/// Wraps the `NativeSuspend` in an async function.
/// - Parameter nativeSuspend: The native suspend function to await.
/// - Returns: The result from the `nativeSuspend`.
/// - Throws: Errors thrown by the `nativeSuspend`.
public func asyncFunction<Result, Failure: Error, Unit>(for nativeSuspend: @escaping NativeSuspend<Result, Failure, Unit>) async throws -> Result {
let asyncFunctionActor = AsyncFunctionActor<Result, Unit>()
return try await withTaskCancellationHandler {
Task { await asyncFunctionActor.cancel() }
} operation: {
try await withUnsafeThrowingContinuation { continuation in
Task {
await asyncFunctionActor.setContinuation(continuation)
let nativeCancellable = nativeSuspend({ output, unit in
Task { await asyncFunctionActor.continueWith(result: output) }
return unit
}, { error, unit in
Task { await asyncFunctionActor.continueWith(error: error) }
return unit
})
await asyncFunctionActor.setNativeCancellable(nativeCancellable)
}
}
}
}
internal actor AsyncFunctionActor<Result, Unit> {
private var isCancelled = false
private var nativeCancellable: NativeCancellable<Unit>? = nil
func setNativeCancellable(_ nativeCancellable: @escaping NativeCancellable<Unit>) {
guard !isCancelled else {
_ = nativeCancellable()
return
}
self.nativeCancellable = nativeCancellable
}
func cancel() {
isCancelled = true
_ = nativeCancellable?()
nativeCancellable = nil
}
private var continuation: UnsafeContinuation<Result, Error>? = nil
func setContinuation(_ continuation: UnsafeContinuation<Result, Error>) {
self.continuation = continuation
}
func continueWith(result: Result) {
continuation?.resume(returning: result)
continuation = nil
}
func continueWith(error: Error) {
continuation?.resume(throwing: error)
continuation = nil
}
}

View file

@ -0,0 +1,19 @@
//
// AsyncResult.swift
// KMPNativeCoroutinesAsync
//
// Created by Rick Clephas on 28/06/2021.
//
import KMPNativeCoroutinesCore
/// Awaits the `NativeSuspend` and returns the result.
/// - Parameter nativeSuspend: The native suspend function to await.
/// - Returns: The `Result` from the `nativeSuspend`.
public func asyncResult<Output, Failure: Error, Unit>(for nativeSuspend: @escaping NativeSuspend<Output, Failure, Unit>) async -> Result<Output, Error> {
do {
return .success(try await asyncFunction(for: nativeSuspend))
} catch {
return .failure(error)
}
}

View file

@ -0,0 +1,54 @@
//
// AsyncStream.swift
// AsyncStream
//
// Created by Rick Clephas on 15/07/2021.
//
import KMPNativeCoroutinesCore
/// Wraps the `NativeFlow` in an `AsyncThrowingStream`.
/// - Parameter nativeFlow: The native flow to collect.
/// - Returns: An stream that yields the collected values.
public func asyncStream<Output, Failure: Error, Unit>(for nativeFlow: @escaping NativeFlow<Output, Failure, Unit>) -> AsyncThrowingStream<Output, Error> {
let asyncStreamActor = AsyncStreamActor<Output, Unit>()
return AsyncThrowingStream { continuation in
continuation.onTermination = { @Sendable _ in
Task { await asyncStreamActor.cancel() }
}
Task {
let nativeCancellable = nativeFlow({ item, unit in
continuation.yield(item)
return unit
}, { error, unit in
if let error = error {
continuation.finish(throwing: error)
} else {
continuation.finish(throwing: nil)
}
return unit
})
await asyncStreamActor.setNativeCancellable(nativeCancellable)
}
}
}
internal actor AsyncStreamActor<Output, Unit> {
private var isCancelled = false
private var nativeCancellable: NativeCancellable<Unit>? = nil
func setNativeCancellable(_ nativeCancellable: @escaping NativeCancellable<Unit>) {
guard !isCancelled else {
_ = nativeCancellable()
return
}
self.nativeCancellable = nativeCancellable
}
func cancel() {
isCancelled = true
_ = nativeCancellable?()
nativeCancellable = nil
}
}

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Rick Clephas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,320 @@
# KMP-NativeCoroutines
A library to use Kotlin Coroutines from Swift code in KMP apps.
## Why this library?
Both KMP and Kotlin Coroutines are amazing but together they have some limitations.
The most important limitation is cancellation support.
Kotlin suspend functions are exposed to Swift as functions with a completion handler.
This allows you to easily use them from your Swift code, but it doesn't support cancellation.
> While Swift 5.5 brings async functions to Swift, it doesn't solve this issue.
> For interoperability with ObjC all functions with a completion handler can be called like an async function.
> This means starting with Swift 5.5 your Kotlin suspend functions will look like Swift async functions.
> But that's just syntactic sugar, so there's still no cancellation support.
Besides cancellation support, ObjC doesn't support generics on protocols.
So all the `Flow` interfaces lose their generic value type which make them hard to use.
This library solves both of these limitations 😄.
## Compatibility
> **NOTE:** at the moment the [new Kotlin Native memory model][new-mm] is still experimental.
> The regular versions of this library are therefore currently using the [`-native-mt`][native-mt] versions
> of the kotlinx.coroutines library.
> If you would like to try the new memory model, please use the `-new-mm` versions instead.
[new-mm]: https://github.com/JetBrains/kotlin/blob/0b871d7534a9c8e90fb9ad61cd5345716448d08c/kotlin-native/NEW_MM.md
[native-mt]: https://github.com/kotlin/kotlinx.coroutines/issues/462
As of version `0.10.0` the library uses Kotlin version `1.6.10`.
Compatibility versions for older Kotlin versions are also available:
| Version | Version suffix | Kotlin | Coroutines |
|--------------|-----------------|:----------:|:-------------------:|
| _latest_ | -new-mm | 1.6.10 | 1.6.0 |
| **_latest_** | **_no suffix_** | **1.6.10** | **1.6.0-native-mt** |
| _latest_ | -kotlin-1.6.0 | 1.6.0 | 1.6.0-native-mt |
| 0.9.0 | -new-mm-3 | 1.6.0 | 1.6.0-RC2 |
| 0.8.0 | _no suffix_ | 1.5.30 | 1.5.2-native-mt |
| 0.8.0 | -kotlin-1.5.20 | 1.5.20 | 1.5.0-native-mt |
You can choose from a couple of Swift implementations.
Depending on the implementation you can support as low as iOS 9, macOS 10.9, tvOS 9 and watchOS 3:
| Implementation | Swift | iOS | macOS | tvOS | watchOS |
|----------------|:-----:|:----:|:-----:|:----:|:-------:|
| Async | 5.5 | 13.0 | 10.15 | 13.0 | 6.0 |
| Combine | 5.0 | 13.0 | 10.15 | 13.0 | 6.0 |
| RxSwift | 5.0 | 9.0 | 10.9 | 9.0 | 3.0 |
## Installation
The library consists of a Kotlin and Swift part which you'll need to add to your project.
The Kotlin part is available on Maven Central and the Swift part can be installed via CocoaPods
or the Swift Package Manager.
Make sure to always use the same versions for all the libraries!
[![latest release](https://img.shields.io/github/v/release/rickclephas/KMP-NativeCoroutines?label=latest%20release&sort=semver)](https://github.com/rickclephas/KMP-NativeCoroutines/releases)
### Kotlin
For Kotlin just add the plugin to your `build.gradle.kts`:
```kotlin
plugins {
id("com.rickclephas.kmp.nativecoroutines") version "<version>"
}
```
### Swift (Swift Package Manager)
All Swift implementations are also available via the Swift Package Manager.
Just add it to your `Package.swift` file:
```swift
dependencies: [
.package(url: "https://github.com/rickclephas/KMP-NativeCoroutines.git", from: "<version>")
]
```
Or add it in Xcode by going to `File` > `Add Packages...` and providing the URL:
`https://github.com/rickclephas/KMP-NativeCoroutines.git`.
### Swift (CocoaPods)
Now for Swift you can choose from a couple of implementations.
Add one or more of the following libraries to your `Podfile`:
```ruby
pod 'KMPNativeCoroutinesAsync' # Swift 5.5 Async/Await implementation
pod 'KMPNativeCoroutinesCombine' # Combine implementation
pod 'KMPNativeCoroutinesRxSwift' # RxSwift implementation
```
## Usage
Using your Kotlin Coroutines code from Swift is almost as easy as calling the Kotlin code.
Just use the wrapper functions in Swift to get Observables, Publishers, AsyncStreams or async functions.
### Kotlin
The plugin will automagically generate the necessary code for you! 🔮
Your `Flow` properties/functions get a `Native` version:
```kotlin
class Clock {
// Somewhere in your Kotlin code you define a Flow property
val time: StateFlow<Long> // This can be any kind of Flow
// The plugin will generate this native property for you
val timeNative
get() = time.asNativeFlow()
}
```
In case of a `StateFlow` or `SharedFlow` property you also get a `NativeValue` or `NativeReplayCache` property:
```kotlin
// For the StateFlow defined above the plugin will generate this native value property
val timeNativeValue
get() = time.value
// In case of a SharedFlow the plugin would generate this native replay cache property
val timeNativeReplayCache
get() = time.replayCache
```
The plugin also generates `Native` versions for all your suspend functions:
```kotlin
class RandomLettersGenerator {
// Somewhere in your Kotlin code you define a suspend function
suspend fun getRandomLetters(): String {
// Code to generate some random letters
}
// The plugin will generate this native function for you
fun getRandomLettersNative() =
nativeSuspend { getRandomLetters() }
}
```
#### Global properties and functions
The plugin is currently unable to generate native versions for global properties and functions.
In such cases you have to manually create the native versions in your Kotlin native code.
#### Custom suffix
If you don't like the naming of these generated properties/functions, you can easily change the suffix.
For example add the following to your `build.gradle.kts` to use the suffix `Apple`:
```kotlin
nativeCoroutines {
suffix = "Apple"
}
```
#### Custom CoroutineScope
For more control you can provide a custom `CoroutineScope` with the `NativeCoroutineScope` annotation:
```kotlin
class Clock {
@NativeCoroutineScope
internal val coroutineScope = CoroutineScope(job + Dispatchers.Default)
}
```
If you don't provide a `CoroutineScope` the default scope will be used which is defined as:
```kotlin
@SharedImmutable
internal val defaultCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
```
#### Ignoring declarations
Use the `NativeCoroutinesIgnore` annotation to tell the plugin to ignore a property or function:
```kotlin
@NativeCoroutinesIgnore
val ignoredFlowProperty: Flow<Int>
@NativeCoroutinesIgnore
suspend fun ignoredSuspendFunction() { }
```
### Swift 5.5 Async/Await
The Async implementation provides some functions to get async Swift functions and `AsyncStream`s.
Use the `asyncFunction(for:)` function to get an async function that can be awaited:
```swift
let handle = Task {
do {
let letters = try await asyncFunction(for: randomLettersGenerator.getRandomLettersNative())
print("Got random letters: \(letters)")
} catch {
print("Failed with error: \(error)")
}
}
// To cancel the suspend function just cancel the async task
handle.cancel()
```
or if you don't like these do-catches you can use the `asyncResult(for:)` function:
```swift
let result = await asyncResult(for: randomLettersGenerator.getRandomLettersNative())
if case let .success(letters) = result {
print("Got random letters: \(letters)")
}
```
For `Flow`s there is the `asyncStream(for:)` function to get an `AsyncStream`:
```swift
let handle = Task {
do {
let stream = asyncStream(for: randomLettersGenerator.getRandomLettersFlowNative())
for try await letters in stream {
print("Got random letters: \(letters)")
}
} catch {
print("Failed with error: \(error)")
}
}
// To cancel the flow (collection) just cancel the async task
handle.cancel()
```
### Combine
The Combine implementation provides a couple functions to get an `AnyPublisher` for your Coroutines code.
For your `Flow`s use the `createPublisher(for:)` function:
```swift
// Create an AnyPublisher for your flow
let publisher = createPublisher(for: clock.timeNative)
// Now use this publisher as you would any other
let cancellable = publisher.sink { completion in
print("Received completion: \(completion)")
} receiveValue: { value in
print("Received value: \(value)")
}
// To cancel the flow (collection) just cancel the publisher
cancellable.cancel()
```
For the suspend functions you should use the `createFuture(for:)` function:
```swift
// Create a Future/AnyPublisher for the suspend function
let future = createFuture(for: randomLettersGenerator.getRandomLettersNative())
// Now use this future as you would any other
let cancellable = future.sink { completion in
print("Received completion: \(completion)")
} receiveValue: { value in
print("Received value: \(value)")
}
// To cancel the suspend function just cancel the future
cancellable.cancel()
```
You can also use the `createPublisher(for:)` function for suspend functions that return a `Flow`:
```swift
let publisher = createPublisher(for: randomLettersGenerator.getRandomLettersFlowNative())
```
**Note:** these functions create deferred `AnyPublisher`s.
Meaning every subscription will trigger the collection of the `Flow` or execution of the suspend function.
### RxSwift
The RxSwift implementation provides a couple functions to get an `Observable` or `Single` for your Coroutines code.
For your `Flow`s use the `createObservable(for:)` function:
```swift
// Create an observable for your flow
let observable = createObservable(for: clock.timeNative)
// Now use this observable as you would any other
let disposable = observable.subscribe(onNext: { value in
print("Received value: \(value)")
}, onError: { error in
print("Received error: \(error)")
}, onCompleted: {
print("Observable completed")
}, onDisposed: {
print("Observable disposed")
})
// To cancel the flow (collection) just dispose the subscription
disposable.dispose()
```
For the suspend functions you should use the `createSingle(for:)` function:
```swift
// Create a single for the suspend function
let single = createSingle(for: randomLettersGenerator.getRandomLettersNative())
// Now use this single as you would any other
let disposable = single.subscribe(onSuccess: { value in
print("Received value: \(value)")
}, onFailure: { error in
print("Received error: \(error)")
}, onDisposed: {
print("Single disposed")
})
// To cancel the suspend function just dispose the subscription
disposable.dispose()
```
You can also use the `createObservable(for:)` function for suspend functions that return a `Flow`:
```swift
let observable = createObservable(for: randomLettersGenerator.getRandomLettersFlowNative())
```
**Note:** these functions create deferred `Observable`s and `Single`s.
Meaning every subscription will trigger the collection of the `Flow` or execution of the suspend function.

View file

@ -0,0 +1,60 @@
//
// Future.swift
// KMPNativeCoroutinesCombine
//
// Created by Rick Clephas on 06/06/2021.
//
import Combine
import KMPNativeCoroutinesCore
/// Creates an `AnyPublisher` for the provided `NativeSuspend`.
/// - Parameter nativeSuspend: The native suspend function to await.
/// - Returns: A publisher that either finishes with a single value or fails with an error.
public func createFuture<Result, Failure: Error, Unit>(
for nativeSuspend: @escaping NativeSuspend<Result, Failure, Unit>
) -> AnyPublisher<Result, Failure> {
return NativeSuspendFuture(nativeSuspend: nativeSuspend)
.eraseToAnyPublisher()
}
internal struct NativeSuspendFuture<Result, Failure: Error, Unit>: Publisher {
typealias Output = Result
typealias Failure = Failure
let nativeSuspend: NativeSuspend<Result, Failure, Unit>
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Result == S.Input {
let subscription = NativeSuspendSubscription(nativeSuspend: nativeSuspend, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
internal class NativeSuspendSubscription<Result, Failure, Unit, S: Subscriber>: Subscription where S.Input == Result, S.Failure == Failure {
private var nativeCancellable: NativeCancellable<Unit>? = nil
private var subscriber: S?
init(nativeSuspend: NativeSuspend<Result, Failure, Unit>, subscriber: S) {
self.subscriber = subscriber
nativeCancellable = nativeSuspend({ output, unit in
if let subscriber = self.subscriber {
_ = subscriber.receive(output)
subscriber.receive(completion: .finished)
}
return unit
}, { error, unit in
self.subscriber?.receive(completion: .failure(error))
return unit
})
}
func request(_ demand: Subscribers.Demand) { }
func cancel() {
subscriber = nil
_ = nativeCancellable?()
nativeCancellable = nil
}
}

View file

@ -0,0 +1,20 @@
//
// FuturePublisher.swift
// KMPNativeCoroutinesCombine
//
// Created by Rick Clephas on 28/06/2021.
//
import Combine
import KMPNativeCoroutinesCore
/// Creates an `AnyPublisher` for the provided `NativeSuspend` that returns a `NativeFlow`.
/// - Parameter nativeSuspend: The native suspend function to await.
/// - Returns: A publisher that publishes the collected values.
public func createPublisher<Output, Failure: Error, Unit>(
for nativeSuspend: @escaping NativeSuspend<NativeFlow<Output, Failure, Unit>, Failure, Unit>
) -> AnyPublisher<Output, Failure> {
return createFuture(for: nativeSuspend)
.flatMap { createPublisher(for: $0) }
.eraseToAnyPublisher()
}

View file

@ -0,0 +1,61 @@
//
// Publisher.swift
// KMPNativeCoroutinesCombine
//
// Created by Rick Clephas on 06/06/2021.
//
import Combine
import KMPNativeCoroutinesCore
/// Creates an `AnyPublisher` for the provided `NativeFlow`.
/// - Parameter nativeFlow: The native flow to collect.
/// - Returns: A publisher that publishes the collected values.
public func createPublisher<Output, Failure: Error, Unit>(
for nativeFlow: @escaping NativeFlow<Output, Failure, Unit>
) -> AnyPublisher<Output, Failure> {
return NativeFlowPublisher(nativeFlow: nativeFlow)
.eraseToAnyPublisher()
}
internal struct NativeFlowPublisher<Output, Failure: Error, Unit>: Publisher {
typealias Output = Output
typealias Failure = Failure
let nativeFlow: NativeFlow<Output, Failure, Unit>
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = NativeFlowSubscription(nativeFlow: nativeFlow, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
internal class NativeFlowSubscription<Output, Failure, Unit, S: Subscriber>: Subscription where S.Input == Output, S.Failure == Failure {
private var nativeCancellable: NativeCancellable<Unit>? = nil
private var subscriber: S?
init(nativeFlow: NativeFlow<Output, Failure, Unit>, subscriber: S) {
self.subscriber = subscriber
nativeCancellable = nativeFlow({ item, unit in
_ = self.subscriber?.receive(item)
return unit
}, { error, unit in
if let error = error {
self.subscriber?.receive(completion: .failure(error))
} else {
self.subscriber?.receive(completion: .finished)
}
return unit
})
}
func request(_ demand: Subscribers.Demand) { }
func cancel() {
subscriber = nil
_ = nativeCancellable?()
nativeCancellable = nil
}
}

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Rick Clephas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,320 @@
# KMP-NativeCoroutines
A library to use Kotlin Coroutines from Swift code in KMP apps.
## Why this library?
Both KMP and Kotlin Coroutines are amazing but together they have some limitations.
The most important limitation is cancellation support.
Kotlin suspend functions are exposed to Swift as functions with a completion handler.
This allows you to easily use them from your Swift code, but it doesn't support cancellation.
> While Swift 5.5 brings async functions to Swift, it doesn't solve this issue.
> For interoperability with ObjC all functions with a completion handler can be called like an async function.
> This means starting with Swift 5.5 your Kotlin suspend functions will look like Swift async functions.
> But that's just syntactic sugar, so there's still no cancellation support.
Besides cancellation support, ObjC doesn't support generics on protocols.
So all the `Flow` interfaces lose their generic value type which make them hard to use.
This library solves both of these limitations 😄.
## Compatibility
> **NOTE:** at the moment the [new Kotlin Native memory model][new-mm] is still experimental.
> The regular versions of this library are therefore currently using the [`-native-mt`][native-mt] versions
> of the kotlinx.coroutines library.
> If you would like to try the new memory model, please use the `-new-mm` versions instead.
[new-mm]: https://github.com/JetBrains/kotlin/blob/0b871d7534a9c8e90fb9ad61cd5345716448d08c/kotlin-native/NEW_MM.md
[native-mt]: https://github.com/kotlin/kotlinx.coroutines/issues/462
As of version `0.10.0` the library uses Kotlin version `1.6.10`.
Compatibility versions for older Kotlin versions are also available:
| Version | Version suffix | Kotlin | Coroutines |
|--------------|-----------------|:----------:|:-------------------:|
| _latest_ | -new-mm | 1.6.10 | 1.6.0 |
| **_latest_** | **_no suffix_** | **1.6.10** | **1.6.0-native-mt** |
| _latest_ | -kotlin-1.6.0 | 1.6.0 | 1.6.0-native-mt |
| 0.9.0 | -new-mm-3 | 1.6.0 | 1.6.0-RC2 |
| 0.8.0 | _no suffix_ | 1.5.30 | 1.5.2-native-mt |
| 0.8.0 | -kotlin-1.5.20 | 1.5.20 | 1.5.0-native-mt |
You can choose from a couple of Swift implementations.
Depending on the implementation you can support as low as iOS 9, macOS 10.9, tvOS 9 and watchOS 3:
| Implementation | Swift | iOS | macOS | tvOS | watchOS |
|----------------|:-----:|:----:|:-----:|:----:|:-------:|
| Async | 5.5 | 13.0 | 10.15 | 13.0 | 6.0 |
| Combine | 5.0 | 13.0 | 10.15 | 13.0 | 6.0 |
| RxSwift | 5.0 | 9.0 | 10.9 | 9.0 | 3.0 |
## Installation
The library consists of a Kotlin and Swift part which you'll need to add to your project.
The Kotlin part is available on Maven Central and the Swift part can be installed via CocoaPods
or the Swift Package Manager.
Make sure to always use the same versions for all the libraries!
[![latest release](https://img.shields.io/github/v/release/rickclephas/KMP-NativeCoroutines?label=latest%20release&sort=semver)](https://github.com/rickclephas/KMP-NativeCoroutines/releases)
### Kotlin
For Kotlin just add the plugin to your `build.gradle.kts`:
```kotlin
plugins {
id("com.rickclephas.kmp.nativecoroutines") version "<version>"
}
```
### Swift (Swift Package Manager)
All Swift implementations are also available via the Swift Package Manager.
Just add it to your `Package.swift` file:
```swift
dependencies: [
.package(url: "https://github.com/rickclephas/KMP-NativeCoroutines.git", from: "<version>")
]
```
Or add it in Xcode by going to `File` > `Add Packages...` and providing the URL:
`https://github.com/rickclephas/KMP-NativeCoroutines.git`.
### Swift (CocoaPods)
Now for Swift you can choose from a couple of implementations.
Add one or more of the following libraries to your `Podfile`:
```ruby
pod 'KMPNativeCoroutinesAsync' # Swift 5.5 Async/Await implementation
pod 'KMPNativeCoroutinesCombine' # Combine implementation
pod 'KMPNativeCoroutinesRxSwift' # RxSwift implementation
```
## Usage
Using your Kotlin Coroutines code from Swift is almost as easy as calling the Kotlin code.
Just use the wrapper functions in Swift to get Observables, Publishers, AsyncStreams or async functions.
### Kotlin
The plugin will automagically generate the necessary code for you! 🔮
Your `Flow` properties/functions get a `Native` version:
```kotlin
class Clock {
// Somewhere in your Kotlin code you define a Flow property
val time: StateFlow<Long> // This can be any kind of Flow
// The plugin will generate this native property for you
val timeNative
get() = time.asNativeFlow()
}
```
In case of a `StateFlow` or `SharedFlow` property you also get a `NativeValue` or `NativeReplayCache` property:
```kotlin
// For the StateFlow defined above the plugin will generate this native value property
val timeNativeValue
get() = time.value
// In case of a SharedFlow the plugin would generate this native replay cache property
val timeNativeReplayCache
get() = time.replayCache
```
The plugin also generates `Native` versions for all your suspend functions:
```kotlin
class RandomLettersGenerator {
// Somewhere in your Kotlin code you define a suspend function
suspend fun getRandomLetters(): String {
// Code to generate some random letters
}
// The plugin will generate this native function for you
fun getRandomLettersNative() =
nativeSuspend { getRandomLetters() }
}
```
#### Global properties and functions
The plugin is currently unable to generate native versions for global properties and functions.
In such cases you have to manually create the native versions in your Kotlin native code.
#### Custom suffix
If you don't like the naming of these generated properties/functions, you can easily change the suffix.
For example add the following to your `build.gradle.kts` to use the suffix `Apple`:
```kotlin
nativeCoroutines {
suffix = "Apple"
}
```
#### Custom CoroutineScope
For more control you can provide a custom `CoroutineScope` with the `NativeCoroutineScope` annotation:
```kotlin
class Clock {
@NativeCoroutineScope
internal val coroutineScope = CoroutineScope(job + Dispatchers.Default)
}
```
If you don't provide a `CoroutineScope` the default scope will be used which is defined as:
```kotlin
@SharedImmutable
internal val defaultCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
```
#### Ignoring declarations
Use the `NativeCoroutinesIgnore` annotation to tell the plugin to ignore a property or function:
```kotlin
@NativeCoroutinesIgnore
val ignoredFlowProperty: Flow<Int>
@NativeCoroutinesIgnore
suspend fun ignoredSuspendFunction() { }
```
### Swift 5.5 Async/Await
The Async implementation provides some functions to get async Swift functions and `AsyncStream`s.
Use the `asyncFunction(for:)` function to get an async function that can be awaited:
```swift
let handle = Task {
do {
let letters = try await asyncFunction(for: randomLettersGenerator.getRandomLettersNative())
print("Got random letters: \(letters)")
} catch {
print("Failed with error: \(error)")
}
}
// To cancel the suspend function just cancel the async task
handle.cancel()
```
or if you don't like these do-catches you can use the `asyncResult(for:)` function:
```swift
let result = await asyncResult(for: randomLettersGenerator.getRandomLettersNative())
if case let .success(letters) = result {
print("Got random letters: \(letters)")
}
```
For `Flow`s there is the `asyncStream(for:)` function to get an `AsyncStream`:
```swift
let handle = Task {
do {
let stream = asyncStream(for: randomLettersGenerator.getRandomLettersFlowNative())
for try await letters in stream {
print("Got random letters: \(letters)")
}
} catch {
print("Failed with error: \(error)")
}
}
// To cancel the flow (collection) just cancel the async task
handle.cancel()
```
### Combine
The Combine implementation provides a couple functions to get an `AnyPublisher` for your Coroutines code.
For your `Flow`s use the `createPublisher(for:)` function:
```swift
// Create an AnyPublisher for your flow
let publisher = createPublisher(for: clock.timeNative)
// Now use this publisher as you would any other
let cancellable = publisher.sink { completion in
print("Received completion: \(completion)")
} receiveValue: { value in
print("Received value: \(value)")
}
// To cancel the flow (collection) just cancel the publisher
cancellable.cancel()
```
For the suspend functions you should use the `createFuture(for:)` function:
```swift
// Create a Future/AnyPublisher for the suspend function
let future = createFuture(for: randomLettersGenerator.getRandomLettersNative())
// Now use this future as you would any other
let cancellable = future.sink { completion in
print("Received completion: \(completion)")
} receiveValue: { value in
print("Received value: \(value)")
}
// To cancel the suspend function just cancel the future
cancellable.cancel()
```
You can also use the `createPublisher(for:)` function for suspend functions that return a `Flow`:
```swift
let publisher = createPublisher(for: randomLettersGenerator.getRandomLettersFlowNative())
```
**Note:** these functions create deferred `AnyPublisher`s.
Meaning every subscription will trigger the collection of the `Flow` or execution of the suspend function.
### RxSwift
The RxSwift implementation provides a couple functions to get an `Observable` or `Single` for your Coroutines code.
For your `Flow`s use the `createObservable(for:)` function:
```swift
// Create an observable for your flow
let observable = createObservable(for: clock.timeNative)
// Now use this observable as you would any other
let disposable = observable.subscribe(onNext: { value in
print("Received value: \(value)")
}, onError: { error in
print("Received error: \(error)")
}, onCompleted: {
print("Observable completed")
}, onDisposed: {
print("Observable disposed")
})
// To cancel the flow (collection) just dispose the subscription
disposable.dispose()
```
For the suspend functions you should use the `createSingle(for:)` function:
```swift
// Create a single for the suspend function
let single = createSingle(for: randomLettersGenerator.getRandomLettersNative())
// Now use this single as you would any other
let disposable = single.subscribe(onSuccess: { value in
print("Received value: \(value)")
}, onFailure: { error in
print("Received error: \(error)")
}, onDisposed: {
print("Single disposed")
})
// To cancel the suspend function just dispose the subscription
disposable.dispose()
```
You can also use the `createObservable(for:)` function for suspend functions that return a `Flow`:
```swift
let observable = createObservable(for: randomLettersGenerator.getRandomLettersFlowNative())
```
**Note:** these functions create deferred `Observable`s and `Single`s.
Meaning every subscription will trigger the collection of the `Flow` or execution of the suspend function.

View file

@ -0,0 +1,12 @@
//
// NativeCallback.swift
// KMPNativeCoroutinesCore
//
// Created by Rick Clephas on 06/06/2021.
//
/// A callback with a single argument.
///
/// The return value is provided as the second argument.
/// This way Swift doesn't known what it is/how to get it.
public typealias NativeCallback<T, Unit> = (T, Unit) -> Unit

View file

@ -0,0 +1,9 @@
//
// NativeCancellable.swift
// KMPNativeCoroutinesCore
//
// Created by Rick Clephas on 06/06/2021.
//
/// A function that cancels the coroutines job.
public typealias NativeCancellable<Unit> = () -> Unit

View file

@ -0,0 +1,15 @@
//
// NativeFlow.swift
// KMPNativeCoroutinesCore
//
// Created by Rick Clephas on 06/06/2021.
//
/// A function that collects a Kotlin coroutines Flow via callbacks.
///
/// The function takes an `onItem` and `onComplete` callback
/// and returns a cancellable that can be used to cancel the collection.
public typealias NativeFlow<Output, Failure: Error, Unit> = (
_ onItem: @escaping NativeCallback<Output, Unit>,
_ onComplete: @escaping NativeCallback<Failure?, Unit>
) -> NativeCancellable<Unit>

View file

@ -0,0 +1,15 @@
//
// NativeSuspend.swift
// KMPNativeCoroutinesCore
//
// Created by Rick Clephas on 06/06/2021.
//
/// A function that awaits a suspend function via callbacks.
///
/// The function takes an `onResult` and `onError` callback
/// and returns a cancellable that can be used to cancel the suspend function.
public typealias NativeSuspend<Result, Failure: Error, Unit> = (
_ onResult: @escaping NativeCallback<Result, Unit>,
_ onError: @escaping NativeCallback<Failure, Unit>
) -> NativeCancellable<Unit>

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Rick Clephas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,320 @@
# KMP-NativeCoroutines
A library to use Kotlin Coroutines from Swift code in KMP apps.
## Why this library?
Both KMP and Kotlin Coroutines are amazing but together they have some limitations.
The most important limitation is cancellation support.
Kotlin suspend functions are exposed to Swift as functions with a completion handler.
This allows you to easily use them from your Swift code, but it doesn't support cancellation.
> While Swift 5.5 brings async functions to Swift, it doesn't solve this issue.
> For interoperability with ObjC all functions with a completion handler can be called like an async function.
> This means starting with Swift 5.5 your Kotlin suspend functions will look like Swift async functions.
> But that's just syntactic sugar, so there's still no cancellation support.
Besides cancellation support, ObjC doesn't support generics on protocols.
So all the `Flow` interfaces lose their generic value type which make them hard to use.
This library solves both of these limitations 😄.
## Compatibility
> **NOTE:** at the moment the [new Kotlin Native memory model][new-mm] is still experimental.
> The regular versions of this library are therefore currently using the [`-native-mt`][native-mt] versions
> of the kotlinx.coroutines library.
> If you would like to try the new memory model, please use the `-new-mm` versions instead.
[new-mm]: https://github.com/JetBrains/kotlin/blob/0b871d7534a9c8e90fb9ad61cd5345716448d08c/kotlin-native/NEW_MM.md
[native-mt]: https://github.com/kotlin/kotlinx.coroutines/issues/462
As of version `0.10.0` the library uses Kotlin version `1.6.10`.
Compatibility versions for older Kotlin versions are also available:
| Version | Version suffix | Kotlin | Coroutines |
|--------------|-----------------|:----------:|:-------------------:|
| _latest_ | -new-mm | 1.6.10 | 1.6.0 |
| **_latest_** | **_no suffix_** | **1.6.10** | **1.6.0-native-mt** |
| _latest_ | -kotlin-1.6.0 | 1.6.0 | 1.6.0-native-mt |
| 0.9.0 | -new-mm-3 | 1.6.0 | 1.6.0-RC2 |
| 0.8.0 | _no suffix_ | 1.5.30 | 1.5.2-native-mt |
| 0.8.0 | -kotlin-1.5.20 | 1.5.20 | 1.5.0-native-mt |
You can choose from a couple of Swift implementations.
Depending on the implementation you can support as low as iOS 9, macOS 10.9, tvOS 9 and watchOS 3:
| Implementation | Swift | iOS | macOS | tvOS | watchOS |
|----------------|:-----:|:----:|:-----:|:----:|:-------:|
| Async | 5.5 | 13.0 | 10.15 | 13.0 | 6.0 |
| Combine | 5.0 | 13.0 | 10.15 | 13.0 | 6.0 |
| RxSwift | 5.0 | 9.0 | 10.9 | 9.0 | 3.0 |
## Installation
The library consists of a Kotlin and Swift part which you'll need to add to your project.
The Kotlin part is available on Maven Central and the Swift part can be installed via CocoaPods
or the Swift Package Manager.
Make sure to always use the same versions for all the libraries!
[![latest release](https://img.shields.io/github/v/release/rickclephas/KMP-NativeCoroutines?label=latest%20release&sort=semver)](https://github.com/rickclephas/KMP-NativeCoroutines/releases)
### Kotlin
For Kotlin just add the plugin to your `build.gradle.kts`:
```kotlin
plugins {
id("com.rickclephas.kmp.nativecoroutines") version "<version>"
}
```
### Swift (Swift Package Manager)
All Swift implementations are also available via the Swift Package Manager.
Just add it to your `Package.swift` file:
```swift
dependencies: [
.package(url: "https://github.com/rickclephas/KMP-NativeCoroutines.git", from: "<version>")
]
```
Or add it in Xcode by going to `File` > `Add Packages...` and providing the URL:
`https://github.com/rickclephas/KMP-NativeCoroutines.git`.
### Swift (CocoaPods)
Now for Swift you can choose from a couple of implementations.
Add one or more of the following libraries to your `Podfile`:
```ruby
pod 'KMPNativeCoroutinesAsync' # Swift 5.5 Async/Await implementation
pod 'KMPNativeCoroutinesCombine' # Combine implementation
pod 'KMPNativeCoroutinesRxSwift' # RxSwift implementation
```
## Usage
Using your Kotlin Coroutines code from Swift is almost as easy as calling the Kotlin code.
Just use the wrapper functions in Swift to get Observables, Publishers, AsyncStreams or async functions.
### Kotlin
The plugin will automagically generate the necessary code for you! 🔮
Your `Flow` properties/functions get a `Native` version:
```kotlin
class Clock {
// Somewhere in your Kotlin code you define a Flow property
val time: StateFlow<Long> // This can be any kind of Flow
// The plugin will generate this native property for you
val timeNative
get() = time.asNativeFlow()
}
```
In case of a `StateFlow` or `SharedFlow` property you also get a `NativeValue` or `NativeReplayCache` property:
```kotlin
// For the StateFlow defined above the plugin will generate this native value property
val timeNativeValue
get() = time.value
// In case of a SharedFlow the plugin would generate this native replay cache property
val timeNativeReplayCache
get() = time.replayCache
```
The plugin also generates `Native` versions for all your suspend functions:
```kotlin
class RandomLettersGenerator {
// Somewhere in your Kotlin code you define a suspend function
suspend fun getRandomLetters(): String {
// Code to generate some random letters
}
// The plugin will generate this native function for you
fun getRandomLettersNative() =
nativeSuspend { getRandomLetters() }
}
```
#### Global properties and functions
The plugin is currently unable to generate native versions for global properties and functions.
In such cases you have to manually create the native versions in your Kotlin native code.
#### Custom suffix
If you don't like the naming of these generated properties/functions, you can easily change the suffix.
For example add the following to your `build.gradle.kts` to use the suffix `Apple`:
```kotlin
nativeCoroutines {
suffix = "Apple"
}
```
#### Custom CoroutineScope
For more control you can provide a custom `CoroutineScope` with the `NativeCoroutineScope` annotation:
```kotlin
class Clock {
@NativeCoroutineScope
internal val coroutineScope = CoroutineScope(job + Dispatchers.Default)
}
```
If you don't provide a `CoroutineScope` the default scope will be used which is defined as:
```kotlin
@SharedImmutable
internal val defaultCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
```
#### Ignoring declarations
Use the `NativeCoroutinesIgnore` annotation to tell the plugin to ignore a property or function:
```kotlin
@NativeCoroutinesIgnore
val ignoredFlowProperty: Flow<Int>
@NativeCoroutinesIgnore
suspend fun ignoredSuspendFunction() { }
```
### Swift 5.5 Async/Await
The Async implementation provides some functions to get async Swift functions and `AsyncStream`s.
Use the `asyncFunction(for:)` function to get an async function that can be awaited:
```swift
let handle = Task {
do {
let letters = try await asyncFunction(for: randomLettersGenerator.getRandomLettersNative())
print("Got random letters: \(letters)")
} catch {
print("Failed with error: \(error)")
}
}
// To cancel the suspend function just cancel the async task
handle.cancel()
```
or if you don't like these do-catches you can use the `asyncResult(for:)` function:
```swift
let result = await asyncResult(for: randomLettersGenerator.getRandomLettersNative())
if case let .success(letters) = result {
print("Got random letters: \(letters)")
}
```
For `Flow`s there is the `asyncStream(for:)` function to get an `AsyncStream`:
```swift
let handle = Task {
do {
let stream = asyncStream(for: randomLettersGenerator.getRandomLettersFlowNative())
for try await letters in stream {
print("Got random letters: \(letters)")
}
} catch {
print("Failed with error: \(error)")
}
}
// To cancel the flow (collection) just cancel the async task
handle.cancel()
```
### Combine
The Combine implementation provides a couple functions to get an `AnyPublisher` for your Coroutines code.
For your `Flow`s use the `createPublisher(for:)` function:
```swift
// Create an AnyPublisher for your flow
let publisher = createPublisher(for: clock.timeNative)
// Now use this publisher as you would any other
let cancellable = publisher.sink { completion in
print("Received completion: \(completion)")
} receiveValue: { value in
print("Received value: \(value)")
}
// To cancel the flow (collection) just cancel the publisher
cancellable.cancel()
```
For the suspend functions you should use the `createFuture(for:)` function:
```swift
// Create a Future/AnyPublisher for the suspend function
let future = createFuture(for: randomLettersGenerator.getRandomLettersNative())
// Now use this future as you would any other
let cancellable = future.sink { completion in
print("Received completion: \(completion)")
} receiveValue: { value in
print("Received value: \(value)")
}
// To cancel the suspend function just cancel the future
cancellable.cancel()
```
You can also use the `createPublisher(for:)` function for suspend functions that return a `Flow`:
```swift
let publisher = createPublisher(for: randomLettersGenerator.getRandomLettersFlowNative())
```
**Note:** these functions create deferred `AnyPublisher`s.
Meaning every subscription will trigger the collection of the `Flow` or execution of the suspend function.
### RxSwift
The RxSwift implementation provides a couple functions to get an `Observable` or `Single` for your Coroutines code.
For your `Flow`s use the `createObservable(for:)` function:
```swift
// Create an observable for your flow
let observable = createObservable(for: clock.timeNative)
// Now use this observable as you would any other
let disposable = observable.subscribe(onNext: { value in
print("Received value: \(value)")
}, onError: { error in
print("Received error: \(error)")
}, onCompleted: {
print("Observable completed")
}, onDisposed: {
print("Observable disposed")
})
// To cancel the flow (collection) just dispose the subscription
disposable.dispose()
```
For the suspend functions you should use the `createSingle(for:)` function:
```swift
// Create a single for the suspend function
let single = createSingle(for: randomLettersGenerator.getRandomLettersNative())
// Now use this single as you would any other
let disposable = single.subscribe(onSuccess: { value in
print("Received value: \(value)")
}, onFailure: { error in
print("Received error: \(error)")
}, onDisposed: {
print("Single disposed")
})
// To cancel the suspend function just dispose the subscription
disposable.dispose()
```
You can also use the `createObservable(for:)` function for suspend functions that return a `Flow`:
```swift
let observable = createObservable(for: randomLettersGenerator.getRandomLettersFlowNative())
```
**Note:** these functions create deferred `Observable`s and `Single`s.
Meaning every subscription will trigger the collection of the `Flow` or execution of the suspend function.

View file

@ -21,7 +21,7 @@
"name": "Build common",
"execution_position": "before_compile",
"shell_path": "/bin/sh",
"script": " if [ \"YES\" = \"$COCOAPODS_SKIP_KOTLIN_BUILD\" ]; then\n echo \"Skipping Gradle build task invocation due to COCOAPODS_SKIP_KOTLIN_BUILD environment variable set to \"YES\"\"\n exit 0\n fi\n set -ev\n REPO_ROOT=\"$PODS_TARGET_SRCROOT\"\n \"$REPO_ROOT/../gradlew\" -p \"$REPO_ROOT\" $KOTLIN_PROJECT_PATH:syncFramework -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME -Pkotlin.native.cocoapods.archs=\"$ARCHS\" -Pkotlin.native.cocoapods.configuration=$CONFIGURATION -Pkotlin.native.cocoapods.cflags=\"$OTHER_CFLAGS\" -Pkotlin.native.cocoapods.paths.headers=\"$HEADER_SEARCH_PATHS\" -Pkotlin.native.cocoapods.paths.frameworks=\"$FRAMEWORK_SEARCH_PATHS\"\n"
"script": " if [ \"YES\" = \"$COCOAPODS_SKIP_KOTLIN_BUILD\" ]; then\n echo \"Skipping Gradle build task invocation due to COCOAPODS_SKIP_KOTLIN_BUILD environment variable set to \"YES\"\"\n exit 0\n fi\n set -ev\n REPO_ROOT=\"$PODS_TARGET_SRCROOT\"\n \"$REPO_ROOT/../gradlew\" -p \"$REPO_ROOT\" $KOTLIN_PROJECT_PATH:syncFramework -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME -Pkotlin.native.cocoapods.archs=\"$ARCHS\" -Pkotlin.native.cocoapods.configuration=$CONFIGURATION\n"
}
],
"platforms": {

View file

@ -1,16 +1,32 @@
PODS:
- common (1.0)
- KMPNativeCoroutinesAsync (0.11.1):
- KMPNativeCoroutinesCore (= 0.11.1)
- KMPNativeCoroutinesCombine (0.11.1):
- KMPNativeCoroutinesCore (= 0.11.1)
- KMPNativeCoroutinesCore (0.11.1)
DEPENDENCIES:
- common (from `../../common`)
- KMPNativeCoroutinesAsync (= 0.11.1)
- KMPNativeCoroutinesCombine (= 0.11.1)
SPEC REPOS:
trunk:
- KMPNativeCoroutinesAsync
- KMPNativeCoroutinesCombine
- KMPNativeCoroutinesCore
EXTERNAL SOURCES:
common:
:path: "../../common"
SPEC CHECKSUMS:
common: ed5a58383c5a02f46882243487c7aac341a3064f
common: 5def32d6e7131f79a49997cca9bbcc9cbd11e08a
KMPNativeCoroutinesAsync: 1e6e09efe1fb04a9412483680829dbd35b55ef18
KMPNativeCoroutinesCombine: 43a442a5e50ee8fcbb8633a361d12907fe933920
KMPNativeCoroutinesCore: ed98a12d8337f861088f6b79636ffb29ff9bb2ad
PODFILE CHECKSUM: f7c2bab6802efae18b223d30cb88e54a074a5f69
PODFILE CHECKSUM: d51c9a2ba0fb9109f719acf8878fb54882154ace
COCOAPODS: 1.9.3
COCOAPODS: 1.11.2

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1100"
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForAnalyzing = "YES"
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES">
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6694FA49269F5170E337048A97626853"
@ -23,14 +23,15 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Debug">
<AdditionalOptions>
</AdditionalOptions>
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
@ -38,17 +39,14 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
buildConfiguration = "Debug"
allowLocationSimulation = "YES">
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES">
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1100"
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForAnalyzing = "YES"
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES">
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8217FBB9D1218C346C0781D0B8F2BBE8"
@ -23,14 +23,15 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Debug">
<AdditionalOptions>
</AdditionalOptions>
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
@ -38,17 +39,14 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
buildConfiguration = "Debug"
allowLocationSimulation = "YES">
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES">
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">

View file

@ -4,13 +4,41 @@
<dict>
<key>SchemeUserState</key>
<dict>
<key>Pods-PeopleInSpaceSwiftUI.xcscheme</key>
<key>KMPNativeCoroutinesAsync.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>KMPNativeCoroutinesCombine.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>KMPNativeCoroutinesCore.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>3</integer>
</dict>
<key>Pods-PeopleInSpaceSwiftUI.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>4</integer>
</dict>
<key>Pods-PeopleInSpaceWidgetExtension.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>5</integer>
</dict>
<key>common.xcscheme</key>
<dict>
<key>isShown</key>

View file

@ -0,0 +1,5 @@
#import <Foundation/Foundation.h>
@interface PodsDummy_KMPNativeCoroutinesAsync : NSObject
@end
@implementation PodsDummy_KMPNativeCoroutinesAsync
@end

View file

@ -0,0 +1,12 @@
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif

View file

@ -0,0 +1,16 @@
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
FOUNDATION_EXPORT double KMPNativeCoroutinesAsyncVersionNumber;
FOUNDATION_EXPORT const unsigned char KMPNativeCoroutinesAsyncVersionString[];

View file

@ -0,0 +1,14 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesAsync
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
OTHER_CFLAGS = $(inherited) -fmodule-map-file="${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore/KMPNativeCoroutinesCore.modulemap"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -Xcc -fmodule-map-file="${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore/KMPNativeCoroutinesCore.modulemap" -import-underlying-module -Xcc -fmodule-map-file="${SRCROOT}/${MODULEMAP_FILE}"
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/KMPNativeCoroutinesAsync
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES
SWIFT_INCLUDE_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore"
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES

View file

@ -0,0 +1,6 @@
module KMPNativeCoroutinesAsync {
umbrella header "KMPNativeCoroutinesAsync-umbrella.h"
export *
module * { export * }
}

View file

@ -0,0 +1,14 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesAsync
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
OTHER_CFLAGS = $(inherited) -fmodule-map-file="${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore/KMPNativeCoroutinesCore.modulemap"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -Xcc -fmodule-map-file="${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore/KMPNativeCoroutinesCore.modulemap" -import-underlying-module -Xcc -fmodule-map-file="${SRCROOT}/${MODULEMAP_FILE}"
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/KMPNativeCoroutinesAsync
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES
SWIFT_INCLUDE_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore"
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES

View file

@ -0,0 +1,5 @@
#import <Foundation/Foundation.h>
@interface PodsDummy_KMPNativeCoroutinesCombine : NSObject
@end
@implementation PodsDummy_KMPNativeCoroutinesCombine
@end

View file

@ -0,0 +1,12 @@
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif

View file

@ -0,0 +1,16 @@
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
FOUNDATION_EXPORT double KMPNativeCoroutinesCombineVersionNumber;
FOUNDATION_EXPORT const unsigned char KMPNativeCoroutinesCombineVersionString[];

View file

@ -0,0 +1,15 @@
APPLICATION_EXTENSION_API_ONLY = YES
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCombine
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
OTHER_CFLAGS = $(inherited) -fmodule-map-file="${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore/KMPNativeCoroutinesCore.modulemap"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -Xcc -fmodule-map-file="${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore/KMPNativeCoroutinesCore.modulemap" -import-underlying-module -Xcc -fmodule-map-file="${SRCROOT}/${MODULEMAP_FILE}"
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/KMPNativeCoroutinesCombine
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES
SWIFT_INCLUDE_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore"
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES

View file

@ -0,0 +1,6 @@
module KMPNativeCoroutinesCombine {
umbrella header "KMPNativeCoroutinesCombine-umbrella.h"
export *
module * { export * }
}

View file

@ -0,0 +1,15 @@
APPLICATION_EXTENSION_API_ONLY = YES
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCombine
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
OTHER_CFLAGS = $(inherited) -fmodule-map-file="${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore/KMPNativeCoroutinesCore.modulemap"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -Xcc -fmodule-map-file="${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore/KMPNativeCoroutinesCore.modulemap" -import-underlying-module -Xcc -fmodule-map-file="${SRCROOT}/${MODULEMAP_FILE}"
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/KMPNativeCoroutinesCombine
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES
SWIFT_INCLUDE_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KMPNativeCoroutinesCore"
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES

Some files were not shown because too many files have changed in this diff Show more