Compare commits
24 commits
backend_kt
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
c666ae73b5 | ||
|
a1d7cf3f95 | ||
|
be2cfc9693 | ||
|
539d27f6a7 | ||
|
3f33e636f1 | ||
|
d11367c0ec | ||
|
f0a0cb5b52 | ||
|
7924d4ea98 | ||
|
a1afb99d80 | ||
|
d97c45b7aa | ||
|
81278bfa6b | ||
|
eeab97d9ea | ||
|
e3964652df | ||
|
d2acc790f8 | ||
|
e03495719c | ||
|
6017e2863a | ||
|
f6fb2385a6 | ||
|
8db96078d2 | ||
|
93aac4c617 | ||
|
4ab4ea1830 | ||
|
ee6408a5b0 | ||
|
d35f8e64fa | ||
|
6f622097e6 | ||
|
b2129ebff9 |
40 changed files with 414 additions and 222 deletions
2
.github/workflows/android.yml
vendored
2
.github/workflows/android.yml
vendored
|
@ -59,4 +59,4 @@ jobs:
|
|||
with:
|
||||
api-level: 29
|
||||
target: google_apis
|
||||
script: ./gradlew app:connectedAndroidTest common:connectedAndroidTest
|
||||
script: ./gradlew app:connectedAndroidTest
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
1
graphql-server/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
build
|
41
graphql-server/build.gradle.kts
Normal file
41
graphql-server/build.gradle.kts
Normal 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"
|
||||
}
|
||||
}
|
3
graphql-server/src/main/appengine/app.yaml
Normal file
3
graphql-server/src/main/appengine/app.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
runtime: java11
|
||||
|
||||
entrypoint: java -Xmx64m -jar graphql-server.jar
|
|
@ -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>()
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.surrus.peopleinspace
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runServer()
|
||||
}
|
||||
|
2
graphql-server/src/main/resources/application.yml
Normal file
2
graphql-server/src/main/resources/application.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
graphql:
|
||||
packages: "com.surrus"
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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 |
|
||||
|
|
22
ios/PeopleInSpaceSwiftUI/Pods/Manifest.lock
generated
22
ios/PeopleInSpaceSwiftUI/Pods/Manifest.lock
generated
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -85,7 +85,9 @@ dependencies {
|
|||
debugImplementation(composeUiTestManifest)
|
||||
}
|
||||
|
||||
implementation("androidx.glance:glance-wear:1.0.0-SNAPSHOT")
|
||||
with(Deps.Glance) {
|
||||
implementation(tiles)
|
||||
}
|
||||
|
||||
implementation(project(":common"))
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue