From 449afdb58ee7d3e6e95b59ac778e1f9a79345ca7 Mon Sep 17 00:00:00 2001 From: Aaron LaBeau <80424345+biozal@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:45:47 -0600 Subject: [PATCH 1/3] feat: update to v5 of the API --- .../QuickStartTasks/app/build.gradle.kts | 12 +-- .../ditto/quickstart/tasks/DittoHandler.kt | 13 ++- .../ditto/quickstart/tasks/MainActivity.kt | 2 +- .../quickstart/tasks/TasksApplication.kt | 97 +++++++++++-------- .../tasks/edit/EditScreenViewModel.kt | 54 +++++++---- .../tasks/list/TasksListScreenViewModel.kt | 68 +++++++++---- .../QuickStartTasks/gradle/libs.versions.toml | 4 +- 7 files changed, 162 insertions(+), 88 deletions(-) diff --git a/android-kotlin/QuickStartTasks/app/build.gradle.kts b/android-kotlin/QuickStartTasks/app/build.gradle.kts index c10dea861..9c31fc687 100644 --- a/android-kotlin/QuickStartTasks/app/build.gradle.kts +++ b/android-kotlin/QuickStartTasks/app/build.gradle.kts @@ -45,7 +45,7 @@ androidComponents { buildConfigFields.forEach { (key, description) -> it.buildConfigFields.put( key, - BuildConfigField("String", "\"${prop[key]}\"", description) + BuildConfigField("String", "${prop[key]}", description) ) } } @@ -61,7 +61,7 @@ android { defaultConfig { applicationId = "live.ditto.quickstart.tasks" - minSdk = 23 + minSdk = 24 targetSdk = 35 versionCode = 1 versionName = "1.0" @@ -83,12 +83,12 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } buildFeatures { @@ -132,7 +132,7 @@ dependencies { implementation(libs.koin.androidx.compose.navigation) // Ditto SDK - implementation(libs.live.ditto) + implementation(libs.com.ditto) // Testing testImplementation(libs.junit) diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoHandler.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoHandler.kt index 40ff8e8b2..8b660b75a 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoHandler.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/DittoHandler.kt @@ -1,9 +1,20 @@ package live.ditto.quickstart.tasks -import live.ditto.* +import com.ditto.kotlin.* class DittoHandler { companion object { lateinit var ditto: Ditto + private set + + fun initialize(config: DittoConfig) { + if (::ditto.isInitialized) { + throw IllegalStateException("Ditto is already initialized") + } + ditto = DittoFactory.create(config = config) + } + + val isInitialized: Boolean + get() = ::ditto.isInitialized } } diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/MainActivity.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/MainActivity.kt index 0ac8b933c..08339e595 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/MainActivity.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/MainActivity.kt @@ -3,7 +3,7 @@ package live.ditto.quickstart.tasks import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import live.ditto.transports.DittoSyncPermissions +import com.ditto.kotlin.transports.DittoSyncPermissions import android.os.StrictMode class MainActivity : ComponentActivity() { diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt index a05cae2f5..f95cf93bd 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt @@ -6,20 +6,22 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import live.ditto.Ditto -import live.ditto.DittoIdentity -import live.ditto.android.DefaultAndroidDittoDependencies +import com.ditto.kotlin.Ditto +import com.ditto.kotlin.DittoAuthenticationProvider +import com.ditto.kotlin.DittoConfig +import com.ditto.kotlin.DittoConnection +import com.ditto.kotlin.DittoFactory +import com.ditto.kotlin.DittoLog +import com.ditto.kotlin.error.DittoException import live.ditto.quickstart.tasks.DittoHandler.Companion.ditto class TasksApplication : Application() { // Create a CoroutineScope // Use SupervisorJob so if one coroutine launched in this scope fails, it doesn't cancel the scope - // - // https://developer.android.com/kotlin/coroutines/coroutines-adv - // Dispatchers.IO - This dispatcher is optimized to perform disk or network I/O outside of the main thread. - private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - + // SDKS-1294: Don't create Ditto in a scope using Dispatchers.IO + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val tag = "TaskApplication" companion object { private var instance: TasksApplication? = null @@ -35,43 +37,58 @@ class TasksApplication : Application() { override fun onCreate() { super.onCreate() - ioScope.launch { - setupDitto() + + // Initialize Ditto synchronously - completes before UI loads + initializeDitto() + + // Perform authentication asynchronously - can happen in background + scope.launch { + performAuthentication() } } - private suspend fun setupDitto() { - val androidDependencies = DefaultAndroidDittoDependencies(applicationContext) - - //read values from build.gradle.kts (Module:app) which reads from environment file - val appId = BuildConfig.DITTO_APP_ID - val token = BuildConfig.DITTO_PLAYGROUND_TOKEN - val authUrl = BuildConfig.DITTO_AUTH_URL - val webSocketURL = BuildConfig.DITTO_WEBSOCKET_URL - - val enableDittoCloudSync = false - - /* - * Setup Ditto Identity - * https://docs.ditto.live/sdk/latest/install-guides/kotlin#integrating-and-initializing - */ - val identity = DittoIdentity.OnlinePlayground( - dependencies = androidDependencies, - appId = appId, - token = token, - customAuthUrl = authUrl, - enableDittoCloudSync = enableDittoCloudSync // This is required to be set to false to use the correct URLs - ) - - ditto = Ditto(androidDependencies, identity) - ditto.updateTransportConfig { config -> - // Set the Ditto Websocket URL - config.connect.websocketUrls.add(webSocketURL) + private fun initializeDitto() { + try { + val appId = BuildConfig.DITTO_APP_ID + val authUrl = BuildConfig.DITTO_AUTH_URL + + val config = DittoConfig( + databaseId = appId, + connect = DittoConfig.Connect.Server(url = authUrl) + ) + + DittoHandler.initialize(config) + DittoLog.d(tag, "Ditto instance created successfully") + + } catch (ex: Throwable) { + DittoLog.e(tag, "Failed to initialize Ditto: $ex") + ex.printStackTrace() + throw ex } + } + + private suspend fun performAuthentication() { + try { + val token = BuildConfig.DITTO_PLAYGROUND_TOKEN - ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false") + DittoHandler.ditto.auth?.setExpirationHandler { ditto, _ -> + try { + val clientInfo = ditto.auth?.login( + token = token, + provider = DittoAuthenticationProvider.development() + ) + DittoLog.d(tag, "Auth response: $clientInfo") + } catch (ex: Throwable) { + DittoLog.e(tag, "Authentication failed: $ex") + ex.printStackTrace() + } + } - // disable sync with v3 peers, required for DQL - ditto.disableSyncWithV3() + DittoLog.d(tag, "Ditto authentication setup complete") + + } catch (ex: Throwable) { + DittoLog.e(tag, "Failed to setup authentication: $ex") + ex.printStackTrace() + } } } diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt index 4c3dfb96d..e2327c746 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt @@ -5,10 +5,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import live.ditto.DittoError +import com.ditto.kotlin.error.DittoException +import com.ditto.kotlin.serialization.toDittoCbor import live.ditto.quickstart.tasks.DittoHandler.Companion.ditto import live.ditto.quickstart.tasks.data.Task +import com.ditto.kotlin.serialization.DittoCborSerializable +import com.ditto.kotlin.serialization.DittoCborSerializable.Utf8String + class EditScreenViewModel : ViewModel() { companion object { @@ -22,6 +26,10 @@ class EditScreenViewModel : ViewModel() { var canDelete = MutableLiveData(false) fun setupWithTask(id: String?) { + check(live.ditto.quickstart.tasks.DittoHandler.isInitialized) { + "Ditto must be initialized before ViewModels are created" + } + canDelete.postValue(id != null) val taskId: String = id ?: return @@ -29,14 +37,14 @@ class EditScreenViewModel : ViewModel() { try { val item = ditto.store.execute( "SELECT * FROM tasks WHERE _id = :_id AND NOT deleted", - mapOf("_id" to taskId) + mapOf("_id" to taskId).toDittoCbor() ).items.first() val task = Task.fromJson(item.jsonString()) _id = task._id title.postValue(task.title) done.postValue(task.done) - } catch (e: DittoError) { + } catch (e: DittoException) { Log.e(TAG, "Unable to setup view task data", e) } } @@ -45,23 +53,35 @@ class EditScreenViewModel : ViewModel() { fun save() { viewModelScope.launch { try { + val titleValue = title.value ?: "" + val doneValue = done.value ?: false if (_id == null) { // Add tasks into the ditto collection using DQL INSERT statement // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents + val addMap = DittoCborSerializable.Dictionary( + mapOf( + Utf8String("title") to Utf8String(titleValue), + Utf8String("done") to DittoCborSerializable.BooleanValue(doneValue), + Utf8String("deleted") to DittoCborSerializable.BooleanValue(false) + ) + ) ditto.store.execute( "INSERT INTO tasks DOCUMENTS (:doc)", - mapOf( - "doc" to mapOf( - "title" to title.value, - "done" to done.value, - "deleted" to false - ) + DittoCborSerializable.Dictionary( + mapOf(Utf8String("doc") to addMap) ) ) } else { // Update tasks into the ditto collection using DQL UPDATE statement // https://docs.ditto.live/sdk/latest/crud/update#updating _id?.let { id -> + val editMap = DittoCborSerializable.Dictionary( + mapOf( + Utf8String("title") to Utf8String(titleValue), + Utf8String("done") to DittoCborSerializable.BooleanValue(doneValue), + Utf8String("id") to DittoCborSerializable.Utf8String(id) + ) + ) ditto.store.execute( """ UPDATE tasks @@ -71,15 +91,11 @@ class EditScreenViewModel : ViewModel() { WHERE _id = :id AND NOT deleted """, - mapOf( - "title" to title.value, - "done" to done.value, - "id" to id - ) + arguments = editMap ) } } - } catch (e: DittoError) { + } catch (e: Throwable) { Log.e(TAG, "Unable to save task", e) } } @@ -93,10 +109,14 @@ class EditScreenViewModel : ViewModel() { _id?.let { id -> ditto.store.execute( "UPDATE tasks SET deleted = true WHERE _id = :id", - mapOf("id" to id) + DittoCborSerializable.Dictionary( + mapOf( + Utf8String("id") to Utf8String(id) + ) + ) ) } - } catch (e: DittoError) { + } catch (e: Throwable) { Log.e(TAG, "Unable to set deleted=true", e) } } diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt index c716696bf..819fe8ee5 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt @@ -9,15 +9,18 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ditto.kotlin.DittoSyncSubscription import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import live.ditto.DittoError -import live.ditto.DittoSyncSubscription import live.ditto.quickstart.tasks.DittoHandler.Companion.ditto import live.ditto.quickstart.tasks.TasksApplication import live.ditto.quickstart.tasks.data.Task +import com.ditto.kotlin.serialization.DittoCborSerializable +import com.ditto.kotlin.serialization.DittoCborSerializable.Utf8String +import okio.Utf8 + // The value of the Sync switch is stored in persistent settings private val Context.preferencesDataStore by preferencesDataStore("tasks_list_settings") private val SYNC_ENABLED_KEY = booleanPreferencesKey("sync_enabled") @@ -27,7 +30,7 @@ class TasksListScreenViewModel : ViewModel() { companion object { private const val TAG = "TasksListScreenViewModel" - private const val QUERY = "SELECT * FROM tasks WHERE NOT deleted ORDER BY title ASC" + private const val QUERY = "SELECT * FROM tasks WHERE NOT deleted" } private val preferencesDataStore = TasksApplication.applicationContext().preferencesDataStore @@ -55,7 +58,7 @@ class TasksListScreenViewModel : ViewModel() { // Register a subscription, which determines what data syncs to this peer // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions syncSubscription = ditto.sync.registerSubscription(QUERY) - } catch (e: DittoError) { + } catch (e: Throwable) { Log.e(TAG, "Unable to start sync", e) } } else if (ditto.isSyncActive) { @@ -63,7 +66,7 @@ class TasksListScreenViewModel : ViewModel() { syncSubscription?.close() syncSubscription = null ditto.stopSync() - } catch (e: DittoError) { + } catch (e: Throwable) { Log.e(TAG, "Unable to stop sync", e) } } @@ -71,13 +74,20 @@ class TasksListScreenViewModel : ViewModel() { } init { + // Defensive check - should never fail with synchronous initialization + check(live.ditto.quickstart.tasks.DittoHandler.isInitialized) { + "Ditto must be initialized before ViewModels are created" + } + viewModelScope.launch { populateTasksCollection() // Register observer, which runs against the local database on this peer // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers ditto.store.registerObserver(QUERY) { result -> - val list = result.items.map { item -> Task.fromJson(item.jsonString()) } + val list = result.items.map { + item -> Task.fromJson(item.jsonString()) + } tasks.postValue(list) } @@ -101,18 +111,24 @@ class TasksListScreenViewModel : ViewModel() { try { // Add tasks into the ditto collection using DQL INSERT statement // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents + val addMap = DittoCborSerializable.Dictionary( + mapOf( + Utf8String("_id") to Utf8String(task._id), + Utf8String("title") to Utf8String(task.title), + Utf8String("done") to DittoCborSerializable.BooleanValue(task.done), + Utf8String("deleted") to DittoCborSerializable.BooleanValue(task.deleted) + + ) + ) ditto.store.execute( "INSERT INTO tasks INITIAL DOCUMENTS (:task)", - mapOf( - "task" to mapOf( - "_id" to task._id, - "title" to task.title, - "done" to task.done, - "deleted" to task.deleted, + DittoCborSerializable.Dictionary( + mapOf( + Utf8String("task") to addMap ) ) ) - } catch (e: DittoError) { + } catch (e: Throwable) { Log.e(TAG, "Unable to insert initial document", e) } } @@ -124,21 +140,27 @@ class TasksListScreenViewModel : ViewModel() { try { val doc = ditto.store.execute( "SELECT * FROM tasks WHERE _id = :_id AND NOT deleted", - mapOf("_id" to taskId) + DittoCborSerializable.Dictionary( + mapOf( + Utf8String("_id") to Utf8String(taskId) + ) + ) ).items.first() - val done = doc.value["done"] as Boolean + val done = doc.value["done"].boolean // Update tasks into the ditto collection using DQL UPDATE statement // https://docs.ditto.live/sdk/latest/crud/update#updating ditto.store.execute( "UPDATE tasks SET done = :toggled WHERE _id = :_id AND NOT deleted", - mapOf( - "toggled" to !done, - "_id" to taskId + DittoCborSerializable.Dictionary( + mapOf( + Utf8String("toggled") to DittoCborSerializable.BooleanValue(!done), + Utf8String("_id") to Utf8String(taskId) + ) ) ) - } catch (e: DittoError) { + } catch (e: Throwable) { Log.e(TAG, "Unable to toggle done state", e) } } @@ -151,9 +173,13 @@ class TasksListScreenViewModel : ViewModel() { // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern ditto.store.execute( "UPDATE tasks SET deleted = true WHERE _id = :id", - mapOf("id" to taskId) + DittoCborSerializable.Dictionary( + mapOf( + Utf8String("id") to Utf8String(taskId) + ) + ) ) - } catch (e: DittoError) { + } catch (e: Throwable) { Log.e(TAG, "Unable to set deleted=true", e) } } diff --git a/android-kotlin/QuickStartTasks/gradle/libs.versions.toml b/android-kotlin/QuickStartTasks/gradle/libs.versions.toml index 608b52f08..9be277c03 100644 --- a/android-kotlin/QuickStartTasks/gradle/libs.versions.toml +++ b/android-kotlin/QuickStartTasks/gradle/libs.versions.toml @@ -14,7 +14,7 @@ appcompat = "1.7.1" datastorePreferences = "1.1.7" koin-bom = "4.1.0" coroutines-tests = "1.10.2" -ditto = "4.13.1" +ditto = "5.0.0-dev-weekly.20260126.180" monitor = "1.7.2" [libraries] @@ -41,7 +41,7 @@ koin-core = { group = "io.insert-koin", name = "koin-core" } koin-android = { group = "io.insert-koin", name = "koin-android" } koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose" } koin-androidx-compose-navigation = { group = "io.insert-koin", name = "koin-androidx-compose-navigation" } -live-ditto = { group = "live.ditto", name = "ditto", version.ref = "ditto" } +com-ditto = { group = "com.ditto", name = "ditto-kotlin", version.ref = "ditto" } kotlinx-coroutines = {group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines-tests" } androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "monitor" } From 922c5cd4ca3668a795f14d586d110c040783bcbf Mon Sep 17 00:00:00 2001 From: Aaron LaBeau <80424345+biozal@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:00:26 -0500 Subject: [PATCH 2/3] Updated for RC3 support and Android best practices --- .../QuickStartTasks/app/build.gradle.kts | 8 +- .../ditto/quickstart/tasks/MainActivity.kt | 18 +-- .../quickstart/tasks/TasksApplication.kt | 57 +++----- .../live/ditto/quickstart/tasks/data/Task.kt | 7 + .../ditto/quickstart/tasks/edit/EditScreen.kt | 12 +- .../tasks/edit/EditScreenViewModel.kt | 89 ++++++------ .../quickstart/tasks/list/TasksListScreen.kt | 6 +- .../tasks/list/TasksListScreenViewModel.kt | 129 +++++++----------- .../ditto/quickstart/tasks/ui/theme/Theme.kt | 20 --- .../QuickStartTasks/gradle/libs.versions.toml | 18 +-- .../xcshareddata/swiftpm/Package.resolved | 4 +- 11 files changed, 144 insertions(+), 224 deletions(-) diff --git a/android-kotlin/QuickStartTasks/app/build.gradle.kts b/android-kotlin/QuickStartTasks/app/build.gradle.kts index 9c31fc687..a4bbf9c15 100644 --- a/android-kotlin/QuickStartTasks/app/build.gradle.kts +++ b/android-kotlin/QuickStartTasks/app/build.gradle.kts @@ -53,7 +53,7 @@ androidComponents { android { namespace = "live.ditto.quickstart.tasks" - compileSdk = 35 + compileSdk = 36 lint { baseline = file("lint-baseline.xml") @@ -96,10 +96,6 @@ android { compose = true } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.14" - } - packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" @@ -111,6 +107,7 @@ dependencies { // Core Android implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) implementation(libs.androidx.datastore.preferences) @@ -122,7 +119,6 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.navigation.compose) - implementation(libs.androidx.runtime.livedata) // Dependency Injection implementation(platform(libs.koin.bom)) diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/MainActivity.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/MainActivity.kt index 08339e595..500685545 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/MainActivity.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/MainActivity.kt @@ -3,23 +3,14 @@ package live.ditto.quickstart.tasks import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import com.ditto.kotlin.transports.DittoSyncPermissions -import android.os.StrictMode class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) - if (BuildConfig.DEBUG) { - StrictMode.setThreadPolicy( - StrictMode.ThreadPolicy.Builder() - .detectDiskReads() - .detectDiskWrites() - .penaltyLog() // Log violations to logcat - .build() - ) - } - setContent { Root() } @@ -28,7 +19,7 @@ class MainActivity : ComponentActivity() { } private fun requestMissingPermissions() { - // requesting permissions at runtime + // Requesting permissions at runtime // https://docs.ditto.live/sdk/latest/install-guides/kotlin#requesting-permissions-at-runtime val missingPermissions = DittoSyncPermissions(this).missingPermissions() if (missingPermissions.isNotEmpty()) { @@ -36,6 +27,3 @@ class MainActivity : ComponentActivity() { } } } - - - diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt index f95cf93bd..4745805cb 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/TasksApplication.kt @@ -2,25 +2,13 @@ package live.ditto.quickstart.tasks import android.app.Application import android.content.Context -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import com.ditto.kotlin.Ditto import com.ditto.kotlin.DittoAuthenticationProvider import com.ditto.kotlin.DittoConfig -import com.ditto.kotlin.DittoConnection import com.ditto.kotlin.DittoFactory import com.ditto.kotlin.DittoLog -import com.ditto.kotlin.error.DittoException -import live.ditto.quickstart.tasks.DittoHandler.Companion.ditto class TasksApplication : Application() { - // Create a CoroutineScope - // Use SupervisorJob so if one coroutine launched in this scope fails, it doesn't cancel the scope - // SDKS-1294: Don't create Ditto in a scope using Dispatchers.IO - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val tag = "TaskApplication" companion object { @@ -37,58 +25,47 @@ class TasksApplication : Application() { override fun onCreate() { super.onCreate() - - // Initialize Ditto synchronously - completes before UI loads initializeDitto() - - // Perform authentication asynchronously - can happen in background - scope.launch { - performAuthentication() - } + setupAuthentication() } private fun initializeDitto() { try { - val appId = BuildConfig.DITTO_APP_ID - val authUrl = BuildConfig.DITTO_AUTH_URL - val config = DittoConfig( - databaseId = appId, - connect = DittoConfig.Connect.Server(url = authUrl) + databaseId = BuildConfig.DITTO_APP_ID, + connect = DittoConfig.Connect.Server(url = BuildConfig.DITTO_AUTH_URL) ) DittoHandler.initialize(config) DittoLog.d(tag, "Ditto instance created successfully") - } catch (ex: Throwable) { DittoLog.e(tag, "Failed to initialize Ditto: $ex") - ex.printStackTrace() throw ex } } - private suspend fun performAuthentication() { + private fun setupAuthentication() { try { val token = BuildConfig.DITTO_PLAYGROUND_TOKEN - DittoHandler.ditto.auth?.setExpirationHandler { ditto, _ -> - try { - val clientInfo = ditto.auth?.login( - token = token, - provider = DittoAuthenticationProvider.development() - ) - DittoLog.d(tag, "Auth response: $clientInfo") - } catch (ex: Throwable) { - DittoLog.e(tag, "Authentication failed: $ex") - ex.printStackTrace() + // Set the expiration handler before starting sync + // https://docs.ditto.live/sdk/latest/sync/authentication + DittoHandler.ditto.auth?.let { auth -> + auth.expirationHandler = { ditto, _ -> + try { + val clientInfo = ditto.auth?.login( + token = token, + provider = DittoAuthenticationProvider.development() + ) + DittoLog.d(tag, "Auth response: $clientInfo") + } catch (ex: Throwable) { + DittoLog.e(tag, "Authentication failed: $ex") + } } } - DittoLog.d(tag, "Ditto authentication setup complete") - } catch (ex: Throwable) { DittoLog.e(tag, "Failed to setup authentication: $ex") - ex.printStackTrace() } } } diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/data/Task.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/data/Task.kt index db4b8bf32..43b5ccc30 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/data/Task.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/data/Task.kt @@ -11,6 +11,13 @@ data class Task( val done: Boolean = false, val deleted: Boolean = false, ) { + fun toMap(): Map = mapOf( + "_id" to _id, + "title" to title, + "done" to done, + "deleted" to deleted + ) + companion object { private const val TAG = "Task" diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreen.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreen.kt index 01d1b3f32..f3c122d89 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreen.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreen.kt @@ -10,10 +10,10 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import live.ditto.quickstart.tasks.R @@ -26,9 +26,9 @@ fun EditScreen(navController: NavController, taskId: String?) { val topBarTitle = if (taskId == null) "New Task" else "Edit Task" - val title: String by editScreenViewModel.title.observeAsState("") - val done: Boolean by editScreenViewModel.done.observeAsState(initial = false) - val canDelete: Boolean by editScreenViewModel.canDelete.observeAsState(initial = false) + val title: String by editScreenViewModel.title.collectAsStateWithLifecycle() + val done: Boolean by editScreenViewModel.done.collectAsStateWithLifecycle() + val canDelete: Boolean by editScreenViewModel.canDelete.collectAsStateWithLifecycle() Scaffold( topBar = { @@ -46,9 +46,9 @@ fun EditScreen(navController: NavController, taskId: String?) { EditForm( canDelete = canDelete, title = title, - onTitleTextChange = { editScreenViewModel.title.value = it }, + onTitleTextChange = { editScreenViewModel.setTitle(it) }, done = done, - onDoneChanged = { editScreenViewModel.done.value = it }, + onDoneChanged = { editScreenViewModel.setDone(it) }, onSaveButtonClicked = { editScreenViewModel.save() navController.popBackStack() diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt index e2327c746..5d61e71b6 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/edit/EditScreenViewModel.kt @@ -1,18 +1,15 @@ package live.ditto.quickstart.tasks.edit import android.util.Log -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import com.ditto.kotlin.error.DittoException -import com.ditto.kotlin.serialization.toDittoCbor import live.ditto.quickstart.tasks.DittoHandler.Companion.ditto import live.ditto.quickstart.tasks.data.Task -import com.ditto.kotlin.serialization.DittoCborSerializable -import com.ditto.kotlin.serialization.DittoCborSerializable.Utf8String - class EditScreenViewModel : ViewModel() { companion object { @@ -21,30 +18,46 @@ class EditScreenViewModel : ViewModel() { private var _id: String? = null - var title = MutableLiveData("") - var done = MutableLiveData(false) - var canDelete = MutableLiveData(false) + private val _title = MutableStateFlow("") + val title: StateFlow = _title.asStateFlow() + + private val _done = MutableStateFlow(false) + val done: StateFlow = _done.asStateFlow() + + private val _canDelete = MutableStateFlow(false) + val canDelete: StateFlow = _canDelete.asStateFlow() + + fun setTitle(value: String) { + _title.value = value + } + + fun setDone(value: Boolean) { + _done.value = value + } fun setupWithTask(id: String?) { check(live.ditto.quickstart.tasks.DittoHandler.isInitialized) { "Ditto must be initialized before ViewModels are created" } - canDelete.postValue(id != null) + _canDelete.value = id != null val taskId: String = id ?: return viewModelScope.launch { try { - val item = ditto.store.execute( + val task = ditto.store.execute( "SELECT * FROM tasks WHERE _id = :_id AND NOT deleted", - mapOf("_id" to taskId).toDittoCbor() - ).items.first() + mapOf("_id" to taskId) + ) { result -> + result.items.firstOrNull()?.let { Task.fromJson(it.jsonString()) } + } - val task = Task.fromJson(item.jsonString()) - _id = task._id - title.postValue(task.title) - done.postValue(task.done) - } catch (e: DittoException) { + task?.let { + _id = it._id + _title.value = it.title + _done.value = it.done + } + } catch (e: Throwable) { Log.e(TAG, "Unable to setup view task data", e) } } @@ -53,35 +66,25 @@ class EditScreenViewModel : ViewModel() { fun save() { viewModelScope.launch { try { - val titleValue = title.value ?: "" - val doneValue = done.value ?: false + val titleValue = _title.value + val doneValue = _done.value if (_id == null) { // Add tasks into the ditto collection using DQL INSERT statement // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents - val addMap = DittoCborSerializable.Dictionary( - mapOf( - Utf8String("title") to Utf8String(titleValue), - Utf8String("done") to DittoCborSerializable.BooleanValue(doneValue), - Utf8String("deleted") to DittoCborSerializable.BooleanValue(false) - ) - ) ditto.store.execute( "INSERT INTO tasks DOCUMENTS (:doc)", - DittoCborSerializable.Dictionary( - mapOf(Utf8String("doc") to addMap) + mapOf( + "doc" to mapOf( + "title" to titleValue, + "done" to doneValue, + "deleted" to false + ) ) ) } else { - // Update tasks into the ditto collection using DQL UPDATE statement + // Update tasks in the ditto collection using DQL UPDATE statement // https://docs.ditto.live/sdk/latest/crud/update#updating _id?.let { id -> - val editMap = DittoCborSerializable.Dictionary( - mapOf( - Utf8String("title") to Utf8String(titleValue), - Utf8String("done") to DittoCborSerializable.BooleanValue(doneValue), - Utf8String("id") to DittoCborSerializable.Utf8String(id) - ) - ) ditto.store.execute( """ UPDATE tasks @@ -91,7 +94,11 @@ class EditScreenViewModel : ViewModel() { WHERE _id = :id AND NOT deleted """, - arguments = editMap + mapOf( + "title" to titleValue, + "done" to doneValue, + "id" to id + ) ) } } @@ -109,11 +116,7 @@ class EditScreenViewModel : ViewModel() { _id?.let { id -> ditto.store.execute( "UPDATE tasks SET deleted = true WHERE _id = :id", - DittoCborSerializable.Dictionary( - mapOf( - Utf8String("id") to Utf8String(id) - ) - ) + mapOf("id" to id) ) } } catch (e: Throwable) { diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt index 502156daa..1a59dc39e 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -39,6 +38,7 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import live.ditto.quickstart.tasks.BuildConfig @@ -50,8 +50,8 @@ import java.util.UUID @Composable fun TasksListScreen(navController: NavController) { val tasksListViewModel: TasksListScreenViewModel = viewModel() - val tasks: List by tasksListViewModel.tasks.observeAsState(emptyList()) - val syncEnabled: Boolean by tasksListViewModel.syncEnabled.observeAsState(true) + val tasks: List by tasksListViewModel.tasks.collectAsStateWithLifecycle() + val syncEnabled: Boolean by tasksListViewModel.syncEnabled.collectAsStateWithLifecycle() var showDeleteDialog by remember { mutableStateOf(false) } var deleteDialogTaskId by remember { mutableStateOf("") } diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt index 819fe8ee5..b41e36fdd 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/list/TasksListScreenViewModel.kt @@ -5,22 +5,21 @@ import android.util.Log import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ditto.kotlin.DittoSyncSubscription +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import live.ditto.quickstart.tasks.DittoHandler.Companion.ditto import live.ditto.quickstart.tasks.TasksApplication import live.ditto.quickstart.tasks.data.Task -import com.ditto.kotlin.serialization.DittoCborSerializable -import com.ditto.kotlin.serialization.DittoCborSerializable.Utf8String -import okio.Utf8 - // The value of the Sync switch is stored in persistent settings private val Context.preferencesDataStore by preferencesDataStore("tasks_list_settings") private val SYNC_ENABLED_KEY = booleanPreferencesKey("sync_enabled") @@ -35,10 +34,14 @@ class TasksListScreenViewModel : ViewModel() { private val preferencesDataStore = TasksApplication.applicationContext().preferencesDataStore - val tasks: MutableLiveData> = MutableLiveData(emptyList()) + // Use StateFlow with store.observe() for reactive updates + // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers + val tasks: StateFlow> = ditto.store.observe(QUERY) { result -> + result.items.map { item -> Task.fromJson(item.jsonString()) } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) - private val _syncEnabled = MutableLiveData(true) - val syncEnabled: LiveData = _syncEnabled + private val _syncEnabled = MutableStateFlow(true) + val syncEnabled: StateFlow = _syncEnabled.asStateFlow() private var syncSubscription: DittoSyncSubscription? = null @@ -49,11 +52,11 @@ class TasksListScreenViewModel : ViewModel() { } _syncEnabled.value = enabled - if (enabled && !ditto.isSyncActive) { + if (enabled && !ditto.sync.isActive) { try { - // starting sync + // Starting sync // https://docs.ditto.live/sdk/latest/sync/start-and-stop-sync - ditto.startSync() + ditto.sync.start() // Register a subscription, which determines what data syncs to this peer // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions @@ -61,11 +64,11 @@ class TasksListScreenViewModel : ViewModel() { } catch (e: Throwable) { Log.e(TAG, "Unable to start sync", e) } - } else if (ditto.isSyncActive) { + } else if (!enabled && ditto.sync.isActive) { try { syncSubscription?.close() syncSubscription = null - ditto.stopSync() + ditto.sync.stop() } catch (e: Throwable) { Log.e(TAG, "Unable to stop sync", e) } @@ -74,7 +77,6 @@ class TasksListScreenViewModel : ViewModel() { } init { - // Defensive check - should never fail with synchronous initialization check(live.ditto.quickstart.tasks.DittoHandler.isInitialized) { "Ditto must be initialized before ViewModels are created" } @@ -82,15 +84,6 @@ class TasksListScreenViewModel : ViewModel() { viewModelScope.launch { populateTasksCollection() - // Register observer, which runs against the local database on this peer - // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers - ditto.store.registerObserver(QUERY) { result -> - val list = result.items.map { - item -> Task.fromJson(item.jsonString()) - } - tasks.postValue(list) - } - setSyncEnabled( preferencesDataStore.data.map { prefs -> prefs[SYNC_ENABLED_KEY] ?: true }.first() ) @@ -98,39 +91,24 @@ class TasksListScreenViewModel : ViewModel() { } // Add initial tasks to the collection if they have not already been added. - private fun populateTasksCollection() { - viewModelScope.launch { - val tasks = listOf( - Task("50191411-4C46-4940-8B72-5F8017A04FA7", "Buy groceries"), - Task("6DA283DA-8CFE-4526-A6FA-D385089364E5", "Clean the kitchen"), - Task("5303DDF8-0E72-4FEB-9E82-4B007E5797F0", "Schedule dentist appointment"), - Task("38411F1B-6B49-4346-90C3-0B16CE97E174", "Pay bills") - ) - - tasks.forEach { task -> - try { - // Add tasks into the ditto collection using DQL INSERT statement - // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents - val addMap = DittoCborSerializable.Dictionary( - mapOf( - Utf8String("_id") to Utf8String(task._id), - Utf8String("title") to Utf8String(task.title), - Utf8String("done") to DittoCborSerializable.BooleanValue(task.done), - Utf8String("deleted") to DittoCborSerializable.BooleanValue(task.deleted) - - ) - ) - ditto.store.execute( - "INSERT INTO tasks INITIAL DOCUMENTS (:task)", - DittoCborSerializable.Dictionary( - mapOf( - Utf8String("task") to addMap - ) - ) - ) - } catch (e: Throwable) { - Log.e(TAG, "Unable to insert initial document", e) - } + private suspend fun populateTasksCollection() { + val tasks = listOf( + Task("50191411-4C46-4940-8B72-5F8017A04FA7", "Buy groceries"), + Task("6DA283DA-8CFE-4526-A6FA-D385089364E5", "Clean the kitchen"), + Task("5303DDF8-0E72-4FEB-9E82-4B007E5797F0", "Schedule dentist appointment"), + Task("38411F1B-6B49-4346-90C3-0B16CE97E174", "Pay bills") + ) + + tasks.forEach { task -> + try { + // Add tasks into the ditto collection using DQL INSERT statement + // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents + ditto.store.execute( + "INSERT INTO tasks INITIAL DOCUMENTS (:task)", + mapOf("task" to task.toMap()) + ) + } catch (e: Throwable) { + Log.e(TAG, "Unable to insert initial document", e) } } } @@ -138,28 +116,21 @@ class TasksListScreenViewModel : ViewModel() { fun toggle(taskId: String) { viewModelScope.launch { try { - val doc = ditto.store.execute( + val task = ditto.store.execute( "SELECT * FROM tasks WHERE _id = :_id AND NOT deleted", - DittoCborSerializable.Dictionary( - mapOf( - Utf8String("_id") to Utf8String(taskId) - ) - ) - ).items.first() - - val done = doc.value["done"].boolean + mapOf("_id" to taskId) + ) { result -> + result.items.firstOrNull()?.let { Task.fromJson(it.jsonString()) } + } - // Update tasks into the ditto collection using DQL UPDATE statement - // https://docs.ditto.live/sdk/latest/crud/update#updating - ditto.store.execute( - "UPDATE tasks SET done = :toggled WHERE _id = :_id AND NOT deleted", - DittoCborSerializable.Dictionary( - mapOf( - Utf8String("toggled") to DittoCborSerializable.BooleanValue(!done), - Utf8String("_id") to Utf8String(taskId) - ) + task?.let { + // Update tasks in the ditto collection using DQL UPDATE statement + // https://docs.ditto.live/sdk/latest/crud/update#updating + ditto.store.execute( + "UPDATE tasks SET done = :toggled WHERE _id = :_id AND NOT deleted", + mapOf("toggled" to !it.done, "_id" to taskId) ) - ) + } } catch (e: Throwable) { Log.e(TAG, "Unable to toggle done state", e) } @@ -173,11 +144,7 @@ class TasksListScreenViewModel : ViewModel() { // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern ditto.store.execute( "UPDATE tasks SET deleted = true WHERE _id = :id", - DittoCborSerializable.Dictionary( - mapOf( - Utf8String("id") to Utf8String(taskId) - ) - ) + mapOf("id" to taskId) ) } catch (e: Throwable) { Log.e(TAG, "Unable to set deleted=true", e) diff --git a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/ui/theme/Theme.kt b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/ui/theme/Theme.kt index 877540455..6cdb3b02d 100644 --- a/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/ui/theme/Theme.kt +++ b/android-kotlin/QuickStartTasks/app/src/main/java/live/ditto/quickstart/tasks/ui/theme/Theme.kt @@ -1,6 +1,5 @@ package live.ditto.quickstart.tasks.ui.theme -import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -9,12 +8,7 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat private val DarkColorScheme = darkColorScheme( primary = Purple80, @@ -55,20 +49,6 @@ fun QuickStartTasksTheme( else -> LightColorScheme } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - // Set the status bar color to match the theme - window.statusBarColor = Color.Transparent.toArgb() - // Set the navigation bar color to match the theme - window.navigationBarColor = if (darkTheme) Color.Black.toArgb() else Color.White.toArgb() - // Set the system bar icons to be light or dark based on the theme - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme - WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = !darkTheme - } - } - MaterialTheme( colorScheme = colorScheme, typography = Typography, diff --git a/android-kotlin/QuickStartTasks/gradle/libs.versions.toml b/android-kotlin/QuickStartTasks/gradle/libs.versions.toml index 9be277c03..d6a0204d4 100644 --- a/android-kotlin/QuickStartTasks/gradle/libs.versions.toml +++ b/android-kotlin/QuickStartTasks/gradle/libs.versions.toml @@ -2,20 +2,21 @@ agp = "8.9.3" kotlin = "2.1.0" coreKtx = "1.16.0" +lifecycleRuntimeCompose = "2.10.0" junit = "4.13.2" -junitVersion = "1.2.1" -espressoCore = "3.6.1" -lifecycleRuntimeKtx = "2.9.2" -activityCompose = "1.10.1" +junitVersion = "1.3.0" +espressoCore = "3.7.0" +lifecycleRuntimeKtx = "2.10.0" +activityCompose = "1.13.0" composeBom = "2025.07.00" navigationCompose = "2.9.2" -runtimeLivedata = "1.8.3" +runtimeLivedata = "1.10.6" appcompat = "1.7.1" -datastorePreferences = "1.1.7" +datastorePreferences = "1.2.1" koin-bom = "4.1.0" coroutines-tests = "1.10.2" -ditto = "5.0.0-dev-weekly.20260126.180" -monitor = "1.7.2" +ditto = "5.0.0-rc.3" +monitor = "1.8.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -36,6 +37,7 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } androidx-appcompat = { module = "androidx.appcompat:appcompat", name = "appcompat", version.ref = "appcompat" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", name = "datastore-preferences", version.ref = "datastorePreferences" } +androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } koin-bom = { group = "io.insert-koin", name = "koin-bom", version.ref = "koin-bom" } koin-core = { group = "io.insert-koin", name = "koin-core" } koin-android = { group = "io.insert-koin", name = "koin-android" } diff --git a/swift/Tasks.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/Tasks.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9d0559ad6..389c720ed 100644 --- a/swift/Tasks.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/swift/Tasks.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getditto/DittoSwiftPackage", "state" : { - "revision" : "c68c60c68ca4783248466781fd7607e1e59af198", - "version" : "4.13.1" + "revision" : "61b203cffc6a85c1c0d0987d1833d62eca7c549c", + "version" : "4.14.3" } } ], From 8baf8c0b5c1e14b07a4f8d51b164037562edf378 Mon Sep 17 00:00:00 2001 From: Aaron LaBeau <80424345+biozal@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:45:56 -0500 Subject: [PATCH 3/3] Updated to try and fix CI/CD Pipeline --- android-kotlin/QuickStartTasks/app/build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android-kotlin/QuickStartTasks/app/build.gradle.kts b/android-kotlin/QuickStartTasks/app/build.gradle.kts index a4bbf9c15..c2a027a23 100644 --- a/android-kotlin/QuickStartTasks/app/build.gradle.kts +++ b/android-kotlin/QuickStartTasks/app/build.gradle.kts @@ -43,9 +43,10 @@ androidComponents { ) buildConfigFields.forEach { (key, description) -> + val rawValue = prop[key]?.toString()?.trim('"') ?: "" it.buildConfigFields.put( key, - BuildConfigField("String", "${prop[key]}", description) + BuildConfigField("String", "\"$rawValue\"", description) ) } }