cbl 2.1.0 icon indicating copy to clipboard operation
cbl: ^2.1.0 copied to clipboard

Couchbase Lite is an embedded, NoSQL JSON Document Style database, supporting Blobs, Encryption, N1QL Queries, Live Queries, Full-Text Search and Data Sync.

Version CI codecov

Couchbase Lite is an embedded, NoSQL database:

  • Multi-Platform - Android, iOS, macOS, Windows, Linux
  • Standalone Dart and Flutter - No manual setup required, just add the package.
  • Fast and Compact - Uses efficient persisted data structures.

It is fully featured:

  • JSON Style Documents - No explicit schema and supports deep nesting.
  • Expressive Queries - N1QL (SQL for JSON), QueryBuilder, Full-Text Search
  • Observable - Get notified of changes in database, queries and data sync.
  • Data Sync - Pull and push data from/to server with full control over synced data.

❤️ If you find this package useful, please ⭐ us on pub.dev and GitHub. 🙏

🐛 & ✨ Did you find a bug or have a feature request? Please open a GitHub issue.

👋 Do you you have a question or feedback? Let us know in a GitHub discussion.

Proudly sponsored by #

Lotum


What are all these packages for?

Couchbase Lite can be used with standalone Dart or with Flutter apps and comes in two editions: Community and Enterprise.

Regardless of the app platform and edition of Couchbase Lite you use, you always need the cbl package. All of the APIs of Couchbase Lite live in this package.

What other packages you need depends on the app platform and the edition of Couchbase Lite you use.

PackageRequired when you want to:PubLikesPointsPopularity
cbluse Couchbase Lite.
cbl_dartuse the Community or Enterprise Edition in a standalone Dart app or in Flutter unit tests.
cbl_flutteruse Couchbase Lite in a Flutter app.
cbl_flutter_ceuse the Community Edition in a Flutter app.
cbl_flutter_eeuse the Enterprise Edition in a Flutter app.
cbl_sentryintegrate Couchbase Lite with Sentry in a Dart or Flutter app.
cbl_generatorgenerate Dart code to access data trough a typed data model.

Table of contents #

🤩 Features #

  • Offline first
  • Documents
    • Schemaless
    • Stored in efficient binary format
  • Blobs
    • Store binary data, for example JPGs or PDFs
  • Queries
    • Write queries for JSON data with SQL semantics
    • Construct queries through a type safe builder API
    • Write queries in N1QL
    • Full-Text Search
    • Indexing
  • Data Sync
  • Data Conflict Handling
  • Change observer APIs for:
    • Database
    • Query
    • Replicator
  • Encryption
    • Full database on device *

*: Enterprise Edition only feature

⛔ Limitations #

Some of the features supported by other platform implementation of Couchbase Lite are currently not supported:

  • Predictive Queries
  • Peer-to-Peer Data Sync
  • Background Data Sync on iOS and Android
  • Integration with system-wide configured proxies
  • VPN On Demand on iOS

🔌 Getting started #

To use Couchbase Lite in a

and follow the instructions for getting started.

🔑 Key concepts #

Synchronous and Asynchronous APIs #

The whole Couchbase Lite API comes in both a synchronous and asynchronous version. The synchronous version is more efficient and slightly more convenient to use, but has the downside that it blocks the current isolate.

In UI applications, such as Flutter apps, this is problematic. Blocking the UI isolate for too long causes janky animations, or worse, makes the app unresponsive. With only a synchronous API available, the solution would be to offload the work to a worker isolate. That is what the asynchronous API does in a transparent way.

Unless you are noticing the performance impact of the overhead of the asynchronous API, use the asynchronous API.

To support writing code that works with both synchronous and asynchronous APIs, synchronous and asynchronous APIs always extend from a common base class that uses FutureOr wherever a result could be synchronous or asynchronous.

Take for example this simplified version of the Query API:

abstract class Query {
  // The common base class leaves open whether the results are returned
  // synchronously or asynchronously.
  FutureOr<ResultSet> execute();
}

abstract class SyncQuery extends Query {
  // The synchronous version of `Query` returns results directly.
  ResultSet execute();
}

abstract class AsyncQuery extends Query {
  // The asynchronous version of `Query` returns results in a `Future`.
  Future<ResultSet> execute();
}

FutureOr can be awaited just like a Future, so by programming against Query your code works with both the synchronous and asynchronous API:

/// Runs a query that returns a result set with one row and one column and
/// returns its value.
Future<int> runCountQuery(Query query) {
  final resultSet = await query.execute();
  final results = await resultSet.allResults();
  // Returns the first column of the first row.
  return result[0].integer(0);
}

Change listeners #

Certain objects allow you to register change listeners. In the case of synchronous APIs, all changes are delivered to the listeners as soon as they are registered.

With asynchronous APIs, changes are only guaranteed to be delivered once the Future returned from the registration call is completed:

// Await the future returned from the registration call.
await db.addChangeListener((change) {
  print('Ids of changed documents: ${change.documentIds}'):
});

// The listener is guaranteed to be notified of this change.
await db.saveDocument(MutableDocument.withId('Hey'));

To stop receiving notifications, call removeChangeListener with the token that was returned from the registration call. Regardless of the whether the API is synchronous or asynchronous, listeners will stop receiving notifications immediately:

final token = await db.addChangeListener((change) { });

// Some time goes by...

await db.removeChangeListener(token);

Change streams #

Streams are a convenient alternative to listen for changes. Similarly to change listeners, change streams returned from synchronous APIs are receiving changes as soon as the stream is subscribed to.

Streams returned from asynchronous APIs start to listen asynchronously. But it's not possible to return a Future from Stream.listen to signal the point in time after which the the stream will observe events. Instead, asynchronous APIs return AsyncListenStreams, which expose a Future in AsyncListenStream.listening that completes when the stream is fully listening:

final stream = db.changes();

stream.listen((change) {
  print('Ids of changed documents: ${change.documentIds}'):
});

// Await the Future exposed by the stream.
await stream.listening;

// The stream is guaranteed to be notified of this change.
await db.saveDocument(MutableDocument.withId('Hey'));

If you only ever open the same physical database once at a time, you don't need to await the listening future. In this case the stream will always observe all subsequent events.

To stop listening to changes just cancel the subscription, like with any other stream.

Closing resources #

Some types implement ClosableResource. At the moment these are Database and Replicator. Once you are done with an instance of these types, call its close method. This will free resources used by the object, as well as remove listeners, close streams and close child resources. For example closing a database will also close any associated replicators.

📖 Usage examples #

Open a database #

Every Database has a name which is used to determine its filename. The full filename is the concatenation of the database name and the extension .cblite2.

When opening a database without specifying a directory it will be put into a default location that is platform dependent:

final db = await Database.openAsync('my-database');

If you want to open a database in a specific directory you can specify the directory like this:

final db = await Database.openAsync(
  'my-database',
  DatabaseConfiguration(directory: 'my-directory')
);

If a database with the same name already exists in the directory, it will be opened. Otherwise a new database will be created.

When you are done with the database, you should close it by calling Database.close. This will free up any resources used by the database, as well as remove change listeners, close change streams and close associated replicators.

Create a document #

The default constructor of MutableDocument creates a document with a randomly generated id and optionally initializes it with some properties:

final doc = MutableDocument({
  'name': 'Alice',
  'age': 29,
});

await db.saveDocument(doc);

It's also possible to create a document with a specific id:

final doc = MutableDocument.withId('ali', {
  'name': 'Alice',
  'age': 29,
});

await db.saveDocument(doc);

Read a document #

To read a Document pass the document's id to Database.document:

final doc = await db.document('ali');

// If the document exists, an immutable `Document` is returned.
if (doc != null) {
  print('Name: ${doc.string('name')}');
  print('Age: ${doc.string('age')}');
}

Update a document #

To update a document, first read it, turn it into a MutableDocument and update its properties. Then save it again with Database.saveDocument:

final doc = await db.document('ali');

final mutableDoc = doc!.toMutable();

// You can use one of the typed setters to update the document's properties.
mutableDoc.setArray(MutableArray(['Dart']), key: 'languages');

// Or alternatively, use this subscript syntax to get a [MutableFragment] and
// use it to update the document.
mutableDoc['languages'].array = MutableArray(['Dart']);

// The untyped `setValue` setter does the conversion from a plain Dart collection
// to a document collection (`MutableArray` or `MutableDictionary`) for you.
mutableDoc.setValue(['Dart'], key: 'languages');

// Again, there is an alternative subscript syntax available.
mutableDoc['languages'].value = ['Dart'];


await db.saveDocument(mutableDoc);

Check out the documentation for Database.saveDocument to learn about how conflicts are handled.

Delete a document #

To delete a document, you need to read it first and than pass it to Database.deleteDocument:

final doc = await db.document('ali');

await db.deleteDocument(doc);

Check out the documentation for Database.deleteDocument to learn about how conflicts are handled.

Build a Query with the QueryBuilder API #

A Query can be built in a type safe way through the QueryBuilder API.

The query below returns the average age of people with the same name:

final query = const QueryBuilder()
  .select(
    SelectResult.property('name'),
    SelectResult.expression(
      Function_.avg(Expression.property('age'))
    ).as('avgAge'),
  )
  .from(DataSource.database(db))
  .groupBy(Expression.property('name'));

final resultSet = await query.execute();
final results = await resultSet
  .asStream()
  // Converts each result into a `Map`, consisting only of plain Dart values.
  .map((result) => result.toPlainMap())
  .toList();

print(results);

Given these documents:

[
  {'name': 'Alice', 'age': 29},
  {'name': 'Bob', 'age': 45},
  {'name': 'Alice', 'age': 16},
]

results will be:

[
  {'name': 'Alice', 'avgAge': 22.5},
  {'name': 'Bob', 'avgAge': 45},
]

Build a Query with N1QL #

N1QL is a query language that is similar to SQL but for JSON style data.

The query below is equivalent to query from the QueryBuilder example above:

final query = await Query.fromN1ql(
  db,
  '''
  SELECT name, avg(age) AS avgAge
  FROM _
  GROUP BY name
  ''',
);

Data sync with Replicator to Sync Gateway #

This example synchronizes the database with a remote Sync Gateway instance, without authentication. This only works when Sync Gateway has been configured with the GUEST user.

A ReplicatorConfiguration with only default values creates a Replicator with type ReplicatorType.pushAndPull that is not continuous.

After starting this replicator, it will push changes from the local database to the remote database and pull changes from the remote database to the local database and then stop again.

Both Replicator.start and Replicator.stop don't immediately start/stop the replicator. The current status of the replicator is available in Replicator.status.activity.

final replicator = await Replicator.create(ReplicatorConfiguration(
  database: db,
  target: UrlEndpoint('http://localhost:4984/my-database'),
));

await replicator.addChangeListener((change) {
    print('Replicator activity: ${change.status.activity}');
});

await replicator.start();

When you are done with the replicator, you should close it by calling Replicator.close. This will free up any resources used by the replicator, as well as remove change listeners and close change streams.

🔮 Tracing #

The execution of certain operations can be traced through the tracing API. This is useful for debugging and performance profiling.

CBL Dart has builtin trace points at which flow control is given to the currently installed TracingDelegate.

Included in this package is the DevToolsTracing delegate, which records timeline events that can be later visualized through the Dart DevTools Performance Page.

You can install a delegate by calling TracingDelegate.install:

await TracingDelegate.install(DevToolsTracing());

The Sentry integration provided by cbl_sentry installs a TracingDelegate to transparently record breadcrumbs and transaction spans.

🗂️ Typed Data #

⚠️ The typed data API is experimental and might be missing some feature that you need. Please file an issue if you find a bug or have a feature request.

cbl allows dynamic access to data without a fixed data model, not requiring any code generation. This is useful when the data is very dynamic or code generation is undesirable.

Often though, the data is known to have a regular structure, and accessing it through a typed Dart API makes working with it easier and safer.

With the help of the cbl_generator package you can quickly create Dart classes that can be used to access data in a typed way. Theses classes can be used with specialized APIs of Database, Query and Replicator.

Getting started #

  1. Setup the cbl_generator package.

  2. Create typed data classes and annotated them with @TypedDocument and @TypedDictionary:

    // user.dart
    
    import 'package:cbl/cbl.dart';
    
    // Declare the part file into which the generated code will be written.
    part 'user.cbl.type.g.dart';
    
    // Per default the type of a document is encoded in the `type` property in
    // the underlying data. The value is a string that is the name of the annotated
    // class. This can be customized by setting `TypedDocument.typeMatcher`.
    @TypedDocument()
    abstract class User with _$User {
      factory User({
        @DocumentId() String? id,
        required PersonalName name,
        String? email,
        required String username,
        required DateTime createdAt,
      }) = MutableUser;
    }
    
    @TypedDictionary()
    abstract class PersonalName with _$PersonalName {
      factory PersonalName({
        required String first,
        required String last,
      }) = MutablePersonalName;
    }
    
  3. Create a typed database by annotating a class with @TypedDatabase:

    // app_database.dart
    
    import 'package:cbl/cbl.dart';
    
    import 'user.dart';
    
    @TypedDatabase(
      // List all the typed data classes that will be used in the database.
      types: {
        User,
        PersonalName,
      },
    )
    abstract class $AppDatabase {}
    
  4. Open an instance of the typed database and use it:

    import 'app_database.cbl.database.g.dart';
    import 'user.dart';
    
    Future<void> useTypedDatabase() {
      // Use a static method on the generated typed database class to open an instance.
      final db = await AppDatabase.openAsync('app');
    
      // Every typed data class has a mutable and immutable variant. The mutable
      // class has the same name as the immutable class, but with the `Mutable`
      // suffix. A mutable instance can be created by constructing it, or from
      // an immutable instance, through the `toMutable` method.
      final user = MutableUser(
        name: PersonalName(first: 'Alice', last: 'Green'),
        email: 'alice@belden.com',
        username: 'ali',
        createdAt: DateTime.now(),
      );
    
      // The API to save typed documents is slightly different than the API to
      // save plain documents. `saveTypedDocument` returns an object that has methods
      // for saving the document with conflict resolution through concurrency control or
      // a custom conflict handler.
      await db.saveTypedDocument(user).withConcurrencyControl();
    
      // To retrieve a typed document, use the `typedDocument` method and pass it the
      // type of the requested document through the type parameter.
      final savedUser = await db.typedDocument<User>(user.id);
    }
    

💡 Where to go next #

🤝 Contributing #

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please make sure to update tests as appropriate.

Read CONTRIBUTING to get started developing.

Prior work #

Thanks to the authors of earlier Couchbase Lite packages. Those packages where valuable references for making decisions about how to approach this project.

⚖️ Disclaimer #

⚠️ This is not an official Couchbase product.

44
likes
140
pub points
82%
popularity

Publisher

verified publisher icongabriel.terwesten.net

Couchbase Lite is an embedded, NoSQL JSON Document Style database, supporting Blobs, Encryption, N1QL Queries, Live Queries, Full-Text Search and Data Sync.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

Icon for licenses.Apache-2.0 (LICENSE)

Dependencies

cbl_ffi, characters, collection, ffi, meta, stream_channel, synchronized, web_socket_channel

More

Packages that depend on cbl