This guide is for integrating Ditto into an existing Flutter application. If you’re looking to get started quickly with a brand new Flutter app, follow the Flutter QuickStart.

Prerequisites

Before you begin, ensure you have the following installed:
  • Code editor (preferably Visual Studio Code or Android Studio)
  • Flutter SDK (>3.19.0)
  • An existing Flutter application to integrate Ditto into
  • Ditto Portal account with a Ditto App (see Getting SDK Connection Details)

Android

  • Android Studio installed on your machine
  • A physical Android device or Android emulator
  • Upgrade your Kotlin Gradle version to 1.9.20 or later
    1. Navigate to the root folder
    2. Open ./android/settings.gradle
    3. Update the org.jetbrains.kotlin.android plugin to 1.9.20
      plugins {
          // ...
          id "org.jetbrains.kotlin.android" version "1.9.20" apply false
      }
      

iOS

  • macOS with the latest version of Xcode installed
  • A physical iOS device or an iOS simulator

Web

Step 1: Add the Ditto Dependency

1

Run the following command in your terminal from the root of your application:
flutter pub add ditto_live
flutter pub add permission_handler
2

For iOS development, add the following permissions to ios/Runner/Info.plist:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Uses Bluetooth to connect and sync with nearby devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Uses Bluetooth to connect and sync with nearby devices</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Uses WiFi to connect and sync with nearby devices</string>
<key>NSBonjourServices</key>
<array>
  <string>_http-alt._tcp.</string>
</array>
3

For Android development, add the following permissions to android/app/src/main/AndroidManifest.xml.
<uses-permission android:name="android.permission.BLUETOOTH"
        android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
        android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"
        tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"
        tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
        android:usesPermissionFlags="neverForLocation"
        tools:targetApi="s" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
        android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
        android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
        android:usesPermissionFlags="neverForLocation"
        tools:targetApi="tiramisu" />
Additionally, if you would like Ditto to continue syncing data while the Android app is running in the background add the following permissions and service declaration to android/app/src/main/AndroidManifest.xml. Then edit the service metadata appropriately.
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" tools:targetApi="upside_down_cake" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" tools:targetApi="tiramisu" />

<application ...>
    <service
        android:name="live.ditto.transports.foregroundservice.DefaultDittoForegroundService"
        android:exported="false"
        android:foregroundServiceType="connectedDevice"
        android:stopWithTask="true">
        <meta-data android:name="notificationChannelName" android:value="Background Data Sync" />
        <meta-data android:name="notificationChannelId" android:value="BackgroundDataSyncId" />
        <meta-data android:name="smallIcon" android:resource="@drawable/small_app_icon" />
        <meta-data android:name="notificationTitle" android:value="Syncing Data" />
        <meta-data android:name="notificationText" android:value="Syncing data across connected devices" />
        <meta-data android:name="notificationId" android:value="1001" />
    </service>
</application>
Make sure you customize the service metadata—it will be displayed to the user in the foreground service notification. Note that your small_app_icon drawable must be monochrome white.The foreground service will be automatically started and stopped on startSync() and stopSync(), respectively.

Step 2: Obtain Ditto Credentials

1

Log in to your Ditto Portal account.
2

Navigate to your application and obtain the App ID, Playground Token, AuthURL, and WebSocket URL. (See Getting SDK Connection Details for details)
3

Locate the appID, playground token, authURL, and websocketURL for your application. These are required to initialize the Ditto SDK.

Step 3: Import & Initialize Ditto

1

Import the SDK in your Dart file where you plan to use it.
Dart
import 'package:ditto_live/ditto_live.dart';
2

Request device permissions.
If you are writing code that targets both web and native platforms, you must not import platform-specific libraries, such as dart:io. However, this prevents using Platform.is... to detect the current platform. Note that this is a compile-time error, so it is not possible to work around this with kIsWeb.To streamline this, the Ditto Flutter SDK provides Ditto.currentPlatform, which is usable on all platforms.
import "package:ditto_live/ditto_live.dart";
import "package:permission_handler/permission_handler.dart";

// ...

final platform = Ditto.currentPlatform;

if (platform case SupportedPlatform.android || SupportedPlatform.iOS) {
  await [
    Permission.bluetoothConnect,
    Permission.bluetoothAdvertise,
    Permission.nearbyWifiDevices,
    Permission.bluetoothScan
  ].request();
}
3

Initialize the SDK to obtain an instance of Ditto. This is the main entry point into all Ditto-related functionality.
Dart

await Ditto.init();

final identity = OnlinePlaygroundIdentity(
    appId: "REPLACE_ME_WITH_YOUR_APP_ID",
    token: "REPLACE_ME_WITH_YOUR_PLAYGROUND_TOKEN",
    customAuthUrl: "REPLACE_ME_WITH_YOUR_AUTH_URL",
    enableDittoCloudSync: false // This is required to be set to false to use the correct URLs
);
final ditto = await Ditto.open(identity);

ditto.updateTransportConfig((config) {
  // Set the Ditto Websocket URL
  config.connect.webSocketUrls.add("wss://REPLACE_ME_WITH_YOUR_WEBSOCKET_URL");
});

// Disable DQL strict mode so that collection definitions are not required in DQL queries
await ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false");

ditto.startSync();

Step 4: Creating and Reading Data

Insert Data

To insert data into the Ditto store, use the following example. This demonstrates inserting a document into a collection named items:
Dart
Future<void> insertData() async {
  final item = {'name': 'Sample Item', 'value': 100};
  await ditto.store.execute(
    "INSERT INTO items VALUES (:item)",
    arguments: {"item": item},
  );
}
Call the insertData function wherever appropriate in your application, such as in a button press handler.
Dart
FloatingActionButton(
  onPressed: insertData,
  child: Icon(Icons.add),
)

Obseve Data Store Changes

To observe changes in the data store, register an observer. This example sets up an observer to listen for changes in the items collection:
Dart
void observeData() {
  final observer = ditto.store.registerObserver(
    "SELECT * FROM items",
    onChange: (event) {
	    // Handle data changed event
	    print("data changed");
	  },
  );
}
Call the observeData function, typically in the initState method of a stateful widget to start observing when the widget is initialized.
Dart
@override
void initState() {
  super.initState();
  observeData();
}
Remember to cancel your observer in your widget’s dispose() method to prevent resource leaks
Dart
@override
void dispose() {
  _observer.cancel();
  super.dispose();
}

Step 5: Syncing Data

To keep your data in sync across devices and with Ditto Server, you need to register sync subscriptions. Sync subscriptions define the specific data that should be automatically synchronized to the device.

Starting Sync

To start sync on the device you need to call startSync. This enables this device to sync data with other peers including the Ditto Cloud.
Dart
ditto.startSync();

Registering a Subscription

Register a subscription to a collection to keep the data in sync. Below is an example of how to register a subscription to the items collection:
Dart
void syncData() {
  ditto.sync.registerSubscription("SELECT * FROM items");
}
Call the syncData function, typically when initializing your Ditto instance.

Syncing Data Offline

Once your application is syncing data using Ditto, you can deploy it to multiple local devices, Android emulators, or iOS simulators. You can then disable the internet to observe the devices syncing offline.

Step 6: Web Browser Support

Web Assets

When Ditto is used on the web, it needs to download and initialize assets for its WebAssembly binary. This happens automatically when calling await Ditto.init(). By default, assets are bundled with the application and loaded from the same web server that serves the application. After running flutter build web, you can find those in the build/web/assets/packages/ditto_live/lib/assets directory. Alternatively, you can host the contents of that directory on a CDN or other web server and configure Ditto to load assets from there. All contents of the directory, excluding the ditto.wasm file itself, must be reachable at the URL given by the wasmShimUrl parameter. The ditto.wasm file can be served from a separate URL given by the wasmUrl parameter. Configuration for loading all assets from a CDN:
await Ditto.init(
  wasmUrl: 'https://your-cdn.com/ditto-assets/ditto.wasm',
  wasmShimUrl: 'https://your-cdn.com/ditto-assets/',
);
Please consider your application’s loading UI while downloading the assets as it may take a few seconds to download and initialize the WebAssembly binary. It is strongly recommended to serve assets using a web server or CDN that supports compression. This will significantly reduce the download size of the assets and thereby the loading time of your application.

Disable Peer to Peer

On the web, Ditto only supports syncing with the Ditto cloud and does not directly connect to other devices. Make sure that your app only enables peer-to-peer transports on mobile platforms.
final ditto = await Ditto.init(
  wasmUrl: 'https://your-cdn.com/ditto-assets/ditto.wasm',
  wasmShimUrl: 'https://your-cdn.com/ditto-assets/',
);

final connect = Connect(webSocketUrls: {"wss://REPLACE_ME_WITH_YOUR_WEBSOCKET_URL"});

// Only enable P2P transports on mobile or desktop
if (!kIsWeb) {
  final peerToPeer = PeerToPeer.all();
}

ditto.transportConfig = TransportConfig(peerToPeer: peerToPeer, connect: connect);
ditto.startSync();

Considerations for Web

  • Flutter for Web is designed to work seamlessly with the Ditto cloud. Due to browser restrictions, direct peer-to-peer synchronization with other devices is not supported.
  • The web platform utilizes an in-memory Ditto store, meaning data is not retained across page reloads.
  • Ditto’s Flutter SDK currently supports the default build mode for the web.
  • When developing with Flutter’s development server, it is required to quit and restart the server after making changes to the source code of your app (support for hot restart is in development).

Step 7: Devtools Extension

The Ditto Flutter SDK has a Devtools extension. This extension provides a DQL editor, which allows you to execute DQL queries against the Ditto instance running in your app. To access it, open Flutter Devtools by pressing v in your terminal after flutter run-ing your app. Then, in the top bar of the devtools window, click on ditto_live (depending on the size of your browser window, it may be in a drop-down menu). The first time you open the Ditto devtools extension, you will need to grant it permission. Once it’s running, it will connect to your app and communicate with the first Ditto instance that your app created. If no Ditto instances have been created, it will not load. The extension has two text fields - one for the query, and one for the arguments. Arguments are parsed as JSON5, which is compatible with JSON, but has some ergonomic improvements, such as:
  • C-style comments with ’//’
  • allowed trailing commas in arrays and objects
  • optional quotes in object keys

Step 8: Working with Hot Restart

Flutter’s hot reload is fully compatible with Ditto and works as expected for UI changes. However, hot restart can cause issues because Ditto maintains persistent state that conflicts with Flutter’s restart mechanism.

Understanding the Issue

When you perform a hot restart in Flutter:
  • The Dart VM resets and reinitializes your app
  • Ditto’s native resources and lockfiles remain in memory/disk
  • Attempting to reinitialize Ditto with the same persistence directory causes conflicts
The simplest solution is to use a different persistence directory for each debug session. This prevents conflicts by ensuring each restart uses a fresh directory:
import 'package:ditto_live/ditto_live.dart';
import 'dart:math';

Future<Ditto> initializeDitto() async {
  // Provide your own logic to decide what sort of build this is. See below for
  // common ways to set this value.
  final isProduction = checkIsProductionBuild();
  
  final persistenceDirectory = switch (isProduction) {
    true => 'ditto',
    false => 'ditto_debug_${Random().nextInt(1 << 32)}',
  };

  final ditto = await Ditto.init(
    identity: yourIdentity,
    persistenceDirectory: persistenceDirectory,
  );

  return ditto;
}
You should consider how you want to decide whether to initialize Ditto with a randomized directory. Common ways to set this value are:
  • --dart-define and const String.fromEnvironment. See this page for more details.
  • Flutter “flavors” - Once a flavor is configured, you can check it in Dart by reading the appFlavor global. See this page for more details.
  • By checking the current build mode - i.e. kDebugMode, kReleaseMode, etc.
This approach means data won’t persist between hot restarts during development. This is typically acceptable for development workflows since you’re focusing on UI/logic changes rather than data persistence. If you need data persistence, you must do a full restart of the app.

Best Practices

  • Use hot reload for UI changes - it’s fully compatible with Ditto
  • Use randomized directories in debug mode for the smoothest development experience
  • Document your approach in your project README so team members understand the development workflow
Hot-reload on the web requires at least Flutter 3.32. In 3.32, it is experimental and requires passing a flag. As of 3.35, it is enabled by default.

Summary

  • Hot reload: Works without issues on all platforms.
  • Hot restart: Requires workarounds on all platforms including web, as Ditto instances cannot be re-initialized in the same process

Step 9: Troubleshooting

  • Ensure that all dependencies are up-to-date by updating to the latest version.
  • Contact the Ditto team through Contact Us