Compare commits

...

24 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
40 changed files with 414 additions and 222 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

@ -29,6 +29,7 @@ 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
@ -66,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.
@ -75,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

View file

@ -64,8 +64,9 @@ dependencies {
implementation(activityCompose)
}
implementation("androidx.glance:glance-appwidget:1.0.0-SNAPSHOT")
with(Deps.Glance) {
implementation(appwidget)
}
with(Deps.Compose) {
implementation(compiler)
@ -79,8 +80,6 @@ dependencies {
implementation(uiTooling)
}
with(Deps.Koin) {
implementation(core)
implementation(android)

View file

@ -7,13 +7,14 @@ 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.action.actionLaunchActivity
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.ImageProvider
import androidx.glance.layout.Text
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
@ -81,12 +82,12 @@ class ISSMapWidget : BaseGlanceAppWidget<ISSMapWidget.Data>() {
override fun Content(data: Data?) {
Box(
modifier = GlanceModifier.background(Color.DarkGray).fillMaxSize().clickable(
actionLaunchActivity<MainActivity>()
actionStartActivity<MainActivity>()
)
) {
val bitmap = data?.bitmap
if (bitmap != null) {
androidx.glance.layout.Image(
Image(
modifier = GlanceModifier.fillMaxSize(),
provider = ImageProvider(bitmap),
contentDescription = "ISS Location"

View file

@ -7,13 +7,12 @@ 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.layout.LazyColumn
import androidx.glance.appwidget.layout.items
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.background
import androidx.glance.layout.Row
import androidx.glance.layout.Text
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
@ -49,10 +48,10 @@ class PeopleInSpaceWidget : BaseGlanceAppWidget<PeopleInSpaceWidget.Data>() {
)
}
if (data != null) {
items(data.people) {
items(data.people.size) {
Row {
Text(
text = it.name,
text = data.people[it].name,
style = TextStyle(
color = ColorProvider(Color.White),
fontSize = TextUnit(10f, TextUnitType.Sp)

View file

@ -7,10 +7,10 @@ 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 androidx.glance.appwidget.GlanceId
import androidx.glance.appwidget.LocalGlanceId
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull

View file

@ -3,12 +3,8 @@ package com.surrus.peopleinspace.ui
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Icon
@ -99,11 +95,11 @@ fun MainLayout() {
AnimatedNavHost(navController, startDestination = Screen.PersonList.title) {
composable(
route = Screen.PersonList.title,
exitTransition = { _, target ->
exitTransition = {
slideOutHorizontally() +
fadeOut(animationSpec = tween(1000))
},
popEnterTransition = { _, _ ->
popEnterTransition = {
slideInHorizontally()
}
) {
@ -116,11 +112,11 @@ fun MainLayout() {
}
composable(
route = Screen.PersonDetails.title + "/{person}",
enterTransition = { _, _ ->
enterTransition = {
slideInHorizontally() +
fadeIn(animationSpec = tween(1000))
},
popExitTransition = { _, _ ->
popExitTransition = {
slideOutHorizontally()
}
) { backStackEntry ->

View file

@ -11,7 +11,7 @@ buildscript {
dependencies {
// keeping this here to allow AS to automatically update
classpath("com.android.tools.build:gradle:7.0.4")
classpath("com.android.tools.build:gradle:7.1.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-serialization:${kotlinVersion}")
@ -20,8 +20,7 @@ buildscript {
classpath(shadow)
classpath(kotlinter)
classpath(gradleVersionsPlugin)
val kmpNativeCoroutinesVersion = if (kotlinVersion == "1.6.10") "0.10.0-new-mm" else "0.8.0"
classpath("com.rickclephas.kmp:kmp-nativecoroutines-gradle-plugin:$kmpNativeCoroutinesVersion")
classpath("com.rickclephas.kmp:kmp-nativecoroutines-gradle-plugin:${Versions.kmpNativeCoroutinesVersion}")
}
}
}
@ -34,8 +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://androidx.dev/snapshots/builds/7888785/artifacts/repository")
maven(url = "https://maven.pkg.jetbrains.space/public/p/kotlinx-coroutines/maven")
maven(url = "https://maven.pkg.jetbrains.space/public/p/ktor/eap")
}
}
// 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,17 +3,21 @@ object Versions {
const val androidCompileSdk = 31
const val androidTargetSdk = androidCompileSdk
const val kotlinCoroutines = "1.6.0-RC3"
const val kotlinCoroutines = "1.6.0"
const val koin = "3.1.4"
const val ktor = "2.0.0-eap-278"
const val kotlinxSerialization = "1.3.1"
const val ktor = "2.0.0-beta-1"
const val kotlinxSerialization = "1.3.2"
const val kotlinxHtmlJs = "0.7.3"
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.21.0-beta"
const val accompanist = "0.22.0-rc"
const val composeDesktopWeb = "1.0.1"
const val junit = "4.12"
const val androidXTestJUnit = "1.1.3"
@ -55,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}"
}
@ -151,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

@ -71,7 +71,7 @@ kotlin {
}
with(Deps.Kotlinx) {
implementation(Deps.Kotlinx.coroutinesCore)
implementation(coroutinesCore)
implementation(serializationCore)
}
@ -90,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 {

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

@ -75,12 +75,9 @@ class PeopleInSpaceRepository : KoinComponent, PeopleInSpaceRepositoryInterface
override suspend fun fetchPeople(): List<Assignment> = peopleInSpaceApi.fetchPeople().people
override fun pollISSPosition(): Flow<IssPosition> {
// The returned will be frozen in Kotlin Native. We can't freeze the Koin internals
// so we'll use local variables to prevent the Koin internals from freezing.
val api = peopleInSpaceApi
return flow {
while (true) {
val position = api.fetchISSPosition().iss_position
val position = peopleInSpaceApi.fetchISSPosition().iss_position
emit(position)
logger.d { position.toString() }
delay(POLL_INTERVAL)

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,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

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

View file

@ -1,6 +1,6 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose") version "1.0.1-rc2"
id("org.jetbrains.compose") version Versions.composeDesktopWeb
}
version = "1.0"

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

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

View file

@ -1,15 +1,15 @@
PODS:
- common (1.0)
- KMPNativeCoroutinesAsync (0.10.0):
- KMPNativeCoroutinesCore (= 0.10.0)
- KMPNativeCoroutinesCombine (0.10.0):
- KMPNativeCoroutinesCore (= 0.10.0)
- KMPNativeCoroutinesCore (0.10.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.10.0)
- KMPNativeCoroutinesCombine (= 0.10.0)
- KMPNativeCoroutinesAsync (= 0.11.1)
- KMPNativeCoroutinesCombine (= 0.11.1)
SPEC REPOS:
trunk:
@ -23,10 +23,10 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
common: 5def32d6e7131f79a49997cca9bbcc9cbd11e08a
KMPNativeCoroutinesAsync: 66512d0bf4933d0b160416795284beb8ecf089b8
KMPNativeCoroutinesCombine: 04db768364187c30210303b3bdf0731e89ccfeb5
KMPNativeCoroutinesCore: 2e2573a75f27178d4cbd7be385f0f0a54416a47a
KMPNativeCoroutinesAsync: 1e6e09efe1fb04a9412483680829dbd35b55ef18
KMPNativeCoroutinesCombine: 43a442a5e50ee8fcbb8633a361d12907fe933920
KMPNativeCoroutinesCore: ed98a12d8337f861088f6b79636ffb29ff9bb2ad
PODFILE CHECKSUM: d3402fa215303d8817450a84072c6d7e3d30d094
PODFILE CHECKSUM: d51c9a2ba0fb9109f719acf8878fb54882154ace
COCOAPODS: 1.11.2

View file

@ -22,14 +22,22 @@ 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 and early access Kotlin versions are also available:
Compatibility versions for older Kotlin versions are also available:
| Version | Version suffix | Kotlin | Coroutines |
|--------------|-----------------|:----------:|:-------------------:|
| _latest_ | -new-mm | 1.6.10 | 1.6.0-RC3 |
| **_latest_** | **_no suffix_** | **1.6.10** | **1.5.2-native-mt** |
| _latest_ | -kotlin-1.6.0 | 1.6.0 | 1.5.2-native-mt |
| _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 |

View file

@ -22,14 +22,22 @@ 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 and early access Kotlin versions are also available:
Compatibility versions for older Kotlin versions are also available:
| Version | Version suffix | Kotlin | Coroutines |
|--------------|-----------------|:----------:|:-------------------:|
| _latest_ | -new-mm | 1.6.10 | 1.6.0-RC3 |
| **_latest_** | **_no suffix_** | **1.6.10** | **1.5.2-native-mt** |
| _latest_ | -kotlin-1.6.0 | 1.6.0 | 1.5.2-native-mt |
| _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 |

View file

@ -22,14 +22,22 @@ 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 and early access Kotlin versions are also available:
Compatibility versions for older Kotlin versions are also available:
| Version | Version suffix | Kotlin | Coroutines |
|--------------|-----------------|:----------:|:-------------------:|
| _latest_ | -new-mm | 1.6.10 | 1.6.0-RC3 |
| **_latest_** | **_no suffix_** | **1.6.10** | **1.5.2-native-mt** |
| _latest_ | -kotlin-1.6.0 | 1.6.0 | 1.5.2-native-mt |
| _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 |

View file

@ -1,15 +1,15 @@
PODS:
- common (1.0)
- KMPNativeCoroutinesAsync (0.10.0):
- KMPNativeCoroutinesCore (= 0.10.0)
- KMPNativeCoroutinesCombine (0.10.0):
- KMPNativeCoroutinesCore (= 0.10.0)
- KMPNativeCoroutinesCore (0.10.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.10.0)
- KMPNativeCoroutinesCombine (= 0.10.0)
- KMPNativeCoroutinesAsync (= 0.11.1)
- KMPNativeCoroutinesCombine (= 0.11.1)
SPEC REPOS:
trunk:
@ -23,10 +23,10 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
common: 5def32d6e7131f79a49997cca9bbcc9cbd11e08a
KMPNativeCoroutinesAsync: 66512d0bf4933d0b160416795284beb8ecf089b8
KMPNativeCoroutinesCombine: 04db768364187c30210303b3bdf0731e89ccfeb5
KMPNativeCoroutinesCore: 2e2573a75f27178d4cbd7be385f0f0a54416a47a
KMPNativeCoroutinesAsync: 1e6e09efe1fb04a9412483680829dbd35b55ef18
KMPNativeCoroutinesCombine: 43a442a5e50ee8fcbb8633a361d12907fe933920
KMPNativeCoroutinesCore: ed98a12d8337f861088f6b79636ffb29ff9bb2ad
PODFILE CHECKSUM: d3402fa215303d8817450a84072c6d7e3d30d094
PODFILE CHECKSUM: d51c9a2ba0fb9109f719acf8878fb54882154ace
COCOAPODS: 1.11.2

View file

@ -2,14 +2,21 @@ pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
resolutionStrategy {
eachPlugin {
if (requested.id.id.startsWith("com.google.cloud.tools.appengine")) {
useModule("com.google.cloud.tools:appengine-gradle-plugin:${requested.version}")
}
}
}
}
rootProject.name = "PeopleInSpace"
include(":app", ":common", ":compose-desktop")
include(":wearApp")
include(":web")
include(":compose-web")
include(":backend")
include(":wearApp")
include(":graphql-server")

View file

@ -85,7 +85,9 @@ dependencies {
debugImplementation(composeUiTestManifest)
}
implementation("androidx.glance:glance-wear:1.0.0-SNAPSHOT")
with(Deps.Glance) {
implementation(tiles)
}
implementation(project(":common"))
}

View file

@ -34,16 +34,11 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
val rotaryEventDispatcher = RotaryEventDispatcher()
CompositionLocalProvider(
LocalImageLoader provides imageLoader,
LocalRotaryEventDispatcher provides rotaryEventDispatcher,
) {
val navController = rememberSwipeDismissableNavController()
RotaryEventHandlerSetup(rotaryEventDispatcher)
SwipeDismissableNavHost(
navController = navController,
startDestination = Screen.PersonList.route

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -50,7 +51,6 @@ fun PersonDetailsScreen(personName: String) {
}
val scrollState = rememberScrollState()
RotaryEventState(scrollState)
PersonDetailsScreen(person, scrollState)
}
@ -67,10 +67,15 @@ private fun PersonDetailsScreen(
Vignette(vignettePosition = VignettePosition.Bottom)
}
},
positionIndicator = { PositionIndicator(scrollState = scrollState) }
positionIndicator = {
if (person != null) {
PositionIndicator(scrollState = scrollState)
}
}
) {
Column(
modifier = Modifier
.rotaryEventHandler(scrollState)
.fillMaxSize()
.padding(horizontal = if (LocalConfiguration.current.isScreenRound) 18.dp else 8.dp)
.verticalScroll(scrollState),

View file

@ -13,11 +13,13 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -35,9 +37,13 @@ import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.Card
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.PositionIndicator
import androidx.wear.compose.material.Scaffold
import androidx.wear.compose.material.ScalingLazyColumn
import androidx.wear.compose.material.ScalingLazyListState
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.Vignette
import androidx.wear.compose.material.VignettePosition
import androidx.wear.compose.material.rememberScalingLazyListState
import coil.annotation.ExperimentalCoilApi
import coil.compose.rememberImagePainter
@ -55,9 +61,7 @@ fun PersonListScreen(
peopleInSpaceViewModel: PeopleInSpaceViewModel = getViewModel()
) {
val peopleState by peopleInSpaceViewModel.peopleInSpace.collectAsState()
val scrollState = rememberScalingLazyListState()
RotaryEventState(scrollState)
PersonListScreen(peopleState, personSelected, issMapClick, scrollState)
PersonListScreen(peopleState, personSelected, issMapClick)
}
@OptIn(ExperimentalAnimationApi::class)
@ -73,24 +77,43 @@ fun PersonListScreen(
visible = people != null,
enter = slideInVertically()
) {
if (people != null) {
if (people.isNotEmpty()) {
PersonList(people, personSelected, issMapClick, scrollState)
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Card(
onClick = { },
modifier = Modifier.testTag(NoPeopleTag)
) {
Text("No people in space!")
}
Scaffold(
vignette = {
if (!people.isNullOrEmpty()) {
Vignette(VignettePosition.Bottom)
}
},
positionIndicator = {
if (!people.isNullOrEmpty()) {
PositionIndicator(scrollState)
}
}
) {
if (people.isNullOrEmpty()) {
EmptyPersonList()
} else {
PersonList(people, personSelected, issMapClick, scrollState)
}
}
}
}
}
@Composable
fun EmptyPersonList() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Card(
onClick = {},
modifier = Modifier.testTag(NoPeopleTag)
) {
Text("No people in space!")
}
}
}
@Composable
fun PersonList(
people: List<Assignment>,
@ -98,15 +121,26 @@ fun PersonList(
issMapClick: () -> Unit,
scrollState: ScalingLazyListState = rememberScalingLazyListState(),
) {
val configuration = LocalConfiguration.current
// extra content padding to prevent bottom item from crop
val extraBottomPadding = remember {
if (configuration.isScreenRound) 40.dp else 0.dp
}
ScalingLazyColumn(
contentPadding = PaddingValues(8.dp),
modifier = Modifier.testTag(PersonListTag),
modifier = Modifier
.testTag(PersonListTag)
.rotaryEventHandler(scrollState)
.padding(horizontal = 4.dp),
contentPadding = PaddingValues(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 8.dp + extraBottomPadding),
state = scrollState,
) {
item {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Button(
modifier = Modifier.size(ButtonDefaults.SmallButtonSize).wrapContentSize(),
modifier = Modifier
.size(ButtonDefaults.SmallButtonSize)
.wrapContentSize(),
onClick = issMapClick
) {
// https://www.svgrepo.com/svg/170716/international-space-station

View file

@ -1,75 +0,0 @@
package com.surrus.peopleinspace
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.InputDeviceCompat
import androidx.core.view.MotionEventCompat
import androidx.core.view.ViewConfigurationCompat
import kotlinx.coroutines.launch
val LocalRotaryEventDispatcher = staticCompositionLocalOf<RotaryEventDispatcher> {
noLocalProvidedFor("LocalRotaryEventDispatcher")
}
/**
* Dispatcher to link rotary event to [ScrollableState].
* The instance should be set up by calling [RotaryEventHandlerSetup] function.
*/
class RotaryEventDispatcher(
var scrollState: ScrollableState? = null
) {
suspend fun onRotate(delta: Float): Float? =
scrollState?.scrollBy(delta)
}
/**
* Custom rotary event setup (Currently, Column / LazyColumn doesn't handle rotary event.)
* Refer to https://developer.android.com/training/wearables/user-input/rotary-input
*/
@Composable
fun RotaryEventHandlerSetup(rotaryEventDispatcher: RotaryEventDispatcher) {
val view = LocalView.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
view.requestFocus()
view.setOnGenericMotionListener { _, event ->
if (event?.action != MotionEvent.ACTION_SCROLL ||
!event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)
) {
return@setOnGenericMotionListener false
}
val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) *
ViewConfigurationCompat.getScaledVerticalScrollFactor(
ViewConfiguration.get(context), context
)
scope.launch {
rotaryEventDispatcher.onRotate(delta)
}
true
}
}
/**
* Register a [ScrollableState] to [LocalRotaryEventDispatcher]
*/
@Composable
fun RotaryEventState(scrollState: ScrollableState?) {
val dispatcher = LocalRotaryEventDispatcher.current
SideEffect {
dispatcher.scrollState = scrollState
}
}
private fun noLocalProvidedFor(name: String): Nothing {
error("CompositionLocal $name not present")
}

View file

@ -0,0 +1,47 @@
package com.surrus.peopleinspace
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.input.pointer.RequestDisallowInterceptTouchEvent
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.InputDeviceCompat
import androidx.core.view.MotionEventCompat
import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.rotaryEventHandler(scrollState: ScrollableState): Modifier = composed {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val scaledVerticalScrollFactor =
remember { ViewConfiguration.get(context).scaledVerticalScrollFactor }
val view = LocalView.current
SideEffect {
// Activate rotary scrolling
view.requestFocus()
}
pointerInteropFilter(RequestDisallowInterceptTouchEvent()) { event ->
if (event.action != MotionEvent.ACTION_SCROLL ||
!event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)
) {
false
} else {
val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) *
scaledVerticalScrollFactor
scope.launch {
scrollState.scrollBy(delta)
}
true
}
}
}

View file

@ -8,16 +8,16 @@ import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier
import androidx.glance.layout.Column
import androidx.glance.layout.Text
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.tile.util.BaseGlanceTileService
import kotlinx.coroutines.flow.first
import org.koin.android.ext.android.inject
import org.koin.core.component.inject
class PeopleInSpaceTile : BaseGlanceTileService<PeopleInSpaceTile.Data>() {
val repository: PeopleInSpaceRepositoryInterface by inject()

View file

@ -1,11 +1,16 @@
package com.surrus.peopleinspace.tile.util
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.glance.wear.GlanceTileService
import androidx.glance.wear.tiles.GlanceTileService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
abstract class BaseGlanceTileService<T> : GlanceTileService(), KoinComponent {
val context: Context by inject()
abstract class BaseGlanceTileService<T> : GlanceTileService() {
@Composable
override fun Content() {
// Terrible hack for lack of suspend load function