Couchbase Lite is an embedded, NoSQL database:
It is fully featured:
❤️ 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.
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();
}
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.
⚠️ This is not an official Couchbase product.
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.
Now in your Dart code, you can use:
import 'package:cbl_flutter/cbl_flutter.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