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" }