From 0e651ba7a8bf980777625585251f5b5cb0936215 Mon Sep 17 00:00:00 2001 From: Aaron LaBeau <80424345+biozal@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:58:17 -0500 Subject: [PATCH 1/3] feat: Updated the app for v5 s puport --- android-java/app/build.gradle.kts | 24 +-- .../com/example/dittotasks/DittoHelper.kt | 75 ++++++++ .../com/example/dittotasks/MainActivity.java | 173 +++++++----------- .../java/com/example/dittotasks/Task.java | 21 ++- android-java/gradle/libs.versions.toml | 4 +- 5 files changed, 169 insertions(+), 128 deletions(-) create mode 100644 android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt diff --git a/android-java/app/build.gradle.kts b/android-java/app/build.gradle.kts index 125486953..475e78a9e 100644 --- a/android-java/app/build.gradle.kts +++ b/android-java/app/build.gradle.kts @@ -6,7 +6,6 @@ import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) } fun loadEnvProperties(): Properties { @@ -38,6 +37,10 @@ fun loadEnvProperties(): Properties { // // More information can be found here: // https://docs.ditto.live/sdk/latest/install-guides/java/android#integrating-and-initializing +fun envValue(prop: Properties, key: String): String { + return prop[key]?.toString()?.trim('"') ?: "" +} + androidComponents { onVariants { val prop = loadEnvProperties() @@ -45,7 +48,7 @@ androidComponents { "DITTO_APP_ID", BuildConfigField( "String", - "\"${prop["DITTO_APP_ID"]}\"", + "\"${envValue(prop, "DITTO_APP_ID")}\"", "Ditto application ID" ) ) @@ -53,7 +56,7 @@ androidComponents { "DITTO_PLAYGROUND_TOKEN", BuildConfigField( "String", - "\"${prop["DITTO_PLAYGROUND_TOKEN"]}\"", + "\"${envValue(prop, "DITTO_PLAYGROUND_TOKEN")}\"", "Ditto online playground authentication token" ) ) @@ -62,7 +65,7 @@ androidComponents { "DITTO_AUTH_URL", BuildConfigField( "String", - "\"${prop["DITTO_AUTH_URL"]}\"", + "\"${envValue(prop, "DITTO_AUTH_URL")}\"", "Ditto Auth URL" ) ) @@ -71,7 +74,7 @@ androidComponents { "DITTO_WEBSOCKET_URL", BuildConfigField( "String", - "\"${prop["DITTO_WEBSOCKET_URL"]}\"", + "\"${envValue(prop, "DITTO_WEBSOCKET_URL")}\"", "Ditto Websocket URL" ) ) @@ -114,7 +117,6 @@ android { } buildFeatures { buildConfig = true - compose = true } // This ensures Ditto can produce meaningful stack traces packaging { @@ -129,12 +131,6 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.ui) - implementation(libs.androidx.ui.graphics) - implementation(libs.androidx.ui.tooling.preview) - implementation(libs.androidx.material3) implementation(libs.ditto) implementation(libs.androidx.recyclerview) implementation(libs.material) @@ -143,8 +139,4 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation("androidx.test.espresso:espresso-contrib:3.6.1") - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.ui.test.junit4) - debugImplementation(libs.androidx.ui.tooling) - debugImplementation(libs.androidx.ui.test.manifest) } diff --git a/android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt b/android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt new file mode 100644 index 000000000..bf3a6e3c2 --- /dev/null +++ b/android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt @@ -0,0 +1,75 @@ +package com.example.dittotasks + +import com.ditto.kotlin.Ditto +import com.ditto.kotlin.DittoAuthenticationProvider +import com.ditto.kotlin.DittoConfig +import com.ditto.kotlin.DittoException +import com.ditto.kotlin.DittoFactory +import com.ditto.kotlin.DittoQueryResult +import com.ditto.kotlin.DittoStoreObserver +import com.ditto.kotlin.DittoSyncSubscription +import kotlinx.coroutines.runBlocking +import java.util.function.Consumer + +/** + * Bridges Ditto v5 Kotlin SDK suspend functions for Java callers. + */ +object DittoHelper { + + @JvmStatic + fun createDitto(appId: String, serverUrl: String): Ditto { + val config = DittoConfig( + databaseId = appId, + connect = DittoConfig.Connect.Server(serverUrl) + ) + return DittoFactory.create(config) + } + + @JvmStatic + fun setupAuth(ditto: Ditto, token: String) { + ditto.auth?.let { auth -> + auth.expirationHandler = { dittoInstance, _ -> + dittoInstance.auth?.login(token, DittoAuthenticationProvider.development()) + } + } + } + + @JvmStatic + @Throws(DittoException::class) + fun execute(ditto: Ditto, query: String, args: Map) { + runBlocking { + ditto.store.execute(query, args) + } + } + + @JvmStatic + fun registerSubscription(ditto: Ditto, query: String): DittoSyncSubscription { + return ditto.sync.registerSubscription(query) + } + + @JvmStatic + fun registerObserver( + ditto: Ditto, + query: String, + callback: Consumer + ): DittoStoreObserver { + return ditto.store.registerObserver(query) { result -> + callback.accept(result) + } + } + + @JvmStatic + fun startSync(ditto: Ditto) { + ditto.sync.start() + } + + @JvmStatic + fun stopSync(ditto: Ditto) { + ditto.sync.stop() + } + + @JvmStatic + fun isSyncActive(ditto: Ditto): Boolean { + return ditto.sync.isActive + } +} diff --git a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java index f06dce793..0ff7d95d2 100644 --- a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java +++ b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java @@ -1,7 +1,10 @@ package com.example.dittotasks; +import android.Manifest; import android.app.AlertDialog; +import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.View; @@ -15,25 +18,18 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.ditto.kotlin.Ditto; +import com.ditto.kotlin.DittoStoreObserver; +import com.ditto.kotlin.DittoSyncSubscription; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -import kotlin.Unit; -import live.ditto.Ditto; -import live.ditto.DittoDependencies; -import live.ditto.DittoError; -import live.ditto.DittoIdentity; -import live.ditto.DittoStoreObserver; -import live.ditto.DittoSyncSubscription; -import live.ditto.android.DefaultAndroidDittoDependencies; -import live.ditto.transports.DittoSyncPermissions; -import live.ditto.transports.DittoTransportConfig; - public class MainActivity extends ComponentActivity { private TaskAdapter taskAdapter; private SwitchCompat syncSwitch; @@ -47,9 +43,6 @@ public class MainActivity extends ComponentActivity { private String DITTO_AUTH_URL = BuildConfig.DITTO_AUTH_URL; private String DITTO_WEBSOCKET_URL = BuildConfig.DITTO_WEBSOCKET_URL; - // This is required to be set to false to use the correct URLs - private Boolean DITTO_ENABLE_CLOUD_SYNC = true; - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -104,16 +97,11 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { void initDitto() { Log.d("DittoInit", "=== Starting Ditto initialization ==="); - - // Enable Ditto's internal debug logging (if available) - Log.d("DittoInit", "Ditto Logger class not available in this version, using Android Log instead"); - + Log.d("DittoInit", "DITTO_APP_ID: " + DITTO_APP_ID); Log.d("DittoInit", "DITTO_PLAYGROUND_TOKEN: " + (DITTO_PLAYGROUND_TOKEN != null ? "Present" : "NULL")); Log.d("DittoInit", "DITTO_AUTH_URL: " + DITTO_AUTH_URL); - Log.d("DittoInit", "DITTO_WEBSOCKET_URL: " + DITTO_WEBSOCKET_URL); - Log.d("DittoInit", "DITTO_ENABLE_CLOUD_SYNC: " + DITTO_ENABLE_CLOUD_SYNC); - + // Skip permission requests during testing to avoid permission dialogs if (!isInstrumentationTest()) { Log.d("DittoInit", "Requesting permissions..."); @@ -124,78 +112,43 @@ void initDitto() { Log.d("DittoInit", "Starting Ditto SDK initialization..."); try { - Log.d("DittoInit", "Creating AndroidDependencies..."); - DittoDependencies androidDependencies = new DefaultAndroidDittoDependencies(getApplicationContext()); - Log.d("DittoInit", "AndroidDependencies created successfully"); - - /* - * Setup Ditto Identity - * https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing - */ - Log.d("DittoInit", "Creating DittoIdentity.OnlinePlayground..."); - var identity = new DittoIdentity - .OnlinePlayground( - androidDependencies, - DITTO_APP_ID, - DITTO_PLAYGROUND_TOKEN, - DITTO_ENABLE_CLOUD_SYNC, // This is required to be set to false to use the correct URLs - DITTO_AUTH_URL); - Log.d("DittoInit", "DittoIdentity created successfully"); - + // Create Ditto with server connection + // https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing Log.d("DittoInit", "Creating Ditto instance..."); - ditto = new Ditto(androidDependencies, identity); + ditto = DittoHelper.createDitto(DITTO_APP_ID, DITTO_AUTH_URL); Log.d("DittoInit", "Ditto instance created successfully"); - //https://docs.ditto.live/sdk/latest/sync/customizing-transport-configurations - Log.d("DittoInit", "Updating transport config..."); - ditto.updateTransportConfig(config -> { - config.getConnect().getWebsocketUrls().add(DITTO_WEBSOCKET_URL); - - // lambda must return Kotlin Unit which corresponds to 'void' in Java - return kotlin.Unit.INSTANCE; - }); - Log.d("DittoInit", "Transport config updated"); - - // disable sync with v3 peers, required for DQL - Log.d("DittoInit", "Disabling sync with v3..."); - ditto.disableSyncWithV3(); - Log.d("DittoInit", "Sync with v3 disabled"); - - // Disable DQL strict mode - // when set to false, collection definitions are no longer required. SELECT queries will return and display all fields by default. - // https://docs.ditto.live/dql/strict-mode - Log.d("DittoInit", "Setting DQL strict mode to false..."); - ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false"); - Log.d("DittoInit", "DQL strict mode disabled"); + // Set up authentication handler (must be set before sync.start()) + Log.d("DittoInit", "Setting up authentication..."); + DittoHelper.setupAuth(ditto, DITTO_PLAYGROUND_TOKEN); + Log.d("DittoInit", "Authentication configured"); // register subscription // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions Log.d("DittoInit", "Registering subscription..."); - taskSubscription = ditto.sync.registerSubscription("SELECT * FROM tasks"); + taskSubscription = DittoHelper.registerSubscription(ditto, "SELECT * FROM tasks"); Log.d("DittoInit", "Subscription registered"); // register observer for live query // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers Log.d("DittoInit", "Registering observer..."); - taskObserver = ditto.store.registerObserver("SELECT * FROM tasks WHERE deleted=false ORDER BY title ASC", null, result -> { - Log.d("DittoInit", "Observer callback triggered with " + result.getItems().size() + " items"); - var tasks = result.getItems().stream().map(Task::fromQueryItem).collect(Collectors.toCollection(ArrayList::new)); - runOnUiThread(() -> { - Log.d("DittoInit", "Updating UI with " + tasks.size() + " tasks"); - taskAdapter.setTasks(new ArrayList<>(tasks)); - }); - return Unit.INSTANCE; - }); + taskObserver = DittoHelper.registerObserver(ditto, + "SELECT * FROM tasks WHERE deleted=false ORDER BY title ASC", + result -> { + Log.d("DittoInit", "Observer callback triggered with " + result.getItems().size() + " items"); + var tasks = result.getItems().stream().map(Task::fromQueryItem).collect(Collectors.toCollection(ArrayList::new)); + runOnUiThread(() -> { + Log.d("DittoInit", "Updating UI with " + tasks.size() + " tasks"); + taskAdapter.setTasks(new ArrayList<>(tasks)); + }); + }); Log.d("DittoInit", "Observer registered"); Log.d("DittoInit", "Starting Ditto sync..."); - ditto.startSync(); + DittoHelper.startSync(ditto); Log.d("DittoInit", "=== Ditto initialization completed successfully ==="); - } catch (DittoError e) { - Log.e("DittoInit", "DittoError during initialization: " + e.getMessage(), e); - e.printStackTrace(); } catch (Exception e) { - Log.e("DittoInit", "Unexpected error during Ditto initialization: " + e.getMessage(), e); + Log.e("DittoInit", "Error during Ditto initialization: " + e.getMessage(), e); e.printStackTrace(); } } @@ -213,8 +166,27 @@ private boolean isInstrumentationTest() { // Request permissions for Ditto // https://docs.ditto.live/sdk/latest/install-guides/java#requesting-permissions-at-runtime void requestPermissions() { - DittoSyncPermissions permissions = new DittoSyncPermissions(this); - String[] missing = permissions.missingPermissions(permissions.requiredPermissions()); + List permissions = new ArrayList<>(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_ADVERTISE); + permissions.add(Manifest.permission.BLUETOOTH_CONNECT); + permissions.add(Manifest.permission.BLUETOOTH_SCAN); + } + if (Build.VERSION.SDK_INT <= 32) { + permissions.add(Manifest.permission.ACCESS_FINE_LOCATION); + } + if (Build.VERSION.SDK_INT <= 30) { + permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissions.add(Manifest.permission.NEARBY_WIFI_DEVICES); + } + + String[] missing = permissions.stream() + .filter(p -> checkSelfPermission(p) != PackageManager.PERMISSION_GRANTED) + .toArray(String[]::new); + if (missing.length > 0) { this.requestPermissions(missing, 0); } @@ -226,28 +198,25 @@ private void createTask(String title) { task.put("done", false); task.put("deleted", false); - HashMap args = new HashMap<>(); - args.put("task", task); - try { + Map args = Map.of("task", 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 DOCUMENTS (:task)", args); - } catch (DittoError e) { + DittoHelper.execute(ditto, "INSERT INTO tasks DOCUMENTS (:task)", args); + } catch (Exception e) { e.printStackTrace(); } } private void editTaskTitle(Task task, String newTitle) { - HashMap args = new HashMap<>(); - args.put("id", task.getId()); - args.put("title", newTitle); + Map args = Map.of("id", task.getId(), "title", newTitle); try { // 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 title=:title WHERE _id=:id", args); - } catch (DittoError e) { + DittoHelper.execute(ditto, "UPDATE tasks SET title=:title WHERE _id=:id", args); + } catch (Exception e) { e.printStackTrace(); } } @@ -257,16 +226,14 @@ private void toggleTask(Task task) { Log.i("MainActivity", "Ditto disabled - toggle task ignored: " + task.getTitle()); return; } - - HashMap args = new HashMap<>(); - args.put("id", task.getId()); - args.put("done", !task.isDone()); + + Map args = Map.of("id", task.getId(), "done", !task.isDone()); try { // 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=:done WHERE _id=:id", args); - } catch (DittoError e) { + DittoHelper.execute(ditto, "UPDATE tasks SET done=:done WHERE _id=:id", args); + } catch (Exception e) { e.printStackTrace(); } } @@ -276,14 +243,14 @@ private void deleteTask(Task task) { Log.i("MainActivity", "Ditto disabled - delete task ignored: " + task.getTitle()); return; } - - HashMap args = new HashMap<>(); - args.put("id", task.getId()); + + Map args = Map.of("id", task.getId()); + try { // UPDATE DQL Statement using Soft-Delete pattern // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern - ditto.store.execute("UPDATE tasks SET deleted=true WHERE _id=:id", args); - } catch (DittoError e) { + DittoHelper.execute(ditto, "UPDATE tasks SET deleted=true WHERE _id=:id", args); + } catch (Exception e) { e.printStackTrace(); } } @@ -293,7 +260,7 @@ private void toggleSync() { return; } - boolean isSyncActive = ditto.isSyncActive(); + boolean isSyncActive = DittoHelper.isSyncActive(ditto); var nextColor = isSyncActive ? null : ColorStateList.valueOf(0xFFBB86FC); var nextText = isSyncActive ? "Sync Inactive" : "Sync Active"; @@ -301,14 +268,14 @@ private void toggleSync() { // https://docs.ditto.live/sdk/latest/sync/start-and-stop-sync try { if (isSyncActive) { - ditto.stopSync(); + DittoHelper.stopSync(ditto); } else { - ditto.startSync(); + DittoHelper.startSync(ditto); } syncSwitch.setChecked(!isSyncActive); syncSwitch.setTrackTintList(nextColor); syncSwitch.setText(nextText); - } catch (DittoError e) { + } catch (Exception e) { e.printStackTrace(); } } diff --git a/android-java/app/src/main/java/com/example/dittotasks/Task.java b/android-java/app/src/main/java/com/example/dittotasks/Task.java index f9f71d105..f06109e49 100644 --- a/android-java/app/src/main/java/com/example/dittotasks/Task.java +++ b/android-java/app/src/main/java/com/example/dittotasks/Task.java @@ -2,7 +2,10 @@ import java.util.Optional; -import live.ditto.DittoQueryResultItem; +import com.ditto.kotlin.DittoQueryResultItem; + +import org.json.JSONException; +import org.json.JSONObject; public class Task { private Optional id; @@ -22,12 +25,16 @@ public Task(String id, String title, boolean done, boolean deleted) { } public static Task fromQueryItem(DittoQueryResultItem item) { - var map = item.getValue(); - return new Task( - (String) map.get("_id"), - (String) map.get("title"), - Boolean.TRUE.equals(map.get("done")), - Boolean.TRUE.equals(map.get("deleted"))); + try { + JSONObject json = new JSONObject(item.jsonString()); + return new Task( + json.optString("_id", null), + json.optString("title", null), + json.optBoolean("done", false), + json.optBoolean("deleted", false)); + } catch (JSONException e) { + throw new RuntimeException("Failed to parse task from query result", e); + } } public String getId() { diff --git a/android-java/gradle/libs.versions.toml b/android-java/gradle/libs.versions.toml index 14bc28de6..5c38551d9 100644 --- a/android-java/gradle/libs.versions.toml +++ b/android-java/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -ditto = "4.13.1" +ditto = "5.0.0-rc.3" agp = "8.7.3" constraintlayout = "2.2.0" kotlin = "2.0.0" @@ -17,7 +17,7 @@ recyclerviewV7 = "28.0.0" [libraries] androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -ditto = { module = "live.ditto:ditto", version.ref = "ditto" } +ditto = { group = "com.ditto", name = "ditto-kotlin-android", version.ref = "ditto" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } From 31e9c31e65a7e5cc5ff595ef75bd5305253cfe97 Mon Sep 17 00:00:00 2001 From: Aaron LaBeau Date: Tue, 5 May 2026 15:17:44 -0500 Subject: [PATCH 2/3] Updated to fix issues and v5 --- android-java/README.md | 13 ++++---- android-java/app/build.gradle.kts | 13 +++++--- .../com/example/dittotasks/MainActivity.java | 31 ++++++++++++------- .../java/com/example/dittotasks/Task.java | 18 +++++------ .../example/dittotasks/ExampleUnitTest.java | 17 ++++++++++ .../com/example/dittotasks/ExampleUnitTest.kt | 17 ---------- android-java/build.gradle.kts | 1 - android-java/gradle/libs.versions.toml | 18 +---------- 8 files changed, 61 insertions(+), 67 deletions(-) create mode 100644 android-java/app/src/test/java/com/example/dittotasks/ExampleUnitTest.java delete mode 100644 android-java/app/src/test/java/com/example/dittotasks/ExampleUnitTest.kt diff --git a/android-java/README.md b/android-java/README.md index 73e210320..ad34bf0d7 100644 --- a/android-java/README.md +++ b/android-java/README.md @@ -13,7 +13,7 @@ After you have completed the [common prerequisites] you will need the following: ## Documentation - [Install Guide](https://docs.ditto.live/sdk/latest/install-guides/java/android) -- [API Reference](https://software.ditto.live/android/Ditto/4.11.1/api-reference/) +- [API Reference](https://software.ditto.live/android/Ditto/5.0.0/api-reference/) - [SDK Release Notes](https://docs.ditto.live/sdk/latest/release-notes/java) [common prerequisites]: https://github.com/getditto/quickstart#common-prerequisites @@ -23,8 +23,8 @@ After you have completed the [common prerequisites] you will need the following: Assuming you have Android Studio and other prerequisites installed, you can build and run the app by following these steps: -1. Create an application at . Make note of the app ID and online playground token. -2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your App ID, Online Playground Token, Auth URL, and Websocket URL. +1. Create an application at . Make note of the database ID (used to be called app ID) and online playground token. +2. Copy the `.env.sample` file at the top level of the `quickstart` repo to `.env` and add your Database ID (used to be called AppId), Online Playground Token, and Auth URL. 3. Launch Android Studio and open the `quickstart/android-java` directory. 4. In Android Studio, select a connected Android device, or create and launch an Android emulator and select it as the destination, then choose the **Run > Run 'app'** menu item. @@ -36,8 +36,9 @@ Compatible with Android Automotive OS (AAOS) ## A Guided Tour of the Android App Source Code -The Android app is a simple to-do list app that demonstrates how to use the Ditto Android SDK to sync data with other devices. -It is implemented using Java and Android Views using an Activity and a programmatically implemented RecyclerView. +The Android app is a simple to-do list app that demonstrates how to use the Ditto Android SDK to sync data with other devices. It is implemented using Java and Android Views using an Activity and a programmatically implemented RecyclerView. + +The Ditto v5 SDK ships as a Kotlin module (`com.ditto:ditto-kotlin-android`) and exposes some APIs as `suspend` functions. To keep the application code idiomatic Java, this project uses a small Kotlin bridge file, `DittoHelper.kt`, that wraps the suspending APIs with `runBlocking` and exposes `@JvmStatic` entry points. All application logic — `MainActivity`, `Task`, and `TaskAdapter` — remains in Java. It is assumed that the reader is familiar with Android development and with Java/Activity/RecyclerView, but needs some guidance on how to use Ditto. The following is a summary of the key parts of integration with Ditto. @@ -54,7 +55,7 @@ This line in `gradle/libs.versions.toml` specifies which version of the Ditto SDK to use: ```kotlin -ditto = "4.11.1" +ditto = "5.0.0" ``` To use a newer version of the SDK, change the version number on this line. diff --git a/android-java/app/build.gradle.kts b/android-java/app/build.gradle.kts index 475e78a9e..6e3a288ca 100644 --- a/android-java/app/build.gradle.kts +++ b/android-java/app/build.gradle.kts @@ -1,7 +1,7 @@ import com.android.build.api.variant.BuildConfigField import java.io.FileInputStream -import java.io.FileNotFoundException import java.util.Properties +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.application) @@ -89,7 +89,7 @@ android { defaultConfig { applicationId = "com.example.dittotasks" minSdk = 24 - targetSdk = 34 + targetSdk = 35 versionCode = 1 versionName = "1.0" @@ -112,9 +112,6 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" - } buildFeatures { buildConfig = true } @@ -127,6 +124,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } +} + dependencies { implementation(libs.androidx.core.ktx) diff --git a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java index 0ff7d95d2..3cc415917 100644 --- a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java +++ b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java @@ -38,10 +38,9 @@ public class MainActivity extends ComponentActivity { DittoSyncSubscription taskSubscription; DittoStoreObserver taskObserver; - private String DITTO_APP_ID = BuildConfig.DITTO_APP_ID; - private String DITTO_PLAYGROUND_TOKEN = BuildConfig.DITTO_PLAYGROUND_TOKEN; - private String DITTO_AUTH_URL = BuildConfig.DITTO_AUTH_URL; - private String DITTO_WEBSOCKET_URL = BuildConfig.DITTO_WEBSOCKET_URL; + private final String dittoAppId = BuildConfig.DITTO_APP_ID; + private final String dittoPlaygroundToken = BuildConfig.DITTO_PLAYGROUND_TOKEN; + private final String dittoAuthUrl = BuildConfig.DITTO_AUTH_URL; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -58,10 +57,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { // Populate connection info (only in debug builds) if(BuildConfig.DEBUG) { TextView appId = findViewById(R.id.ditto_app_id); - appId.setText(String.format("App ID: %s", DITTO_APP_ID)); + appId.setText(String.format("App ID: %s", dittoAppId)); TextView playgroundToken = findViewById(R.id.ditto_playground_token); - playgroundToken.setText(String.format("Playground Token: %s", DITTO_PLAYGROUND_TOKEN)); + playgroundToken.setText(String.format("Playground Token: %s", dittoPlaygroundToken)); } else { // Hide credential views in production findViewById(R.id.ditto_app_id).setVisibility(View.GONE); @@ -98,9 +97,9 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { void initDitto() { Log.d("DittoInit", "=== Starting Ditto initialization ==="); - Log.d("DittoInit", "DITTO_APP_ID: " + DITTO_APP_ID); - Log.d("DittoInit", "DITTO_PLAYGROUND_TOKEN: " + (DITTO_PLAYGROUND_TOKEN != null ? "Present" : "NULL")); - Log.d("DittoInit", "DITTO_AUTH_URL: " + DITTO_AUTH_URL); + Log.d("DittoInit", "DITTO_APP_ID: " + dittoAppId); + Log.d("DittoInit", "DITTO_PLAYGROUND_TOKEN: " + (dittoPlaygroundToken != null ? "Present" : "NULL")); + Log.d("DittoInit", "DITTO_AUTH_URL: " + dittoAuthUrl); // Skip permission requests during testing to avoid permission dialogs if (!isInstrumentationTest()) { @@ -115,12 +114,12 @@ void initDitto() { // Create Ditto with server connection // https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing Log.d("DittoInit", "Creating Ditto instance..."); - ditto = DittoHelper.createDitto(DITTO_APP_ID, DITTO_AUTH_URL); + ditto = DittoHelper.createDitto(dittoAppId, dittoAuthUrl); Log.d("DittoInit", "Ditto instance created successfully"); // Set up authentication handler (must be set before sync.start()) Log.d("DittoInit", "Setting up authentication..."); - DittoHelper.setupAuth(ditto, DITTO_PLAYGROUND_TOKEN); + DittoHelper.setupAuth(ditto, dittoPlaygroundToken); Log.d("DittoInit", "Authentication configured"); // register subscription @@ -193,6 +192,11 @@ void requestPermissions() { } private void createTask(String title) { + if (ditto == null) { + Log.i("MainActivity", "Ditto disabled - create task ignored: " + title); + return; + } + HashMap task = new HashMap<>(); task.put("title", title); task.put("done", false); @@ -210,6 +214,11 @@ private void createTask(String title) { } private void editTaskTitle(Task task, String newTitle) { + if (ditto == null) { + Log.i("MainActivity", "Ditto disabled - edit task ignored: " + task.getTitle()); + return; + } + Map args = Map.of("id", task.getId(), "title", newTitle); try { diff --git a/android-java/app/src/main/java/com/example/dittotasks/Task.java b/android-java/app/src/main/java/com/example/dittotasks/Task.java index f06109e49..a48450ebf 100644 --- a/android-java/app/src/main/java/com/example/dittotasks/Task.java +++ b/android-java/app/src/main/java/com/example/dittotasks/Task.java @@ -1,24 +1,22 @@ package com.example.dittotasks; -import java.util.Optional; - import com.ditto.kotlin.DittoQueryResultItem; import org.json.JSONException; import org.json.JSONObject; public class Task { - private Optional id; - private String title; - private boolean done; - private boolean deleted; + private final String id; + private final String title; + private final boolean done; + private final boolean deleted; public Task(String title) { this(null, title, false, false); } public Task(String id, String title, boolean done, boolean deleted) { - this.id = Optional.ofNullable(id); + this.id = id; this.title = title; this.done = done; this.deleted = deleted; @@ -28,8 +26,8 @@ public static Task fromQueryItem(DittoQueryResultItem item) { try { JSONObject json = new JSONObject(item.jsonString()); return new Task( - json.optString("_id", null), - json.optString("title", null), + json.isNull("_id") ? null : json.optString("_id", null), + json.isNull("title") ? null : json.optString("title", null), json.optBoolean("done", false), json.optBoolean("deleted", false)); } catch (JSONException e) { @@ -38,7 +36,7 @@ public static Task fromQueryItem(DittoQueryResultItem item) { } public String getId() { - return id.orElse(null); + return id; } public String getTitle() { diff --git a/android-java/app/src/test/java/com/example/dittotasks/ExampleUnitTest.java b/android-java/app/src/test/java/com/example/dittotasks/ExampleUnitTest.java new file mode 100644 index 000000000..fcd67efe3 --- /dev/null +++ b/android-java/app/src/test/java/com/example/dittotasks/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.example.dittotasks; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} diff --git a/android-java/app/src/test/java/com/example/dittotasks/ExampleUnitTest.kt b/android-java/app/src/test/java/com/example/dittotasks/ExampleUnitTest.kt deleted file mode 100644 index 835cd961c..000000000 --- a/android-java/app/src/test/java/com/example/dittotasks/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.dittotasks - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/android-java/build.gradle.kts b/android-java/build.gradle.kts index 952b93066..922f55110 100644 --- a/android-java/build.gradle.kts +++ b/android-java/build.gradle.kts @@ -2,5 +2,4 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false - alias(libs.plugins.kotlin.compose) apply false } \ No newline at end of file diff --git a/android-java/gradle/libs.versions.toml b/android-java/gradle/libs.versions.toml index 5c38551d9..ef0498e31 100644 --- a/android-java/gradle/libs.versions.toml +++ b/android-java/gradle/libs.versions.toml @@ -1,41 +1,25 @@ [versions] -ditto = "5.0.0-rc.3" +ditto = "5.0.0" agp = "8.7.3" -constraintlayout = "2.2.0" kotlin = "2.0.0" coreKtx = "1.10.1" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" lifecycleRuntimeKtx = "2.6.1" -activityCompose = "1.8.0" -composeBom = "2024.04.01" material = "1.12.0" recyclerview = "1.3.2" -recyclerviewV7 = "28.0.0" [libraries] -androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } ditto = { group = "com.ditto", name = "ditto-kotlin-android", version.ref = "ditto" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } -androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } -androidx-ui = { group = "androidx.compose.ui", name = "ui" } -androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } -androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" } material = { module = "com.google.android.material:material", version.ref = "material" } -recyclerview-v7 = { module = "com.android.support:recyclerview-v7", version.ref = "recyclerviewV7" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } From a3e2ef99cf46d28ece549cca6bfccecbd96a5f71 Mon Sep 17 00:00:00 2001 From: Aaron LaBeau Date: Tue, 5 May 2026 16:31:36 -0500 Subject: [PATCH 3/3] Updated based on PR comments --- android-java/app/build.gradle.kts | 15 ++------------- .../java/com/example/dittotasks/MainActivity.java | 15 ++++++++++----- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/android-java/app/build.gradle.kts b/android-java/app/build.gradle.kts index 6e3a288ca..11840d22c 100644 --- a/android-java/app/build.gradle.kts +++ b/android-java/app/build.gradle.kts @@ -18,8 +18,7 @@ fun loadEnvProperties(): Properties { val requiredEnvVars = listOf( "DITTO_APP_ID", "DITTO_PLAYGROUND_TOKEN", - "DITTO_AUTH_URL", - "DITTO_WEBSOCKET_URL" + "DITTO_AUTH_URL" ) for (envVar in requiredEnvVars) { @@ -32,8 +31,7 @@ fun loadEnvProperties(): Properties { } // Define BuildConfig.DITTO_APP_ID, BuildConfig.DITTO_PLAYGROUND_TOKEN, -// BuildConfig.DITTO_CUSTOM_AUTH_URL, BuildConfig.DITTO_WEBSOCKET_URL -// based on values in the .env file +// and BuildConfig.DITTO_AUTH_URL based on values in the .env file // // More information can be found here: // https://docs.ditto.live/sdk/latest/install-guides/java/android#integrating-and-initializing @@ -69,15 +67,6 @@ androidComponents { "Ditto Auth URL" ) ) - - it.buildConfigFields.put( - "DITTO_WEBSOCKET_URL", - BuildConfigField( - "String", - "\"${envValue(prop, "DITTO_WEBSOCKET_URL")}\"", - "Ditto Websocket URL" - ) - ) } } diff --git a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java index 3cc415917..5df56efc9 100644 --- a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java +++ b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java @@ -24,6 +24,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -90,7 +91,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { taskAdapter.setOnTaskLongPressListener(this::showEditTaskModal); // Initialize empty list - Ditto observer will populate it - taskAdapter.setTasks(List.of()); + taskAdapter.setTasks(Collections.emptyList()); } @@ -202,7 +203,7 @@ private void createTask(String title) { task.put("done", false); task.put("deleted", false); - Map args = Map.of("task", task); + Map args = Collections.singletonMap("task", task); try { // Add tasks into the ditto collection using DQL INSERT statement @@ -219,7 +220,9 @@ private void editTaskTitle(Task task, String newTitle) { return; } - Map args = Map.of("id", task.getId(), "title", newTitle); + Map args = new HashMap<>(); + args.put("id", task.getId()); + args.put("title", newTitle); try { // Update tasks into the ditto collection using DQL UPDATE statement @@ -236,7 +239,9 @@ private void toggleTask(Task task) { return; } - Map args = Map.of("id", task.getId(), "done", !task.isDone()); + Map args = new HashMap<>(); + args.put("id", task.getId()); + args.put("done", !task.isDone()); try { // Update tasks into the ditto collection using DQL UPDATE statement @@ -253,7 +258,7 @@ private void deleteTask(Task task) { return; } - Map args = Map.of("id", task.getId()); + Map args = Collections.singletonMap("id", task.getId()); try { // UPDATE DQL Statement using Soft-Delete pattern