diff --git a/core/src/commonIntegrationTest/kotlin/com/powersync/SyncIntegrationTest.kt b/core/src/commonIntegrationTest/kotlin/com/powersync/SyncIntegrationTest.kt index 02ebe334..def2843e 100644 --- a/core/src/commonIntegrationTest/kotlin/com/powersync/SyncIntegrationTest.kt +++ b/core/src/commonIntegrationTest/kotlin/com/powersync/SyncIntegrationTest.kt @@ -177,7 +177,7 @@ class SyncIntegrationTest { ), ) - turbine.waitFor { it.priorityStatusFor(priority).hasSynced == true } + turbine.waitFor { it.statusForPriority(priority).hasSynced == true } expectUserCount(priorityNo + 1) } @@ -224,7 +224,7 @@ class SyncIntegrationTest { // Connect to the same database again database = openDb() assertFalse { database.currentStatus.hasSynced == true } - assertTrue { database.currentStatus.priorityStatusFor(BucketPriority(1)).hasSynced == true } + assertTrue { database.currentStatus.statusForPriority(BucketPriority(1)).hasSynced == true } database.close() syncLines.close() } diff --git a/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt b/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt index 492e6b17..2a3abfdb 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt @@ -342,7 +342,7 @@ internal class PowerSyncDatabaseImpl( if (priority == null) { { it.hasSynced == true } } else { - { it.priorityStatusFor(priority).hasSynced == true } + { it.statusForPriority(priority).hasSynced == true } } if (predicate(currentStatus)) { diff --git a/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt b/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt index 2e4b84e6..8f7c5264 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt @@ -85,7 +85,7 @@ public interface SyncStatusData { /** * Status information for whether buckets in [priority] have been synchronized. */ - public fun priorityStatusFor(priority: BucketPriority): PriorityStatusEntry { + public fun statusForPriority(priority: BucketPriority): PriorityStatusEntry { val byDescendingPriorities = priorityStatusEntries.sortedByDescending { it.priority } for (entry in byDescendingPriorities) { diff --git a/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt b/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt index 3d9ebfaf..30588d47 100644 --- a/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt +++ b/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt @@ -286,7 +286,7 @@ class SyncStreamTest { ), ) - turbine.waitFor { it.priorityStatusFor(priority).hasSynced == true } + turbine.waitFor { it.statusForPriority(priority).hasSynced == true } verifySuspend(order) { if (priorityNo == 0) { diff --git a/demos/android-supabase-todolist/app/src/main/java/com/powersync/androidexample/App.kt b/demos/android-supabase-todolist/app/src/main/java/com/powersync/androidexample/App.kt index d0b7588b..637062c6 100644 --- a/demos/android-supabase-todolist/app/src/main/java/com/powersync/androidexample/App.kt +++ b/demos/android-supabase-todolist/app/src/main/java/com/powersync/androidexample/App.kt @@ -39,7 +39,7 @@ fun App() { } val db = remember { PowerSyncDatabase(driverFactory, schema) } val syncStatus = db.currentStatus - val status = syncStatus.asFlow().collectAsState(initial = null) + val status by syncStatus.asFlow().collectAsState(syncStatus) val navController = remember { NavController(Screen.Home) } val authViewModel = remember { @@ -87,7 +87,7 @@ fun App() { HomeScreen( modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), items = items, - isConnected = status.value?.connected, + status = status, onSignOutSelected = { handleSignOut() }, inputText = listsInputText, onItemClicked = handleOnItemClicked, @@ -106,7 +106,7 @@ fun App() { modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), navController = navController, items = todoItems, - isConnected = status.value?.connected, + isConnected = status.connected, inputText = todosInputText, onItemClicked = todos.value::onItemClicked, onItemDoneChanged = todos.value::onItemDoneChanged, diff --git a/demos/android-supabase-todolist/app/src/main/java/com/powersync/androidexample/screens/HomeScreen.kt b/demos/android-supabase-todolist/app/src/main/java/com/powersync/androidexample/screens/HomeScreen.kt index 86238d8e..2b95c3e3 100644 --- a/demos/android-supabase-todolist/app/src/main/java/com/powersync/androidexample/screens/HomeScreen.kt +++ b/demos/android-supabase-todolist/app/src/main/java/com/powersync/androidexample/screens/HomeScreen.kt @@ -13,12 +13,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.powersync.bucket.BucketPriority import com.powersync.demos.Screen import com.powersync.demos.components.Input import com.powersync.demos.components.ListContent import com.powersync.demos.components.Menu import com.powersync.demos.components.WifiIcon import com.powersync.demos.powersync.ListItem +import com.powersync.sync.SyncStatusData @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -26,14 +28,13 @@ internal fun HomeScreen( modifier: Modifier = Modifier, items: List, inputText: String, - isConnected: Boolean?, + status: SyncStatusData, onSignOutSelected: () -> Unit, onItemClicked: (item: ListItem) -> Unit, onItemDeleteClicked: (item: ListItem) -> Unit, onAddItemClicked: () -> Unit, onInputTextChanged: (value: String) -> Unit, ) { - Column(modifier) { TopAppBar( title = { @@ -47,7 +48,7 @@ internal fun HomeScreen( onSignOutSelected ) }, actions = { - WifiIcon(isConnected ?: false) + WifiIcon(status.connected) Spacer(modifier = Modifier.width(16.dp)) } ) @@ -60,11 +61,20 @@ internal fun HomeScreen( ) Box(Modifier.weight(1F)) { - ListContent( - items = items, - onItemClicked = onItemClicked, - onItemDeleteClicked = onItemDeleteClicked - ) + // This assumes that the bucket for lists has a priority of 1 (but it will work fine + // with sync rules not defining any priorities at all too). + // When giving lists a higher priority than items, we can have a consistent snapshot of + // lists without items. In the case where many items exist (that might take longer to + // sync initially), this allows us to display lists earlier. + if (status.statusForPriority(BucketPriority(1)).hasSynced == true) { + ListContent( + items = items, + onItemClicked = onItemClicked, + onItemDeleteClicked = onItemDeleteClicked + ) + } else { + Text("Busy with sync...") + } } } } diff --git a/demos/supabase-todolist/README.md b/demos/supabase-todolist/README.md index 9948b023..e418bfd2 100644 --- a/demos/supabase-todolist/README.md +++ b/demos/supabase-todolist/README.md @@ -3,7 +3,7 @@ It is a simple to-do list application demonstrating use of the PowerSync Kotlin Mutiplatform SDK together with [Supabase](https://supabase.com/) in a basic Kotlin Multiplatform Compose App. -Supported KMP targets: Android and iOS. +Supported KMP targets: Android, iOS and Desktop (JVM). ## Setting up your development environment diff --git a/demos/supabase-todolist/desktopApp/build.gradle.kts b/demos/supabase-todolist/desktopApp/build.gradle.kts index 8b07520c..d40b4091 100644 --- a/demos/supabase-todolist/desktopApp/build.gradle.kts +++ b/demos/supabase-todolist/desktopApp/build.gradle.kts @@ -11,7 +11,7 @@ kotlin { sourceSets { jvmMain.dependencies { implementation(compose.desktop.currentOs) - implementation(project(":shared")) + implementation(projects.shared) } } } diff --git a/demos/supabase-todolist/gradle/libs.versions.toml b/demos/supabase-todolist/gradle/libs.versions.toml index bce8228e..ee72cb8a 100644 --- a/demos/supabase-todolist/gradle/libs.versions.toml +++ b/demos/supabase-todolist/gradle/libs.versions.toml @@ -18,6 +18,7 @@ junit = "4.13.2" compose = "1.6.11" compose-preview = "1.7.2" +lifecycle = "2.8.2" # plugins android-gradle-plugin = "8.5.1" @@ -49,6 +50,7 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "kto ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } @@ -56,6 +58,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose-preview" } +compose-lifecycle = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } [plugins] androidApplication = { id = "com.android.application", version.ref = "android-gradle-plugin" } diff --git a/demos/supabase-todolist/iosApp/iosApp.xcodeproj/project.pbxproj b/demos/supabase-todolist/iosApp/iosApp.xcodeproj/project.pbxproj index 8192f188..08233b9b 100644 --- a/demos/supabase-todolist/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/demos/supabase-todolist/iosApp/iosApp.xcodeproj/project.pbxproj @@ -118,7 +118,6 @@ 7555FF79242A565900829871 /* Resources */, F85CB1118929364A9C6EFABC /* Frameworks */, 3C5ACF3A4AAFF294B2A5839B /* [CP] Embed Pods Frameworks */, - 79E17356D7D971831C2A4119 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -231,23 +230,6 @@ 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; }; - 79E17356D7D971831C2A4119 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/demos/supabase-todolist/settings.gradle.kts b/demos/supabase-todolist/settings.gradle.kts index 8ec76071..e54b14ee 100644 --- a/demos/supabase-todolist/settings.gradle.kts +++ b/demos/supabase-todolist/settings.gradle.kts @@ -43,7 +43,7 @@ rootProject.name = "supabase-todolist" include(":androidApp") include(":shared") -//include(":desktopApp") +include(":desktopApp") includeBuild("../..") { dependencySubstitution { diff --git a/demos/supabase-todolist/shared/build.gradle.kts b/demos/supabase-todolist/shared/build.gradle.kts index 26fe55b9..006f53f1 100644 --- a/demos/supabase-todolist/shared/build.gradle.kts +++ b/demos/supabase-todolist/shared/build.gradle.kts @@ -15,7 +15,7 @@ version = "1.0-SNAPSHOT" kotlin { androidTarget() -// jvm() + jvm() iosX64() iosArm64() iosSimulatorArm64() @@ -48,6 +48,7 @@ kotlin { implementation(compose.material) implementation(compose.components.resources) implementation(compose.materialIconsExtended) + implementation(libs.compose.lifecycle) } androidMain.dependencies { api(libs.androidx.activity.compose) @@ -55,9 +56,10 @@ kotlin { api(libs.androidx.core) } -// jvmMain.dependencies { -// implementation(compose.desktop.common) -// } + jvmMain.dependencies { + implementation(compose.desktop.common) + implementation(libs.kotlinx.coroutines.swing) + } } } diff --git a/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/App.kt b/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/App.kt index 2e33626d..2a19a406 100644 --- a/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/App.kt +++ b/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/App.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import com.powersync.DatabaseDriverFactory import com.powersync.PowerSyncDatabase +import com.powersync.bucket.BucketPriority import com.powersync.connector.supabase.SupabaseConnector import com.powersync.demos.components.EditDialog import com.powersync.demos.powersync.ListContent @@ -36,9 +37,15 @@ fun App(factory: DatabaseDriverFactory, modifier: Modifier = Modifier) { ) } val db = remember { PowerSyncDatabase(factory, schema) } - val status = db.currentStatus.asFlow().collectAsState(initial = null) - val hasSynced by remember { derivedStateOf { status.value?.hasSynced } } - + val status by db.currentStatus.asFlow().collectAsState(initial = db.currentStatus) + + // This assumes that the buckets for lists has a priority of 1 (but it will work fine with sync + // rules not defining any priorities at all too). When giving lists a higher priority than + // items, we can have a consistent snapshot of lists without items. In the case where many items + // exist (that might take longer to sync initially), this allows us to display lists earlier. + val hasSyncedLists by remember { + derivedStateOf { status.statusForPriority(BucketPriority(1)).hasSynced } + } val navController = remember { NavController(Screen.Home) } val authViewModel = remember { @@ -86,14 +93,14 @@ fun App(factory: DatabaseDriverFactory, modifier: Modifier = Modifier) { HomeScreen( modifier = modifier.background(MaterialTheme.colors.background), items = items, - isConnected = status.value?.connected, + isConnected = status.connected, onSignOutSelected = { handleSignOut() }, inputText = listsInputText, onItemClicked = handleOnItemClicked, onItemDeleteClicked = lists.value::onItemDeleteClicked, onAddItemClicked = lists.value::onAddItemClicked, onInputTextChanged = lists.value::onInputTextChanged, - hasSynced = hasSynced + hasSynced = hasSyncedLists ) } @@ -106,7 +113,7 @@ fun App(factory: DatabaseDriverFactory, modifier: Modifier = Modifier) { modifier = modifier.background(MaterialTheme.colors.background), navController = navController, items = todoItems, - isConnected = status.value?.connected, + isConnected = status.connected, inputText = todosInputText, onItemClicked = todos.value::onItemClicked, onItemDoneChanged = todos.value::onItemDoneChanged, diff --git a/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/screens/HomeScreen.kt b/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/screens/HomeScreen.kt index f0db1c76..c2f17f5e 100644 --- a/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/screens/HomeScreen.kt +++ b/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/screens/HomeScreen.kt @@ -28,7 +28,7 @@ internal fun HomeScreen( modifier: Modifier = Modifier, items: List, inputText: String, - isConnected: Boolean?, + isConnected: Boolean, hasSynced: Boolean?, onSignOutSelected: () -> Unit, onItemClicked: (item: ListItem) -> Unit, @@ -49,7 +49,7 @@ internal fun HomeScreen( onSignOutSelected ) }, actions = { - WifiIcon(isConnected ?: false) + WifiIcon(isConnected) Spacer(modifier = Modifier.width(16.dp)) } ) diff --git a/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/screens/TodosScreen.kt b/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/screens/TodosScreen.kt index a61ef1cb..b407f994 100644 --- a/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/screens/TodosScreen.kt +++ b/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/screens/TodosScreen.kt @@ -32,7 +32,7 @@ internal fun TodosScreen( navController: NavController, items: List, inputText: String, - isConnected: Boolean?, + isConnected: Boolean, onItemClicked: (item: TodoItem) -> Unit, onItemDoneChanged: (item: TodoItem, isDone: Boolean) -> Unit, onItemDeleteClicked: (item: TodoItem) -> Unit, @@ -53,7 +53,7 @@ internal fun TodosScreen( } }, actions = { - WifiIcon(isConnected ?: false) + WifiIcon(isConnected) Spacer(modifier = Modifier.width(16.dp)) } ) diff --git a/demos/supabase-todolist/shared/src/desktopMain/kotlin/main.desktop.kt b/demos/supabase-todolist/shared/src/desktopMain/kotlin/main.desktop.kt deleted file mode 100644 index ea4a82d8..00000000 --- a/demos/supabase-todolist/shared/src/desktopMain/kotlin/main.desktop.kt +++ /dev/null @@ -1,6 +0,0 @@ -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.powersync.demos.RootContent - -@Composable fun MainView() = RootContent(Modifier.fillMaxSize()) \ No newline at end of file diff --git a/demos/supabase-todolist/shared/src/desktopMain/kotlin/com/powersync/demos/Utils.kt b/demos/supabase-todolist/shared/src/jvmMain/kotlin/com/powersync/demos/Utils.kt similarity index 100% rename from demos/supabase-todolist/shared/src/desktopMain/kotlin/com/powersync/demos/Utils.kt rename to demos/supabase-todolist/shared/src/jvmMain/kotlin/com/powersync/demos/Utils.kt diff --git a/demos/supabase-todolist/shared/src/jvmMain/kotlin/main.desktop.kt b/demos/supabase-todolist/shared/src/jvmMain/kotlin/main.desktop.kt new file mode 100644 index 00000000..ca9171e0 --- /dev/null +++ b/demos/supabase-todolist/shared/src/jvmMain/kotlin/main.desktop.kt @@ -0,0 +1,7 @@ +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.powersync.DatabaseDriverFactory +import com.powersync.demos.App + +@Composable fun MainView() = App(DatabaseDriverFactory(), Modifier.fillMaxSize())