Skip to Content
FlutterWidgets

Flutter InstantDB provides specialized widgets that automatically update when your data changes. These widgets handle loading states, errors, and real-time updates seamlessly.

InstantBuilder

The InstantBuilder widget is the foundation for reactive UI in Flutter InstantDB. It automatically rebuilds when query results change.

Basic Usage

InstantBuilder( query: {'todos': {}}, builder: (context, result) { if (result.isLoading) { return const CircularProgressIndicator(); } if (result.hasError) { return Text('Error: ${result.error}'); } final todos = result.data!['todos'] as List; return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; return ListTile( title: Text(todo['text']), trailing: Checkbox( value: todo['completed'], onChanged: (value) => _toggleTodo(todo['id'], value), ), ); }, ); }, )

With Custom Loading and Error Widgets

InstantBuilder( query: {'todos': {}}, loadingBuilder: (context) => const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Loading todos...'), ], ), ), errorBuilder: (context, error) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error, size: 64, color: Colors.red), SizedBox(height: 16), Text('Failed to load todos'), SizedBox(height: 8), Text(error, style: TextStyle(fontSize: 12)), SizedBox(height: 16), ElevatedButton( onPressed: () => _retry(), child: Text('Retry'), ), ], ), ), builder: (context, result) { final todos = result.data!['todos'] as List; if (todos.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.check_circle, size: 64, color: Colors.grey), SizedBox(height: 16), Text('No todos yet'), Text('Add your first todo above!'), ], ), ); } return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) => TodoTile(todo: todos[index]), ); }, )

InstantBuilderTyped

For better type safety and data transformation, use InstantBuilderTyped:

InstantBuilderTyped<List<Todo>>( query: {'todos': {}}, transformer: (data) { final todosData = data['todos'] as List; return todosData.map((json) => Todo.fromJson(json)).toList(); }, builder: (context, todos) { return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; return TodoTile( title: todo.text, isCompleted: todo.completed, onToggle: () => _toggleTodo(todo.id), ); }, ); }, )

Advanced Transformer

Transform and sort data before rendering:

InstantBuilderTyped<List<Todo>>( query: {'todos': {}}, transformer: (data) { final todosData = data['todos'] as List; final todos = todosData.map((json) => Todo.fromJson(json)).toList(); // Sort by priority, then by creation date todos.sort((a, b) { final priorityComparison = b.priority.compareTo(a.priority); if (priorityComparison != 0) return priorityComparison; return b.createdAt.compareTo(a.createdAt); }); return todos; }, builder: (context, sortedTodos) { return ListView.builder( itemCount: sortedTodos.length, itemBuilder: (context, index) => TodoTile(todo: sortedTodos[index]), ); }, )

Watch Widget

For simpler reactive widgets that don’t need query data, use the Watch widget:

class CounterDisplay extends StatelessWidget { final Signal<int> counter; const CounterDisplay({super.key, required this.counter}); @override Widget build(BuildContext context) { return Watch((context) { return Text( 'Count: ${counter.value}', style: Theme.of(context).textTheme.headlineMedium, ); }); } }

InstantProvider

The InstantProvider widget provides access to your InstantDB instance throughout your widget tree:

class MyApp extends StatelessWidget { final InstantDB db; const MyApp({super.key, required this.db}); @override Widget build(BuildContext context) { return InstantProvider( db: db, child: MaterialApp( title: 'My App', home: const HomePage(), ), ); } } // Access in child widgets class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { final db = InstantProvider.of(context); return Scaffold( body: InstantBuilder( query: {'todos': {}}, builder: (context, result) { // Build your UI }, ), floatingActionButton: FloatingActionButton( onPressed: () => _addTodo(db), child: const Icon(Icons.add), ), ); } }

Custom Reactive Widgets

Create your own reactive widgets using the underlying signals:

class TodoCounter extends StatelessWidget { const TodoCounter({super.key}); @override Widget build(BuildContext context) { final db = InstantProvider.of(context); final todosQuery = db.query({'todos': {}}); return Watch((context) { final result = todosQuery.value; if (!result.hasData) { return const Text('...'); } final todos = result.data!['todos'] as List; final completed = todos.where((todo) => todo['completed'] == true).length; return Text('$completed / ${todos.length} completed'); }); } }

Widget Composition

Combine multiple InstantDB widgets for complex UIs:

class TodosPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Todos'), actions: [ // Show todo count in app bar const TodoCounter(), const SizedBox(width: 16), ], ), body: Column( children: [ // Add todo form const AddTodoForm(), // Filter tabs const TodoFilters(), // Main todo list Expanded( child: InstantBuilderTyped<List<Todo>>( query: {'todos': {}}, transformer: _transformTodos, builder: (context, todos) { return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) => TodoTile( todo: todos[index], onToggle: () => _toggleTodo(todos[index]), onDelete: () => _deleteTodo(todos[index].id), ), ); }, ), ), ], ), ); } }

Performance Considerations

Optimize Rebuilds

Use const constructors when possible:

InstantBuilder( query: {'todos': {}}, loadingBuilder: (context) => const LoadingSpinner(), // const errorBuilder: (context, error) => ErrorWidget(error: error), builder: (context, result) { final todos = result.data!['todos'] as List; return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) => TodoTile( key: ValueKey(todos[index]['id']), // Stable keys todo: todos[index], ), ); }, )

Selective Updates

Use specific queries to minimize unnecessary rebuilds:

// Instead of querying all todos and filtering in the widget InstantBuilder( query: { 'todos': { 'where': {'userId': currentUserId}, // Filter at query level }, }, builder: (context, result) { // Only rebuilds when user's todos change }, )

Memoization

Use useMemo for expensive computations:

InstantBuilderTyped<List<Todo>>( query: {'todos': {}}, transformer: (data) { // This transformer only runs when data actually changes return (data['todos'] as List) .map((json) => Todo.fromJson(json)) .toList(); }, builder: (context, todos) { // Expensive computation memoized final groupedTodos = useMemo( () => _groupTodosByCategory(todos), [todos], ); return CategoryView(groups: groupedTodos); }, )

Error Boundaries

Handle errors at the widget level:

class TodosWithErrorBoundary extends StatelessWidget { @override Widget build(BuildContext context) { return ErrorBoundary( onError: (error, stackTrace) { // Log error to analytics Analytics.reportError(error, stackTrace); }, child: InstantBuilder( query: {'todos': {}}, builder: (context, result) { if (result.hasError) { return ErrorRetryWidget( error: result.error!, onRetry: () => _retryQuery(), ); } // Success case return TodosList(data: result.data!); }, ), ); } }

Next Steps

Learn more about Flutter InstantDB widgets and patterns: