ObjectBox #

When the Flutter app you’re developing needs local storage with very large data volumes (reaching tens of thousands to millions of data rows) and complex object relations, SharedPreferences or Hive start showing their limitations. SQLite with wrapper libraries like Drift is indeed reliable, but you have to write object-relational mapping (ORM) layers and SQL queries that often eat development time. As a modern alternative, ObjectBox comes offering an object-oriented NoSQL database solution with extreme performance specifically designed for mobile, desktop, and embedded devices.

In this in-depth guide, we’ll thoroughly unpack ObjectBox from basic to advanced levels. We’ll learn about the native architecture behind its speed, entity configuration, managing the database lifecycle (Store), advanced query techniques using the Query Builder, one-to-many (ToOne) and many-to-many (ToMany) relation modeling, ACID transactions for data integrity, and reactive integration with state management.

Introduction & Object-Oriented Architecture #

ObjectBox differs from most other mobile databases. Instead of wrapping the SQL-based SQLite library or using plain-text JSON parsers, ObjectBox is written using the high-performance C++ programming language as its core engine. This C++ library interacts directly with the device’s file system using Memory-Mapped Files technology similar to the LMDB (Lightning Memory-Mapped Database) database architecture.

In ObjectBox’s object-oriented architecture, you don’t need to think about rows, columns, or tables. You define your data models as regular Dart classes, and ObjectBox directly stores those objects into the device’s physical storage as compressed binary structures. This process eliminates the need to convert from Dart objects to JSON, then from JSON to database format, which usually consumes a lot of CPU time (serialization process).

Some of the main advantages of ObjectBox’s architecture include:

  • Extreme Speed: Read and write operations in ObjectBox far surpass SQLite/Drift because data is accessed directly through native C memory pointers without SQL string interpretation overhead.
  • Full ACID Transactions: ObjectBox guarantees your data integrity through transactions meeting the ACID principle (Atomicity, Consistency, Isolation, Durability). If the app suddenly crashes or the device dies mid-write operation, the database is guaranteed not to corrupt.
  • Low Resource Usage: Very efficient RAM memory and CPU cycle usage makes your app more battery-friendly.
  • Native Relations: Relationships between objects are modeled directly as object references, not as foreign keys that must be manually joined in queries.

Installation & Web Platform Limitations #

To integrate ObjectBox into a Flutter project, you need to add the main dependency and the native binary libraries appropriate for the target operating system.

Add the following configuration to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  objectbox: ^4.0.3
  objectbox_flutter_libs: any   # Native binary library for Android and iOS
  path: ^1.9.0
  path_provider: ^2.1.5

dev_dependencies:
  build_runner: ^2.4.13
  objectbox_generator: any      # ObjectBox binding code generator

After that, run the flutter pub get command in your project terminal.

[!WARNING] Critical Web Platform Limitation: The most crucial aspect you must consider before choosing ObjectBox is the lack of native support for the Web platform. Because ObjectBox’s core engine is written in C++ and compiled natively for each CPU architecture (ARM, x86), it can’t run in standard browser environments directly. If your Flutter app is designed to run as a multi-platform app covering Android, iOS, Desktop, and Web, you must use alternative libraries like Hive or Drift (with the WASM driver) for the web part, or use conditional imports techniques to separate local storage implementations per platform.


Entity: Defining Data Models #

In ObjectBox, the Dart model classes you want to store in the database are called Entities. You define an Entity by adding the @Entity() annotation above your class definition. Every Entity must have a unique identification property (ID) of int data type marked with the @Id() annotation.

Here’s an example of a Product data model configured as an ObjectBox Entity:

// lib/features/inventory/data/models/product.dart

import 'package:objectbox/objectbox.dart';

@Entity()
class Product {
  // ID must be int type. A value of 0 indicates a new unsaved object.
  // ObjectBox will give an auto-increment ID when you save it to the Box.
  @Id()
  int id;

  // Indexes are used to speed up search processes based on this field
  @Index()
  String code;

  String name;
  double price;
  bool isAvailable;

  // Convert the DateTime representation into millisecond date/time format in the database
  @Property(type: PropertyType.date)
  DateTime createdAt;

  // Properties with the @Transient annotation won't be stored in the physical database.
  // Very useful for holding temporary UI state.
  @Transient()
  bool isCheckedInUi;

  Product({
    this.id = 0,
    required this.code,
    required this.name,
    required this.price,
    required this.isAvailable,
    required this.createdAt,
    this.isCheckedInUi = false,
  });
}

After defining the Entity, you must run the code generator to create the objectbox.g.dart binding file containing the internal property mapping logic:

flutter pub run build_runner build --delete-conflicting-outputs

Important Annotation Explanations: #

  • @Id(): Marks the primary key. If you want to set the ID manually from the app (not auto-increment), use @Id(assignable: true).
  • @Index(): Creates a search index on that property. Highly recommended for properties you often use in query filtering conditions (where clauses) to avoid full database scans.
  • @Unique(): Guarantees that the property value can’t be the same as other data in the database. If duplication occurs, ObjectBox will throw an error during writing.
  • @Property(type: ...): Used to provide additional configuration like special storage types (e.g., storing images as binary bytes).

Store & Database Lifecycle Management #

Store is the main object acting as the entry point for interacting with your ObjectBox database. Store represents the physical database file on disk and manages the connection to the native database engine. Because Store initialization consumes considerable CPU resources, you must create one single Store instance (Singleton) and use it throughout your app’s lifecycle.

Here’s how to design a clean and safe Store management helper class:

// lib/core/storage/objectbox_manager.dart

import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:objectbox/objectbox.dart';
import '../../features/inventory/data/models/product.dart';
import 'objectbox.g.dart'; // File generated by build_runner

class ObjectBoxManager {
  // Single ObjectBox Store instance
  late final Store _store;

  // Private constructor
  ObjectBoxManager._(this._store);

  // Async initialization method
  static Future<ObjectBoxManager> create() async {
    // 1. Get the safe app documents folder on the device
    final directory = await getApplicationDocumentsDirectory();
    
    // 2. Determine the special ObjectBox storage folder path
    final String databasePath = p.join(directory.path, 'objectbox_db');

    // 3. Open the Store. The openStore() function is defined in objectbox.g.dart
    final Store store = await openStore(directory: databasePath);

    return ObjectBoxManager._(store);
  }

  // Getter to access the Store from outside
  Store get store => _store;

  // Getter to make accessing a specific Entity Box easier
  Box<Product> get productBox => Box<Product>(_store);

  // Safely close the Store when the app closes
  // Very important to release the file lock in the operating system
  void dispose() {
    if (!_store.isClosed()) {
      _store.close();
    }
  }
}

In main.dart, you perform global initialization:

// lib/main.dart

import 'package:flutter/material.dart';
import 'core/storage/objectbox_manager.dart';

// Global variable for easy access throughout the app (or injected via Dependency Injection)
late final ObjectBoxManager localDatabase;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize the database before runApp
  localDatabase = await ObjectBoxManager.create();

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(child: Text('ObjectBox Ready to Use')),
      ),
    );
  }
}

Synchronous & Asynchronous CRUD Operations #

Data write and read interactions in ObjectBox are done through the Box<T> class. Unlike other databases requiring the await keyword on every operation (like Drift or SQFlite), ObjectBox provides synchronous methods by default. This is because its very fast data access through RAM mapping makes those operations non-blocking to the UI thread for small data sizes.

However, for large volume data writes, ObjectBox also provides asynchronous methods that run on background threads so your app stays responsive.

Here’s a complete CRUD operation example on a Box:

import 'features/inventory/data/models/product.dart';
import 'main.dart'; // Access the global localDatabase variable

void objectBoxCrudDemonstration() {
  final Box<Product> box = localDatabase.productBox;

  // ==========================================
  // 1. CREATE / INSERT (Add Data Operations)
  // ==========================================
  
  final product1 = Product(
    code: 'PROD-A',
    name: 'Gaming Mouse X',
    price: 350000.0,
    isAvailable: true,
    createdAt: DateTime.now(),
  );

  // Save one object synchronously. Returns the new ID.
  final int newId = box.put(product1);
  debugPrint('Product successfully saved with ID: $newId');

  // Save many objects at once (Bulk Write) - Much faster than put() loops
  final productList = [
    Product(code: 'PROD-B', name: 'Mechanical Keyboard', price: 750000.0, isAvailable: true, createdAt: DateTime.now()),
    Product(code: 'PROD-C', name: 'LED 24 Inch Monitor', price: 1800000.0, isAvailable: false, createdAt: DateTime.now()),
  ];
  final List<int> listIds = box.putMany(productList);
  debugPrint('New product ID list: $listIds');

  // ==========================================
  // 2. READ (Data Read Operations)
  // ==========================================

  // Read one data based on ID (Returns null if not found)
  final Product? product = box.get(newId);
  if (product != null) {
    debugPrint('Found product: ${product.name}');
  }

  // Read many data based on an ID list
  final List<Product?> listResults = box.getMany(listIds);
  debugPrint('Number of data found: ${listResults.length}');

  // Read all data in the database
  final List<Product> allProducts = box.getAll();
  debugPrint('Total all products: ${allProducts.length}');

  // ==========================================
  // 3. UPDATE (Data Change Operations)
  // ==========================================

  if (product != null) {
    // Just change the value, then call put() again using the object with the same ID
    product.price = 320000.0; // Discount
    box.put(product); // Because product.id isn't 0, ObjectBox will update the data
  }

  // ==========================================
  // 4. DELETE (Data Delete Operations)
  // ==========================================

  // Delete data based on ID
  final bool deleteStatus = box.remove(newId);
  debugPrint('Deletion status: $deleteStatus');

  // Delete many data at once
  box.removeMany(listIds);

  // Delete the entire database contents (Use very carefully!)
  // box.clear();

  // ==========================================
  // 5. ASYNC OPERATIONS
  // ==========================================
  
  // Very important for writing thousands of rows of data in the background
  box.putAsync(product1).then((asyncId) {
    debugPrint('Async put completed with ID: $asyncId');
  });
}

Query Builder: Data Search & Filtering #

To search data with specific criteria, you use the Query Builder provided type-safely by ObjectBox. The previously generated objectbox.g.dart file contains metadata properties (like Product_) mapping database columns, so you avoid manual query typing in plain text form.

[!IMPORTANT] Important: Query Memory Management: Every time you finish creating a query with query.build(), you must call the query.close() method after finishing reading the results. This is crucial because query objects allocate pointer memory on the native C++ side. If you forget to close the query, that memory won’t be freed by Dart’s Garbage Collector, which will gradually cause memory leaks.

Here’s an example of query variations using the Query Builder:

import 'core/storage/objectbox.g.dart'; // Must be imported to detect Product_
import 'features/inventory/data/models/product.dart';
import 'main.dart';

void objectBoxQueryDemonstration() {
  final Box<Product> box = localDatabase.productBox;

  // 1. Query with one simple condition (Find available products)
  final availableQuery = box.query(Product_.isAvailable.equals(true)).build();
  final List<Product> availableResults = availableQuery.find();
  availableQuery.close(); // Must be closed!

  // 2. Query with multiple conditions (AND)
  final filterQuery = box.query(
    Product_.isAvailable.equals(true)
    .and(Product_.price.greaterThan(500000.0))
  ).build();
  final List<Product> filterResults = filterQuery.find();
  filterQuery.close();

  // 3. Value range query (Price search between 100,000 and 1,000,000)
  final rangeQuery = box.query(Product_.price.between(100000.0, 1000000.0)).build();
  final List<Product> rangeResults = rangeQuery.find();
  rangeQuery.close();

  // 4. String query with partial text matching (Case Insensitive)
  final nameQuery = box.query(
    Product_.name.contains('gaming', caseSensitive: false)
  ).build();
  final List<Product> nameResults = nameQuery.find();
  nameQuery.close();

  // 5. Query with Sorting and Pagination (Limit-Offset)
  // Display the 10 most expensive available products
  final sortPageQuery = box.query(Product_.isAvailable.equals(true))
    .order(Product_.price, flags: Order.descending) // Sort descending
    .build()
    ..limit = 10
    ..offset = 0; // First page
    
  final List<Product> mostExpensiveList = sortPageQuery.find();
  sortPageQuery.close();

  // 6. Count data without loading objects into RAM
  final countQuery = box.query(Product_.price.lessThan(200000.0)).build();
  final int cheapCount = countQuery.count();
  countQuery.close();
  debugPrint('Number of cheap products: $cheapCount');
}

Relation Modeling: ToOne and ToMany #

ObjectBox supports database relations fully and handles them efficiently using the lazy loading concept. Relation data is only loaded from disk to RAM memory when that relation property is actively accessed in code.

You divide relations into two main types:

  1. ToOne<Target>: Connects one object to exactly one other target object (One-to-One or Many-to-One relations).
  2. ToMany<Target>: Connects one object to many other target objects (One-to-Many or Many-to-Many relations).

Relation Schema Diagram #

Observe the relation diagram below to see how ToOne and ToMany are modeled and how backlinks are used to access relations from the opposite direction.

graph TD
    Customer["Customer (Entity)"] -. "Backlink (ToMany)" .-> Order["Order (Entity)"]
    Order -->|ToOne| Customer
    
    Category["Category (Entity)"] -->|ToMany| Product["Product (Entity)"]
    Product -. "Backlink (ToMany)" .-> Category

1. ToOne Implementation (One Order has One Customer) #

First, let’s define the Customer and Order Entities:

// lib/features/orders/data/models/customer.dart
import 'package:objectbox/objectbox.dart';
import 'order.dart';

@Entity()
class Customer {
  @Id()
  int id;
  String name;
  String email;

  // Backlink: Automatic access from Customer to see all their orders
  // We point to the 'customer' property in the Order class
  @Backlink('customer')
  final orders = ToMany<Order>();

  Customer({this.id = 0, required this.name, required this.email});
}
// lib/features/orders/data/models/order.dart
import 'package:objectbox/objectbox.dart';
import 'customer.dart';

@Entity()
class Order {
  @Id()
  int id;
  String orderNumber;
  double grandTotal;

  // ToOne relation to Customer
  final customer = ToOne<Customer>();

  Order({this.id = 0, required this.orderNumber, required this.grandTotal});
}

Here’s an example of how to write and read ToOne relation data:

void toOneRelationDemo() {
  final Box<Customer> customerBox = localDatabase.store.box<Customer>();
  final Box<Order> orderBox = localDatabase.store.box<Order>();

  // 1. Create Customer data
  final budi = Customer(name: 'Budi Hartono', email: '[email protected]');
  customerBox.put(budi); // budi's ID will be assigned automatically

  // 2. Create Order data
  final newOrder = Order(orderNumber: 'ORD-2026-001', grandTotal: 450000.0);
  
  // Connect the order to customer Budi
  newOrder.customer.target = budi;

  // Save the Order. ObjectBox will automatically record budi's relation ID.
  orderBox.put(newOrder);

  // 3. Read the relation (Lazy Loading)
  final Order? savedOrder = orderBox.get(newOrder.id);
  if (savedOrder != null) {
    // The customer object is loaded asynchronously in the background when the .target property is accessed
    final Customer? orderCustomer = savedOrder.customer.target;
    debugPrint('Order belongs to customer: ${orderCustomer?.name}'); // Output: Budi Hartono
  }
}

2. ToMany Implementation (One Category has Many Products) #

Let’s use a Category model that has a ToMany relation to Product:

// lib/features/inventory/data/models/category.dart
import 'package:objectbox/objectbox.dart';
import 'product.dart';

@Entity()
class Category {
  @Id()
  int id;
  String name;

  // ToMany relation to Product
  final products = ToMany<Product>();

  Category({this.id = 0, required this.name});
}

Here’s how to add products to a specific category:

void toManyRelationDemo() {
  final Box<Category> categoryBox = localDatabase.store.box<Category>();
  final Box<Product> productBox = localDatabase.store.box<Product>();

  // 1. Create a New Category
  final electronics = Category(name: 'Home Electronics');

  // 2. Create New Products
  final fan = Product(code: 'FAN-01', name: 'Standing Fan', price: 250000.0, isAvailable: true, createdAt: DateTime.now());
  final blender = Product(code: 'BLEN-02', name: 'Juicer Blender', price: 400000.0, isAvailable: true, createdAt: DateTime.now());

  // 3. Insert the products into the Category's product list
  electronics.products.addAll([fan, blender]);

  // 4. Save the Category.
  // IMPORTANT: ObjectBox will automatically save the new Product objects (fan & blender)
  // in that products list into the product Box in a chain (Cascading Put).
  categoryBox.put(electronics);

  // Read the category contents
  final Category? cat = categoryBox.get(electronics.id);
  debugPrint('Category ${cat?.name} has ${cat?.products.length} products.');
}

ACID Transactions & Performance Optimization #

Every time you call box.put(...), ObjectBox implicitly opens and closes a write transaction. If you call put() in a loop 1,000 times, ObjectBox will perform transaction opening and physical writing operations 1,000 times. This is a huge performance loss because of disk I/O limitations.

To overcome this, you must unify all those write operations into a single transaction using the runInTransaction method. This way, the transaction commit is only done once at the end of the process, increasing write speed by tens of times.

Here’s the transaction implementation technique:

void objectBoxTransactionDemo() {
  final List<Product> newList = List.generate(500, (index) {
    return Product(
      code: 'PROD-INDEX-$index',
      name: 'Item Number $index',
      price: 10000.0 * index,
      isAvailable: true,
      createdAt: DateTime.now(),
    );
  });

  // OPTIMIZATION: Run the mass write transaction synchronously
  // These 500 put operations will be unified into one single physical transaction commit
  localDatabase.store.runInTransaction(TxMode.write, () {
    final Box<Product> box = localDatabase.productBox;
    for (final product in newList) {
      box.put(product);
    }
  });

  // IF an error or logic failure occurs inside the transaction block:
  try {
    localDatabase.store.runInTransaction(TxMode.write, () {
      final Box<Product> box = localDatabase.productBox;
      
      box.put(Product(code: 'A', name: 'Product A', price: 100.0, isAvailable: true, createdAt: DateTime.now()));
      
      // Simulate an error occurring mid-way
      throw Exception('Simulated transaction system failure');
      
      box.put(Product(code: 'B', name: 'Product B', price: 200.0, isAvailable: true, createdAt: DateTime.now()));
    });
  } catch (e) {
    debugPrint('Transaction failed: All write operations in the block are automatically rolled back.');
    // Product A is guaranteed not to be stored in the physical database
  }
}

Reactive Streams: Automatic UI Synchronization #

ObjectBox supports reactive programming concepts natively. You can listen to data changes on a Box or a specific query, then update the user interface in real-time. This is possible because ObjectBox provides Dart Stream integration.

Creating a Watch Query for the UI #

Here’s an example implementation of a Watch Query listening to the product list and channeling it as a Stream for consumption by a StreamBuilder in a UI widget:

// lib/features/inventory/presentation/widgets/product_list_stream.dart

import 'package:flutter/material.dart';
import '../../data/models/product.dart';
import '../../../../main.dart'; // Access the global database
import '../../../../core/storage/objectbox.g.dart';

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

  @override
  State<ProductListStream> createState() => _ProductListStreamState();
}

class _ProductListStreamState extends State<ProductListStream> {
  late final Query<Product> _productQuery;
  late final Stream<List<Product>> _productStream;

  @override
  void initState() {
    super.initState();

    // 1. Create an active product read query
    _productQuery = localDatabase.productBox
        .query(Product_.isAvailable.equals(true))
        .order(Product_.name)
        .build();

    // 2. Enable watch() to produce a Stream
    // triggerImmediately: true ensures the stream immediately sends initial data when listened to
    _productStream = _productQuery
        .watch(triggerImmediately: true)
        .map((query) => query.find()); // Map the query result into a Dart product list
  }

  @override
  void dispose() {
    // 3. Must close the query and free the native C++ binary memory pointers
    _productQuery.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<Product>>(
      stream: _productStream,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }
        if (snapshot.hasError) {
          return Center(child: Text('Error: ${snapshot.error}'));
        }

        final products = snapshot.data ?? [];
        if (products.isEmpty) {
          return const Center(child: Text('No products available'));
        }

        return ListView.builder(
          itemCount: products.length,
          itemBuilder: (context, index) {
            final product = products[index];
            return ListTile(
              title: Text(product.name),
              subtitle: Text('Rp ${product.price}'),
            );
          },
        );
      },
    );
  }
}

Through this watch() implementation, whenever any other part of your code calls box.put() or box.remove(), the stream automatically triggers new data delivery and the widget interface above instantly re-renders.


State Management Integration & ObjectBox Admin #

To unify ObjectBox with clean architecture, you should hide direct database access under the state provider layer like Riverpod.

Riverpod Integration #

Here’s how to design clean ObjectBox provisioning using Riverpod:

// lib/core/providers/database_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:objectbox/objectbox.dart';
import '../storage/objectbox_manager.dart';
import '../../features/inventory/data/models/product.dart';

// Main provider for ObjectBoxManager
// The value will be overridden in the main() file
final objectBoxManagerProvider = Provider<ObjectBoxManager>((ref) {
  throw UnimplementedError('ObjectBoxManager has not been overridden');
});

// Provider to access the Product Box instantly
final productBoxProvider = Provider<Box<Product>>((ref) {
  return ref.watch(objectBoxManagerProvider).productBox;
});

// StreamProvider for reactive product data
final reactiveProductsProvider = StreamProvider<List<Product>>((ref) {
  final box = ref.watch(productBoxProvider);
  
  // Create the query, listen, and close the query automatically when the provider is destroyed (autoDispose)
  final query = box.query().build();
  
  ref.onDispose(() {
    query.close();
  });

  return query.watch(triggerImmediately: true).map((q) => q.find());
});

Then in the main.dart file, you override the provider:

// lib/main.dart (Riverpod setup)

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/storage/objectbox_manager.dart';
import 'core/providers/database_provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final ObjectBoxManager obxManager = await ObjectBoxManager.create();

  runApp(
    ProviderScope(
      overrides: [
        objectBoxManagerProvider.overrideWithValue(obxManager),
      ],
      child: const MaterialApp(home: HomeScreen()),
    ),
  );
}

ObjectBox Admin for Debugging #

During development mode, viewing database contents directly from the device or emulator is very difficult because the data is encrypted binary type. ObjectBox provides a very sophisticated web GUI visualization tool called ObjectBox Admin.

To use it:

  1. Add the objectbox_admin package to your pubspec.yaml file (recommended only for debugging purposes).
  2. Enable admin in the main.dart function after the Store is opened:
import 'package:flutter/foundation.dart';
import 'package:objectbox/objectbox.dart';
import 'core/storage/objectbox_manager.dart';

void initializeAdmin(Store store) {
  // Enable Admin ONLY in debug mode so it doesn't pollute the production app
  if (kDebugMode) {
    // Run the admin server on the default port 8090
    final admin = Admin(store);
    debugPrint('ObjectBox Admin Server running at http://localhost:8090');
    
    // You can open the link above through a computer web browser
    // to view data, do visual queries, and monitor the database schema.
  }
}

ObjectBox Admin makes the verification process of whether your relational data is correctly connected much easier and speeds up visual data bug investigations during the code writing process.

Summary #

  • ObjectBox is an object-oriented NoSQL database written in native C++. It offers the fastest read/write speeds on mobile devices without needing to write manual SQL mapping.
  • Entity & ID: Use the @Entity() annotation and ensure there’s an @Id() int id field as the primary key. Put the value 0 to store new data.
  • Single Store: Open the Store only once at app startup, then use that instance throughout the app’s active period to prevent database file locking.
  • Transaction Optimization: Use store.runInTransaction to unify many write operations (put) into a single commit for disk I/O efficiency.
  • Query Memory Management: Always call query.close() after getting query results to free native C++ memory allocations and avoid memory leaks.
  • Lazy Relations: Data relationships are modeled through ToOne<T> and ToMany<T> classes supporting lazy data loading processes.
  • Reactive Queries: Leverage query.watch() to produce data Streams that automatically update interfaces when the database changes.
  • No Web Feature: ObjectBox doesn’t support Flutter Web because of native C++ dependencies. If the Web platform is mandatory, use Hive or Drift.

← Previous: Hive   Next: Drift →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact