Flutter Plugin for Couchbase Lite, an Embedded, NoSQL JSON Document Style Database

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 - SQL++ (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 


This package allows you to use Couchbase Lite in a Flutter app.

To get started, go to the documentation.

Below is a sneak peak of what it's like to use Couchbase Lite.

import 'package:cbl/cbl.dart';

Future<void> run() async {
  // Open the database (creating it if it doesn’t exist).
  final database = await Database.openAsync('my-database');

  // Create a new document.
  final mutableDocument = MutableDocument({'type': 'SDK', 'majorVersion': 2});
  await database.saveDocument(mutableDocument);

  print(
    'Created document with id ${mutableDocument.id} and '
    'type ${mutableDocument.string('type')}.',
  );

  // Update the document.
  mutableDocument.setString('Dart', key: 'language');
  await database.saveDocument(mutableDocument);

  print(
    'Updated document with id ${mutableDocument.id}, '
    'adding language ${mutableDocument.string("language")!}.',
  );

  // Read the document.
  final document = (await database.document(mutableDocument.id))!;

  print(
    'Read document with id ${document.id}, '
    'type ${document.string('type')} and '
    'language ${document.string('language')}.',
  );

  // Create a query to fetch documents of type SDK.
  print('Querying Documents of type=SDK.');
  final query = await Query.fromN1ql(database, '''
    SELECT * FROM _
    WHERE type = 'SDK'
  ''');

  // Run the query.
  final result = await query.execute();
  final results = await result.allResults();
  print('Number of results: ${results.length}');

  // Close the database.
  await database.close();
}

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

Use this package as a library

Depend on it

Run this command:

With Flutter:

 $ flutter pub add cbl_flutter

This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):

dependencies:
  cbl_flutter: ^2.0.9

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:cbl_flutter/cbl_flutter.dart';

example/lib/main.dart

// ignore_for_file: avoid_print, lines_longer_than_80_chars, diagnostic_describe_all_properties

/// This simple example app allows users to view and add to a list of log
/// messages.
///
/// It demonstrates how to:
///
/// - initialize Couchbase Lite,
/// - open a database,
/// - create an index,
/// - save a new document,
/// - build a query through the query builder,
/// - explain how that query is executed,
/// - and watch that query.
library main;

import 'dart:async';

import 'package:cbl/cbl.dart';
import 'package:cbl_flutter/cbl_flutter.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

Future<void> main() async {
  await initApp();
  runApp(const MyApp());
}

// === UI ======================================================================

const spacing = 16.0;

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => const MaterialApp(
        debugShowCheckedModeBanner: false,
        home: LogMessagesPage(),
      );
}

class LogMessagesPage extends StatefulWidget {
  const LogMessagesPage({super.key});

  @override
  State<LogMessagesPage> createState() => _LogMessagesPageState();
}

class _LogMessagesPageState extends State<LogMessagesPage> {
  List<LogMessage> _logMessages = [];
  late StreamSubscription _logMessagesSub;

  @override
  void initState() {
    super.initState();
    _logMessagesSub =
        logMessageRepository.allLogMessagesStream().listen((logMessages) {
      setState(() => _logMessages = logMessages);
    });
  }

  @override
  void dispose() {
    _logMessagesSub.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        body: SafeArea(
          child: Column(children: [
            Expanded(
              child: ListView.builder(
                // By reversing the ListView new messages will be immediately
                // visible and when opening the page the ListView starts out
                // scrolled all the way to the bottom.
                reverse: true,
                itemCount: _logMessages.length,
                itemBuilder: (context, index) {
                  // Since the ListView is reversed, we need to access the
                  // log messages in reverse, to end up with the correct
                  // ordering.
                  final logMessage =
                      _logMessages[_logMessages.length - 1 - index];

                  return LogMessageTile(logMessage: logMessage);
                },
              ),
            ),
            const Divider(height: 0),
            _LogMessageForm(onSubmit: logMessageRepository.createLogMessage)
          ]),
        ),
      );
}

class LogMessageTile extends StatelessWidget {
  const LogMessageTile({super.key, required this.logMessage});

  final LogMessage logMessage;

  @override
  Widget build(BuildContext context) => Padding(
        padding: const EdgeInsets.all(spacing),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              DateFormat.yMd().add_jm().format(logMessage.createdAt),
              style: Theme.of(context).textTheme.bodySmall,
            ),
            const SizedBox(height: spacing / 4),
            Text(logMessage.message)
          ],
        ),
      );
}

class _LogMessageForm extends StatefulWidget {
  const _LogMessageForm({required this.onSubmit});

  final ValueChanged<String> onSubmit;

  @override
  _LogMessageFormState createState() => _LogMessageFormState();
}

class _LogMessageFormState extends State<_LogMessageForm> {
  late final TextEditingController _messageController;
  late final FocusNode _messageFocusNode;

  @override
  void initState() {
    super.initState();
    _messageController = TextEditingController();
    _messageFocusNode = FocusNode();
  }

  @override
  void dispose() {
    _messageController.dispose();
    _messageFocusNode.dispose();
    super.dispose();
  }

  void _onSubmit() {
    final message = _messageController.text.trim();
    if (message.isEmpty) {
      return;
    }

    widget.onSubmit(message);
    _messageController.clear();
    _messageFocusNode.requestFocus();
  }

  @override
  Widget build(BuildContext context) => Padding(
        padding: const EdgeInsets.all(spacing),
        child: Row(
          children: [
            Expanded(
              child: TextField(
                decoration:
                    const InputDecoration.collapsed(hintText: 'Message'),
                autofocus: true,
                focusNode: _messageFocusNode,
                controller: _messageController,
                minLines: 1,
                maxLines: 10,
                style: Theme.of(context).textTheme.bodyMedium,
                textCapitalization: TextCapitalization.sentences,
              ),
            ),
            const SizedBox(width: spacing / 2),
            TextButton(
              onPressed: _onSubmit,
              child: const Text('Write to log'),
            )
          ],
        ),
      );
}

// === Log Message Storage =====================================================

/// The model of a log message.
abstract class LogMessage {
  String get id;
  DateTime get createdAt;
  String get message;
}

/// Implementation of a [LogMessage] that wraps a [DictionaryInterface], which
/// contains a log message.
///
/// [DictionaryInterface] is implemented by a few types in `cbl`:
///
/// - [Document],
/// - [Dictionary],
/// - [Result].
///
/// Accessing data through objects returned from the `cbl` API is more efficient
/// than converting them to plain Dart objects (e.g.
/// [DictionaryInterface.toPlainMap]). These objects pull only the data that is
/// accessed, out of the binary encoded, stored data.
class CblLogMessage extends LogMessage {
  CblLogMessage(this.dict);

  final DictionaryInterface dict;

  // `dict` could be a `Document` or a `Result` from a query.
  // The extension `DictionaryDocumentIdExt` handles getting the id in both
  // cases.
  @override
  String get id => dict.documentId;

  @override
  DateTime get createdAt => dict.value('createdAt')!;

  @override
  String get message => dict.value('message')!;
}

extension DictionaryDocumentIdExt on DictionaryInterface {
  /// If this is a [Document], returns its `id`, otherwise returns the [String]
  /// in the field `id`.
  String get documentId {
    final self = this;
    return self is Document ? self.id : self.value('id')!;
  }
}

/// Repository for [LogMessage]s, which abstracts data storage access.
class LogMessageRepository {
  LogMessageRepository(this.database);

  Database database;

  Future<LogMessage> createLogMessage(String message) async {
    final doc = MutableDocument({
      'type': 'logMessage',
      'createdAt': DateTime.now(),
      'message': message,
    });
    await database.saveDocument(doc);
    return CblLogMessage(doc);
  }

  Stream<List<LogMessage>> allLogMessagesStream() {
    final query = const QueryBuilder()
        .select(
          SelectResult.expression(Meta.id),
          SelectResult.property('createdAt'),
          SelectResult.property('message'),
        )
        .from(DataSource.database(database))
        .where(
          Expression.property('type').equalTo(Expression.value('logMessage')),
        )
        .orderBy(Ordering.property('createdAt'));

    // The line below prints an explanation of how the query will be executed.
    // This explanation should contain this line, which tells us that the query
    // uses the index we created in the `initApp` function:
    // 4|0|0| SEARCH TABLE kv_default AS example USING INDEX type+createdAt (<expr>=?)
    Future(query.explain).then(print);

    return query.changes().asyncMap(
          (change) => change.results.asStream().map(CblLogMessage.new).toList(),
        );
  }
}

// === App Setup ===============================================================

late Database database;
late LogMessageRepository logMessageRepository;

/// Initializes global app state.
Future<void> initApp() async {
  WidgetsFlutterBinding.ensureInitialized();

  await TracingDelegate.install(DevToolsTracing());

  await CouchbaseLiteFlutter.init();

  // Uncomment the line below to reset the database each time the app starts.
  // await Database.remove('example');

  database = await Database.openAsync('example');

  // This index speeds up queries, among others, that filter documents by an
  // exact `type` and sort by `createdAt`.
  await database.createIndex(
    'type+createdAt',
    ValueIndex([
      ValueIndexItem.property('type'),
      ValueIndexItem.property('createdAt'),
    ]),
  );

  logMessageRepository = LogMessageRepository(database);
}example/lib/main.dart
// ignore_for_file: avoid_print, lines_longer_than_80_chars, diagnostic_describe_all_properties

/// This simple example app allows users to view and add to a list of log
/// messages.
///
/// It demonstrates how to:
///
/// - initialize Couchbase Lite,
/// - open a database,
/// - create an index,
/// - save a new document,
/// - build a query through the query builder,
/// - explain how that query is executed,
/// - and watch that query.
library main;

import 'dart:async';

import 'package:cbl/cbl.dart';
import 'package:cbl_flutter/cbl_flutter.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

Future<void> main() async {
  await initApp();
  runApp(const MyApp());
}

// === UI ======================================================================

const spacing = 16.0;

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => const MaterialApp(
        debugShowCheckedModeBanner: false,
        home: LogMessagesPage(),
      );
}

class LogMessagesPage extends StatefulWidget {
  const LogMessagesPage({super.key});

  @override
  State<LogMessagesPage> createState() => _LogMessagesPageState();
}

class _LogMessagesPageState extends State<LogMessagesPage> {
  List<LogMessage> _logMessages = [];
  late StreamSubscription _logMessagesSub;

  @override
  void initState() {
    super.initState();
    _logMessagesSub =
        logMessageRepository.allLogMessagesStream().listen((logMessages) {
      setState(() => _logMessages = logMessages);
    });
  }

  @override
  void dispose() {
    _logMessagesSub.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        body: SafeArea(
          child: Column(children: [
            Expanded(
              child: ListView.builder(
                // By reversing the ListView new messages will be immediately
                // visible and when opening the page the ListView starts out
                // scrolled all the way to the bottom.
                reverse: true,
                itemCount: _logMessages.length,
                itemBuilder: (context, index) {
                  // Since the ListView is reversed, we need to access the
                  // log messages in reverse, to end up with the correct
                  // ordering.
                  final logMessage =
                      _logMessages[_logMessages.length - 1 - index];

                  return LogMessageTile(logMessage: logMessage);
                },
              ),
            ),
            const Divider(height: 0),
            _LogMessageForm(onSubmit: logMessageRepository.createLogMessage)
          ]),
        ),
      );
}

class LogMessageTile extends StatelessWidget {
  const LogMessageTile({super.key, required this.logMessage});

  final LogMessage logMessage;

  @override
  Widget build(BuildContext context) => Padding(
        padding: const EdgeInsets.all(spacing),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              DateFormat.yMd().add_jm().format(logMessage.createdAt),
              style: Theme.of(context).textTheme.bodySmall,
            ),
            const SizedBox(height: spacing / 4),
            Text(logMessage.message)
          ],
        ),
      );
}

class _LogMessageForm extends StatefulWidget {
  const _LogMessageForm({required this.onSubmit});

  final ValueChanged<String> onSubmit;

  @override
  _LogMessageFormState createState() => _LogMessageFormState();
}

class _LogMessageFormState extends State<_LogMessageForm> {
  late final TextEditingController _messageController;
  late final FocusNode _messageFocusNode;

  @override
  void initState() {
    super.initState();
    _messageController = TextEditingController();
    _messageFocusNode = FocusNode();
  }

  @override
  void dispose() {
    _messageController.dispose();
    _messageFocusNode.dispose();
    super.dispose();
  }

  void _onSubmit() {
    final message = _messageController.text.trim();
    if (message.isEmpty) {
      return;
    }

    widget.onSubmit(message);
    _messageController.clear();
    _messageFocusNode.requestFocus();
  }

  @override
  Widget build(BuildContext context) => Padding(
        padding: const EdgeInsets.all(spacing),
        child: Row(
          children: [
            Expanded(
              child: TextField(
                decoration:
                    const InputDecoration.collapsed(hintText: 'Message'),
                autofocus: true,
                focusNode: _messageFocusNode,
                controller: _messageController,
                minLines: 1,
                maxLines: 10,
                style: Theme.of(context).textTheme.bodyMedium,
                textCapitalization: TextCapitalization.sentences,
              ),
            ),
            const SizedBox(width: spacing / 2),
            TextButton(
              onPressed: _onSubmit,
              child: const Text('Write to log'),
            )
          ],
        ),
      );
}

// === Log Message Storage =====================================================

/// The model of a log message.
abstract class LogMessage {
  String get id;
  DateTime get createdAt;
  String get message;
}

/// Implementation of a [LogMessage] that wraps a [DictionaryInterface], which
/// contains a log message.
///
/// [DictionaryInterface] is implemented by a few types in `cbl`:
///
/// - [Document],
/// - [Dictionary],
/// - [Result].
///
/// Accessing data through objects returned from the `cbl` API is more efficient
/// than converting them to plain Dart objects (e.g.
/// [DictionaryInterface.toPlainMap]). These objects pull only the data that is
/// accessed, out of the binary encoded, stored data.
class CblLogMessage extends LogMessage {
  CblLogMessage(this.dict);

  final DictionaryInterface dict;

  // `dict` could be a `Document` or a `Result` from a query.
  // The extension `DictionaryDocumentIdExt` handles getting the id in both
  // cases.
  @override
  String get id => dict.documentId;

  @override
  DateTime get createdAt => dict.value('createdAt')!;

  @override
  String get message => dict.value('message')!;
}

extension DictionaryDocumentIdExt on DictionaryInterface {
  /// If this is a [Document], returns its `id`, otherwise returns the [String]
  /// in the field `id`.
  String get documentId {
    final self = this;
    return self is Document ? self.id : self.value('id')!;
  }
}

/// Repository for [LogMessage]s, which abstracts data storage access.
class LogMessageRepository {
  LogMessageRepository(this.database);

  Database database;

  Future<LogMessage> createLogMessage(String message) async {
    final doc = MutableDocument({
      'type': 'logMessage',
      'createdAt': DateTime.now(),
      'message': message,
    });
    await database.saveDocument(doc);
    return CblLogMessage(doc);
  }

  Stream<List<LogMessage>> allLogMessagesStream() {
    final query = const QueryBuilder()
        .select(
          SelectResult.expression(Meta.id),
          SelectResult.property('createdAt'),
          SelectResult.property('message'),
        )
        .from(DataSource.database(database))
        .where(
          Expression.property('type').equalTo(Expression.value('logMessage')),
        )
        .orderBy(Ordering.property('createdAt'));

    // The line below prints an explanation of how the query will be executed.
    // This explanation should contain this line, which tells us that the query
    // uses the index we created in the `initApp` function:
    // 4|0|0| SEARCH TABLE kv_default AS example USING INDEX type+createdAt (<expr>=?)
    Future(query.explain).then(print);

    return query.changes().asyncMap(
          (change) => change.results.asStream().map(CblLogMessage.new).toList(),
        );
  }
}

// === App Setup ===============================================================

late Database database;
late LogMessageRepository logMessageRepository;

/// Initializes global app state.
Future<void> initApp() async {
  WidgetsFlutterBinding.ensureInitialized();

  await TracingDelegate.install(DevToolsTracing());

  await CouchbaseLiteFlutter.init();

  // Uncomment the line below to reset the database each time the app starts.
  // await Database.remove('example');

  database = await Database.openAsync('example');

  // This index speeds up queries, among others, that filter documents by an
  // exact `type` and sort by `createdAt`.
  await database.createIndex(
    'type+createdAt',
    ValueIndex([
      ValueIndexItem.property('type'),
      ValueIndexItem.property('createdAt'),
    ]),
  );

  logMessageRepository = LogMessageRepository(database);
}

Download details:

Author: cbl-dart.dev

Source: https://github.com/cbl-dart/cbl-dart

#flutter #android #ios

Flutter Plugin for Couchbase Lite, an Embedded, NoSQL JSON Document Style Database
1.60 GEEK