diff --git a/android-kotlin/QuickStartTasks/app/build.gradle.kts b/android-kotlin/QuickStartTasks/app/build.gradle.kts index c10dea861..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) ) } } @@ -53,7 +54,7 @@ androidComponents { android { namespace = "live.ditto.quickstart.tasks" - compileSdk = 35 + compileSdk = 36 lint { baseline = file("lint-baseline.xml") @@ -61,7 +62,7 @@ android { defaultConfig { applicationId = "live.ditto.quickstart.tasks" - minSdk = 23 + minSdk = 24 targetSdk = 35 versionCode = 1 versionName = "1.0" @@ -83,12 +84,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 { @@ -96,10 +97,6 @@ android { compose = true } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.14" - } - packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" @@ -111,6 +108,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 +120,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)) @@ -132,7 +129,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..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 live.ditto.transports.DittoSyncPermissions -import android.os.StrictMode +import androidx.activity.enableEdgeToEdge +import com.ditto.kotlin.transports.DittoSyncPermissions 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 a05cae2f5..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,24 +2,14 @@ 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 live.ditto.Ditto -import live.ditto.DittoIdentity -import live.ditto.android.DefaultAndroidDittoDependencies -import live.ditto.quickstart.tasks.DittoHandler.Companion.ditto +import com.ditto.kotlin.DittoAuthenticationProvider +import com.ditto.kotlin.DittoConfig +import com.ditto.kotlin.DittoFactory +import com.ditto.kotlin.DittoLog 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) - + private val tag = "TaskApplication" companion object { private var instance: TasksApplication? = null @@ -35,43 +25,47 @@ class TasksApplication : Application() { override fun onCreate() { super.onCreate() - ioScope.launch { - setupDitto() - } + initializeDitto() + setupAuthentication() } - 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 + private fun initializeDitto() { + try { + val config = DittoConfig( + databaseId = BuildConfig.DITTO_APP_ID, + connect = DittoConfig.Connect.Server(url = BuildConfig.DITTO_AUTH_URL) + ) - /* - * 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) + DittoHandler.initialize(config) + DittoLog.d(tag, "Ditto instance created successfully") + } catch (ex: Throwable) { + DittoLog.e(tag, "Failed to initialize Ditto: $ex") + throw ex } + } - ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false") + private fun setupAuthentication() { + try { + val token = BuildConfig.DITTO_PLAYGROUND_TOKEN - // disable sync with v3 peers, required for DQL - ditto.disableSyncWithV3() + // 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") + } } } 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 4c3dfb96d..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,11 +1,12 @@ 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 live.ditto.DittoError import live.ditto.quickstart.tasks.DittoHandler.Companion.ditto import live.ditto.quickstart.tasks.data.Task @@ -17,26 +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?) { - canDelete.postValue(id != null) + check(live.ditto.quickstart.tasks.DittoHandler.isInitialized) { + "Ditto must be initialized before ViewModels are created" + } + + _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) - ).items.first() + ) { 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: DittoError) { + 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) } } @@ -45,6 +66,8 @@ class EditScreenViewModel : ViewModel() { fun save() { viewModelScope.launch { try { + 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 @@ -52,14 +75,14 @@ class EditScreenViewModel : ViewModel() { "INSERT INTO tasks DOCUMENTS (:doc)", mapOf( "doc" to mapOf( - "title" to title.value, - "done" to done.value, + "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 -> ditto.store.execute( @@ -72,14 +95,14 @@ class EditScreenViewModel : ViewModel() { AND NOT deleted """, mapOf( - "title" to title.value, - "done" to done.value, + "title" to titleValue, + "done" to doneValue, "id" to id ) ) } } - } catch (e: DittoError) { + } catch (e: Throwable) { Log.e(TAG, "Unable to save task", e) } } @@ -96,7 +119,7 @@ class EditScreenViewModel : ViewModel() { mapOf("id" to 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/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 c716696bf..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,15 +5,17 @@ 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.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 @@ -27,15 +29,19 @@ 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 - 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 @@ -46,24 +52,24 @@ 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 syncSubscription = ditto.sync.registerSubscription(QUERY) - } catch (e: DittoError) { + } 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() - } catch (e: DittoError) { + ditto.sync.stop() + } catch (e: Throwable) { Log.e(TAG, "Unable to stop sync", e) } } @@ -71,16 +77,13 @@ class TasksListScreenViewModel : ViewModel() { } init { + 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()) } - tasks.postValue(list) - } - setSyncEnabled( preferencesDataStore.data.map { prefs -> prefs[SYNC_ENABLED_KEY] ?: true }.first() ) @@ -88,33 +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 - 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, - ) - ) - ) - } catch (e: DittoError) { - 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) } } } @@ -122,23 +116,22 @@ 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", mapOf("_id" to taskId) - ).items.first() - - val done = doc.value["done"] as Boolean + ) { 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", - mapOf( - "toggled" to !done, - "_id" to 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: DittoError) { + } + } catch (e: Throwable) { Log.e(TAG, "Unable to toggle done state", e) } } @@ -153,7 +146,7 @@ class TasksListScreenViewModel : ViewModel() { "UPDATE tasks SET deleted = true WHERE _id = :id", mapOf("id" to taskId) ) - } 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/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 608b52f08..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 = "4.13.1" -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,12 +37,13 @@ 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" } 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" } 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" } } ],