Skip to main content

Flutter SDK

Complete guide to using Cocobase with Flutter and Dart applications.

Installation

Add Cocobase to your pubspec.yaml:
dependencies:
  cocobase_flutter: ^latest_version
Install dependencies:
flutter pub get

Quick Start

Initialize Cocobase

import 'package:cocobase_flutter/cocobase_flutter.dart';

void main() {
  final config = CocobaseConfig(
    apiKey: "YOUR_API_KEY",
    baseUrl: "https://api.cocobase.buzz",  // Optional, defaults to this
  );
  final db = Cocobase(config);

  runApp(MyApp());
}

Basic Operations

// List documents
final docs = await db.listDocuments("posts");

// Get single document
final doc = await db.getDocument("posts", "doc-id");

// Create document
final created = await db.createDocument("posts", {
  "title": "My First Post",
  "content": "Hello World!"
});

// Update document
await db.updateDocument("posts", "doc-id", {
  "title": "Updated Title"
});

// Delete document
await db.deleteDocument("posts", "doc-id");

Querying Data

CocoBase Flutter provides two powerful ways to query your data: Filter Map (simple) and QueryBuilder (advanced). Pass filters as a simple Map<String, dynamic>:
// Find active users older than 18
final users = await db.listDocuments("users", filters: {
  'status': 'active',
  'age__gt': 18,  // __gt = greater than
});
Why Filter Map?
  • ✅ Simple and readable
  • ✅ No need to learn QueryBuilder syntax
  • ✅ Works with all operators
  • ✅ Perfect for beginners

Method 2: QueryBuilder (For Complex Queries)

For more complex queries, use the fluent QueryBuilder:
final users = await db.listDocuments("users",
  queryBuilder: QueryBuilder()
    .where('status', 'active')
    .whereGreaterThan('age', 18)
    .whereContains('email', '@gmail.com')
    .orderByDesc('createdAt')
    .limit(10),
);
Why QueryBuilder?
  • 🔗 Chainable methods - Build queries step by step
  • 📝 Self-documenting - Method names are clear (whereGreaterThan, not __gt)
  • 🔍 IDE support - Get autocomplete and type hints
  • 🎯 Complex queries - Combine multiple conditions easily

Operators Reference

Comparison Operators

OperatorFilter MapQueryBuilder MethodExample
Equalfield: value.where(field, value)'age': 25
Greater Thanfield__gt.whereGreaterThan(field, value)'age__gt': 18
Greater or Equalfield__gte.whereGreaterThanOrEqual(field, value)'age__gte': 18
Less Thanfield__lt.whereLessThan(field, value)'age__lt': 65
Less or Equalfield__lte.whereLessThanOrEqual(field, value)'age__lte': 65
Not Equalfield__ne.whereNotEqual(field, value)'status__ne': 'deleted'

String Operators

OperatorFilter MapQueryBuilder MethodExample
Containsfield__contains.whereContains(field, value)'title__contains': 'flutter'
Starts Withfield__startswith.whereStartsWith(field, value)'email__startswith': 'admin'
Ends Withfield__endswith.whereEndsWith(field, value)'domain__endswith': '.com'

Array/List Operators

OperatorFilter MapQueryBuilder MethodExample
In Arrayfield__in.whereIn(field, values)'status__in': 'active,pending'
Not In Arrayfield__notin.whereNotIn(field, values)'status__notin': 'deleted,archived'

Special Operators

OperatorFilter MapQueryBuilder MethodExample
Is Nullfield__isnull.whereIsNull(field, bool)'deletedAt__isnull': true

OR Queries

CocoBase supports three types of OR queries for different needs.

Type 1: Simple OR Conditions

Use when you want “field1 = value1 OR field2 = value2”:
// Find users with admin role OR with email verified
final query = QueryBuilder()
  .or('role', 'admin')
  .or('emailVerified', true);

final users = await db.listDocuments("users", queryBuilder: query);
Use when you want to search across multiple fields:
// Search for "john" in name, email, or phone
final query = QueryBuilder()
  .searchInFields(['name', 'email', 'phone'], 'john');

final users = await db.listDocuments("users", queryBuilder: query);

Type 3: Named OR Groups

Use when you want to group OR conditions:
// Users where: (role=admin OR role=moderator) AND status=active
final query = QueryBuilder()
  .orGroup('roleGroup', 'role', 'admin')
  .orGroup('roleGroup', 'role', 'moderator')
  .where('status', 'active');

final users = await db.listDocuments("users", queryBuilder: query);

Sorting and Pagination

Sorting

Using QueryBuilder:
// Sort by creation date (newest first)
final query = QueryBuilder()
  .orderByDesc('createdAt');

// Sort by price (lowest first)
final query = QueryBuilder()
  .orderByAsc('price');

// Or use sortBy with explicit order
final query = QueryBuilder()
  .sortBy('name', 'asc');
Using Filter Map:
final books = await db.listDocuments("books", filters: {
  'orderBy': 'title',  // Sort field
  'order': 'asc',      // Sort direction
});

Pagination

Using QueryBuilder:
// Get the first 10 documents
final query = QueryBuilder()
  .limit(10);

// Skip first 20, get next 10 (page 3)
final query = QueryBuilder()
  .limit(10)
  .offset(20);

// Aliases available
queryBuilder.take(10);   // same as limit(10)
queryBuilder.skip(20);   // same as offset(20)
Using Filter Map:
final books = await db.listDocuments("books", filters: {
  'limit': 10,
  'offset': 20,
});

Field Selection and Population

Select Specific Fields

Include only certain fields in the response:
// Only get title and price, not full document
final query = QueryBuilder()
  .select('title')
  .select('price');

// Or select multiple at once
final query = QueryBuilder()
  .selectAll(['title', 'price', 'author']);

final books = await db.listDocuments("books", queryBuilder: query);

Population (Relationships)

Load related documents automatically:
// Single relationship
final query = QueryBuilder()
  .populate('author');  // Load author details

// Multiple relationships
final query = QueryBuilder()
  .populateAll(['author', 'publisher']);

final books = await db.listDocuments("books", queryBuilder: query);

Type Conversion & Type Safety

Learn how to work with strongly-typed documents and eliminate null safety issues.

Why Type Safety?

Without Type Safety (Dynamic):
final docs = await db.listDocuments("books");  // Returns dynamic data

// ❌ No autocomplete - what fields exist?
print(docs[0].data['title']);  // Might be null, no type checking

// ❌ Easy to make mistakes
print(docs[0].data['titulo']);  // Typo - no error at compile time

// ❌ Manual type casting required
final price = (docs[0].data['price'] as double) * 2;
With Type Safety (Converted):
final books = await db.listDocuments<Book>("books");

// ✅ Full autocomplete - IDE knows all fields
print(books[0].data.title);  // Perfect!

// ✅ Compile-time type checking
// books[0].data.titulo;  // ❌ ERROR: no property 'titulo'

// ✅ No casting needed
final price = books[0].data.price * 2;  // Dart knows it's double

Creating Models

Basic Model:
class Book {
  final String title;
  final String author;
  final double price;

  Book({
    required this.title,
    required this.author,
    required this.price,
  });

  // Create from JSON (required for type conversion)
  factory Book.fromJson(Map<String, dynamic> json) {
    return Book(
      title: json['title'] as String,
      author: json['author'] as String,
      price: (json['price'] as num).toDouble(),
    );
  }

  // Convert back to JSON (required for createDocument)
  Map<String, dynamic> toJson() {
    return {
      'title': title,
      'author': author,
      'price': price,
    };
  }
}
Model with Optional Fields:
class Product {
  final String name;
  final double price;
  final String? description;  // Optional
  final List<String>? tags;   // Optional list

  Product({
    required this.name,
    required this.price,
    this.description,
    this.tags,
  });

  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      name: json['name'] as String,
      price: (json['price'] as num).toDouble(),
      description: json['description'] as String?,
      tags: (json['tags'] as List<dynamic>?)?.cast<String>(),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'price': price,
      'description': description,
      'tags': tags,
    };
  }
}

Registration Methods

Method 1: Global Registration (Recommended) Register converters once in your app initialization:
void main() async {
  final config = CocobaseConfig(apiKey: "YOUR_KEY");
  final db = Cocobase(config);

  // Register all your converters here
  CocobaseConverters.register<Book>(Book.fromJson);
  CocobaseConverters.register<User>(User.fromJson);
  CocobaseConverters.register<Product>(Product.fromJson);

  runApp(const MyApp());
}
Advantages:
  • ✅ Register once, use everywhere
  • ✅ Cleaner API calls
  • ✅ Best for production code
Method 2: Explicit Converter (Alternative) Pass converter directly to method:
// Use when you need one-off conversions
final books = await db.listDocuments<Book>(
  "books",
  converter: Book.fromJson,  // Explicit parameter
);
Method 3: Check Before Registering
if (!CocobaseConverters.hasConverter<Book>()) {
  CocobaseConverters.register<Book>(Book.fromJson);
}

// Safe to use
final books = await db.listDocuments<Book>("books");

Using Converted Documents

List of Typed Documents:
final books = await db.listDocuments<Book>("books");

// books is List<Document<Book>>
// Each doc.data is a Book instance

for (var doc in books) {
  print('ID: ${doc.id}');
  print('Title: ${doc.data.title}');  // Type-safe!
  print('Price: \$${doc.data.price}');
  print('Created: ${doc.createdAt}');
}
Single Typed Document:
final doc = await db.getDocument<Book>("books", "doc-id");

// doc is Document<Book>
// doc.data is a Book instance

print(doc.data.title);
print(doc.data.author);
With Query Filters:
// Type-safe querying
final books = await db.listDocuments<Book>("books",
  queryBuilder: QueryBuilder()
    .where('status', 'published')
    .whereGreaterThan('price', 10)
    .populate('author')
    .orderByDesc('publishedAt')
    .limit(20),
);

// Process with full type safety
for (var book in books) {
  print('${book.data.title} - \$${book.data.price}');
}

Creating Documents with Type Safety

When you create new documents, your model class must have a toJson() method:
// Create an instance of your model
final newBook = Book(
  title: 'Clean Code',
  author: 'Robert Martin',
  price: 45.99,
);

// Pass it to createDocument - toJson() is called automatically
final created = await db.createDocument<Book>("books", newBook);

// Result contains the new document ID from the server
print('Created with ID: ${created.id}');
print('Title: ${created.data.title}');
How it works:
  1. You create a Book instance with your data
  2. createDocument<Book>() calls toJson() automatically to serialize it
  3. The API receives the JSON and stores it
  4. The response is converted back to a Book instance using fromJson()
  5. You get a Document<Book> with the server-assigned ID

Batch Operations

// Create multiple typed documents
final newBooks = [
  Book(title: 'Book 1', author: 'Author 1', price: 19.99),
  Book(title: 'Book 2', author: 'Author 2', price: 24.99),
];

final results = await db.batchCreateDocuments<Book>("books", newBooks);

for (var result in results.documents) {
  print('Created: ${result.data.title} (${result.id})');
}

Advanced Query Examples

Example 1: Search with Filters

Find published books by specific authors:
final books = await db.listDocuments<Book>("books",
  queryBuilder: QueryBuilder()
    .where('status', 'published')
    .searchInFields(['title', 'description'], 'flutter')
    .whereIn('authorId', ['auth1', 'auth2', 'auth3'])
    .orderByDesc('publishedAt')
    .limit(20),
);

Example 2: Price Range Query

Find products in a price range:
final products = await db.listDocuments<Product>("products",
  queryBuilder: QueryBuilder()
    .whereGreaterThanOrEqual('price', 10)
    .whereLessThanOrEqual('price', 100)
    .populate('category'),
);

Example 3: Complex OR Logic

Find premium users (verified OR have payment method):
final query = QueryBuilder()
  .orGroup('premium', 'emailVerified', true)
  .orGroup('premium', 'paymentMethodId__isnull', false)
  .where('status', 'active');

final users = await db.listDocuments<User>("users", queryBuilder: query);

Example 4: Using Filter Map for Complex Query

final orders = await db.listDocuments<Order>("orders", filters: {
  'status': 'completed',
  'totalAmount__gte': 100,
  'createdAt__gte': '2024-01-01',
  'orderBy': 'createdAt',
  'order': 'desc',
  'limit': 20,
  'offset': 0,
});

Authentication

Email/Password

// Register (returns null)
await db.auth.register(
  email: "[email protected]",
  password: "securePassword123",
);

// Get the authenticated user after registration
final user = await db.auth.getUser();
print('User ID: ${user.id}');
print('Email: ${user.email}');

// Login (returns null)
await db.auth.login(
  email: "[email protected]",
  password: "securePassword123",
);

// Get the authenticated user after login
final user = await db.auth.getUser();

// Logout
await db.auth.logout();

// Check current user (synchronous)
final currentUser = db.auth.currentUser;
Important: The register() and login() methods return null. After successful authentication, use getUser() to retrieve the authenticated user details.

Complete Authentication Example

// Registration flow
try {
  await db.auth.register(
    email: "[email protected]",
    password: "securePassword123",
  );

  // Get user details after registration
  final user = await db.auth.getUser();
  if (user != null) {
    print('Registered successfully! User ID: ${user.id}');
  }
} catch (e) {
  print('Registration failed: $e');
}

// Login flow
try {
  await db.auth.login(
    email: "[email protected]",
    password: "securePassword123",
  );

  // Get user details after login
  final user = await db.auth.getUser();
  if (user != null) {
    print('Logged in successfully! Email: ${user.email}');
  }
} catch (e) {
  print('Login failed: $e');
}

OAuth

// Google OAuth (returns null)
await db.auth.signInWithGoogle();

// Get user after OAuth
final user = await db.auth.getUser();

// GitHub OAuth (returns null)
await db.auth.signInWithGitHub();

// Get user after OAuth
final user = await db.auth.getUser();

Real-time Data

Watch Collection

// Watch for changes
final subscription = db.watchCollection("posts", (event) {
  print('Event type: ${event.type}');  // created, updated, deleted
  print('Document: ${event.document}');
});

// Cancel subscription
subscription.cancel();

Watch Document

final subscription = db.watchDocument("posts", "doc-id", (document) {
  print('Document updated: ${document.data}');
});

// Cancel subscription
subscription.cancel();

Building Reusable Queries

Create query builders as functions:
QueryBuilder publishedBooksQuery(String searchTerm) {
  return QueryBuilder()
    .where('status', 'published')
    .searchInFields(['title', 'description'], searchTerm)
    .orderByDesc('publishedAt')
    .limit(20);
}

// Use it anywhere
final results = await db.listDocuments<Book>("books",
  queryBuilder: publishedBooksQuery('flutter'),
);

Debugging Queries

final query = QueryBuilder()
  .where('status', 'active')
  .whereGreaterThan('age', 18)
  .limit(10);

print(query.build());
// Output: status=active&age__gt=18&limit=10

Best Practices

1. Always Define fromJson() and toJson()

class Book {
  // ... fields ...

  // Required for reading from API
  factory Book.fromJson(Map<String, dynamic> json) { ... }

  // Required for creating/updating via API
  Map<String, dynamic> toJson() { ... }
}

2. Handle Optional Fields

class Book {
  final String? subtitle;  // Optional field

  factory Book.fromJson(Map<String, dynamic> json) {
    return Book(
      subtitle: json['subtitle'] as String?,  // Can be null
    );
  }
}

3. Use Type Casting in fromJson()

factory Book.fromJson(Map<String, dynamic> json) {
  return Book(
    title: json['title'] as String,  // Type cast for safety
    price: (json['price'] as num).toDouble(),  // Handle int or double
    age: json['age'] as int,
  );
}

4. Register Converters Early

void main() {
  // Register all converters at app startup
  CocobaseConverters.register<Book>(Book.fromJson);
  CocobaseConverters.register<User>(User.fromJson);
  CocobaseConverters.register<Post>(Post.fromJson);

  runApp(MyApp());
}

5. Use Filter Map for Simple Queries

// Good - simple and readable
final users = await db.listDocuments("users", filters: {
  'status': 'active',
  'age__gte': 18,
});

// Use QueryBuilder only when you need complex logic

6. Always Limit Results

// Good - always set a limit
final posts = await db.listDocuments("posts", filters: {'limit': 20});

// Bad - could return thousands of documents
final posts = await db.listDocuments("posts");

7. Error Handling

try {
  final doc = await db.getDocument<Book>("books", "doc-id");
  print(doc.data.title);
} catch (e) {
  print('Error: $e');
}

Troubleshooting

Issue: Type mismatch error

Cause: Field type mismatch in fromJson()
// ❌ Wrong - expects String but API returns int
final age = json['age'] as String;

// ✅ Correct
final age = json['age'] as int;

Issue: Converter not registered

Cause: Forgot to register converter
// ✅ Register first
CocobaseConverters.register<Book>(Book.fromJson);

// Now it works
final books = await db.listDocuments<Book>("books");

Issue: Null safety error

Cause: Optional field treated as required
// ✅ Use nullable type
class Book {
  final String? subtitle;  // Optional field

  factory Book.fromJson(Map<String, dynamic> json) {
    return Book(
      subtitle: json['subtitle'] as String?,  // Can be null
    );
  }
}

Query Limits

  • Maximum limit: 1000 documents per request
  • Offset range: 0 to 100,000
  • Field name length: 255 characters
  • Filter value length: 10,000 characters

Next Steps