Flutter Widgets
InstantDB Flutter 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 InstantDB Flutter. 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 widgetsclass 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 widgetInstantBuilder( 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 InstantDB Flutter widgets and patterns:
- Presence Widgets - Show real-time user presence
- Authentication Widgets - Handle user authentication UI
- Advanced Patterns - Complex widget compositions
- Performance Tips - Optimize your reactive UI