cbl 1.0.0-beta.9 copy "cbl: ^1.0.0-beta.9" to clipboard
cbl: ^1.0.0-beta.9 copied to clipboard

outdated

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

Version License CI codecov

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

It can be used as a standalone embedded database for mobile or desktop apps. Or it can be combined with Sync Gateway to synchronize with a central data store.

This package provides a Dart API through wich Couchbase Lite can be used on all native platforms which are supported by Dart.

This package is in beta. Use it with caution and report any issues you see.

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 for Flutter #

Head over to the Flutter plugin for Couchbase Lite (cbl_flutter) to get started using Couchbase Lite in your Flutter app.

🔑 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 main thread.

In UI applications, such as Flutter apps, this is problematic. Blocking the UI thread for too long causes janky animations, or worse unresponsiveness. 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 result is returned
  // synchronously or asynchronously.
  FutureOr<ResultSet> execute();
}

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

abstract class AsyncQuery extends Query {
  // The asynchronous version 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 to changes. Similarly to change listeners, change streams returned from synchronous APIs are receiving changes as soon as the stream is subscribed to.

For streams returned from asynchronous APIs, it's not possible to return a Future from Stream.listen. 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'));

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 QueryBuilder #

This query 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 #

This is the equivalent N1QL query to the one above:

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

Data sync with 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.

The default is to create a replicator with type ReplicatorType.pushAndPull and which 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.

💡 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.

⚖️ Disclaimer #

⚠️ This is not an official Couchbase product.

65
likes
0
pub points
88%
popularity

Publisher

verified publishercbl-dart.dev

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

License

unknown (LICENSE)

Dependencies

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

More

Packages that depend on cbl