bloomBUILD•SHIP•BLOOM
.md
Async Server State

Data Layer & Caching

Bloom Data brings TanStack Query semantics to Flutter with a zero-cost Fine-Grained Signals reactivity model, automatic SWR caching, in-flight deduplication, and native SQLite offline persistence.

Bloom Data vs. TanStack Query (React Query)

While TanStack Query relies on React component-level hook reruns and JavaScript runtime type erasure, Bloom Data is built on compile-time Dart generics, zero-rebuild Signals, and integrated mobile offline persistence:

FeatureBloom Data (Flutter)TanStack Query (React / JS)
Reactivity EngineFine-Grained Signals (0 full widget rebuilds)Component Hooks (Entire component re-runs)
Type SafetyCompile-Time Reified Dart GenericsTypeScript (Erased at runtime)
Bulk Cache Writes84 ms (119k ops/sec) 🥇132 ms (75k ops/sec)
In-Flight Deduplication23 ms resolution (1k calls / 50 keys) 🥇34 ms resolution
Offline PersistenceNative SQLite & SecureStorage adapterRequires external persist plugins
Realtime InvalidationNative RealtimeQueryBridgeManual socket.on boilerplate wiring

Declarative Queries (SWR)

Define queries inside your controller with declarative stale and garbage collection timeouts:

lib/features/posts/posts_controller.dart
dart
// lib/features/posts/posts_controller.dart
import 'package:bloom_framework/bloom.dart';
import '../services/api_service.dart';

class PostsController extends BloomController {
  late final postsQuery = createQuery<List<Post>>(
    queryKey: ['posts', 'feed'],
    queryFn: () => inject<ApiService>().fetchPosts(),
    staleTime: const Duration(minutes: 5),
    gcTime: const Duration(hours: 1),
    persister: inject<BloomStorageAdapter>(), // Native SQLite / SecureStorage persistence
  );

  void refreshPosts() {
    postsQuery.refetch();
  }
}

Optimistic Mutations & Rollback

Mutate server state optimistically for instantaneous user feedback with safe automatic rollback if network requests fail:

Optimistic Mutation Pattern
dart
// Optimistic Mutation with Automatic Rollback
late final addPostMutation = createMutation<Post, String>(
  mutationFn: (newTitle) => inject<ApiService>().createPost(newTitle),
  optimisticKey: ['posts', 'feed'],
  optimisticData: (newTitle, previousPosts) {
    // 1. Optimistically update local signal cache immediately
    final optimisticPost = Post(id: 'temp-${DateTime.now()}', title: newTitle);
    return [...(previousPosts ?? []), optimisticPost];
  },
  onError: (error, newTitle, rollbackData) {
    // 2. Automatic rollback if network request fails
    logger.error('Post creation failed, rolling back cache', error: error);
  },
  invalidateKeys: [['posts', 'feed']], // 3. Revalidate from server on success
  offlineQueue: true, // 4. Enqueue mutation for background replay if offline
);

Fine-Grained Signals Reactivity

Wrap only the leaf UI nodes with Watch. Unlike React hooks, parent layouts and sibling widgets never re-render when query signals update:

Widget Consumption
dart
// In your Flutter Widget
Watch((context) {
  final state = controller.postsQuery;

  if (state.isLoading.value) {
    return const Center(child: CircularProgressIndicator());
  }

  if (state.isError.value) {
    return Center(child: Text('Error: ${state.error.value}'));
  }

  final posts = state.data.value ?? [];
  return ListView.builder(
    itemCount: posts.length,
    itemBuilder: (context, i) => ListTile(title: Text(posts[i].title)),
  );
})

Native Realtime Query Bridge

Connect channel broadcasts directly to query cache invalidation. When the server pushes an event, Bloom automatically schedules a background revalidation:

lib/features/tasks/tasks_controller.dart
dart
// Live Query Invalidation via WebSockets
class LiveListController extends BloomController {
  late final tasksQuery = createQuery<List<Task>>(
    queryKey: ['lists', listId, 'tasks'],
    queryFn: () => inject<TaskApi>().fetchTasks(listId),
  );

  @override
  void onInit() {
    // Automatically refetches tasksQuery when backend broadcasts to this channel
    inject<BloomRealtimeClient>().invalidateQueriesOnBroadcast(
      channel: 'lists:$listId',
      key: ['lists', listId, 'tasks'],
    );
  }
}