dartvex_flutter 0.2.0
dartvex_flutter: ^0.2.0 copied to clipboard
Flutter widgets for Convex — ConvexProvider, QueryBuilder, MutationBuilder, and more. Reactive UI powered by Dartvex.
dartvex_flutter #
Flutter widgets and builders for dartvex — the pure Dart client for Convex.
dartvex_flutter removes the repetitive widget lifecycle code around realtime
Convex subscriptions, mutations, actions, and connection state. The package is
designed around a small runtime interface so a future local-first adapter can
plug into the same widgets without breaking the public API.
The Dartvex ecosystem #
| Package | Description |
|---|---|
dartvex |
Core client — WebSocket sync, subscriptions, auth |
dartvex_flutter |
Flutter widgets — Provider, Query, Mutation |
dartvex_codegen |
CLI code generator — type-safe Dart bindings from schema |
dartvex_local |
Offline support — SQLite cache, mutation queue |
dartvex_auth_better |
Better Auth adapter |
Source and full docs: github.com/AndreFrelicot/dartvex
Features #
ConvexQuery— reactive query widget with automatic subscription managementConvexMutation/ConvexAction— request builder widgets, with optional optimistic updates onConvexMutationConvexImage— native image display from Convex file storageConvexCachedImage— native Convex storage images with persistent disk cachingPaginatedQueryBuilder— cursor-based, reactive gapless pagination with load-moreConvexAuthProvider/ConvexAuthBuilder— auth state widgetsConvexConnectionBuilder/ConvexConnectionIndicator— coarse connection statusConvexConnectionStatusBuilder— rich connection status (inflight, retries, loading)ConvexAuthRefreshingBuilder— auth-refreshing indicatorConvexOfflineImage/ConvexAssetCache— native offline binary asset cachingFakeConvexClient— test helper for unit and widget tests- App lifecycle reconnect when a Flutter app resumes while disconnected
- NSURLSession network transports on iOS and macOS, installed automatically
- Runtime-interface based — works with
ConvexClientRuntimeand local-first adapters
The core storage flow works on web: resolve a signed storage URL and render it
with Image.network. The ConvexImage, ConvexCachedImage,
ConvexOfflineImage, and ConvexAssetCache helpers are native-only because
they use dart:io and flutter_cache_manager for streaming downloads and disk
cache/offline fallback.
Installation #
dependencies:
dartvex: ^0.2.0
dartvex_flutter: ^0.2.0
Requires Dart ^3.8.0 and Flutter >=3.32.0.
Platform Transports #
On iOS and macOS the plugin automatically installs NSURLSession-backed
transports at startup (via Dart plugin registration, before main() runs).
Every network path the SDK uses — the sync WebSocket, storage uploads, auth
endpoints, asset-cache downloads, and ConvexFileDownloader — then travels
the same system network stack as Safari and native apps instead of raw
dart:io sockets. POSIX sockets bypass VPNs, proxies, and per-app network
policy, and are blackholed with errno 65 on some iOS devices, which is why
Apple discourages them.
Android, Linux, and Windows keep the default dart:io transport; web uses
package:web. To opt back into dart:io on Apple platforms, reset the
process-wide seams:
import 'package:dartvex_flutter/dartvex_flutter.dart';
void main() {
defaultWebSocketAdapterOverride = null;
defaultHttpClientFactory = null;
runApp(const MyApp());
}
A per-client ConvexClientConfig.adapterFactory or an explicitly provided
HTTP client always takes precedence over the installed defaults.
Provider Setup #
import 'package:dartvex/dartvex.dart';
import 'package:dartvex_flutter/dartvex_flutter.dart';
final client = ConvexClient('https://your-deployment.convex.cloud');
final runtime = ConvexClientRuntime(client);
class AppRoot extends StatelessWidget {
const AppRoot({super.key});
@override
Widget build(BuildContext context) {
return ConvexProvider(
client: runtime,
child: const MyApp(),
);
}
}
To reconnect immediately when the device regains connectivity, pass a
ConnectivityPlusSignal into the client configuration:
final client = ConvexClient(
'https://your-deployment.convex.cloud',
config: ConvexClientConfig(
connectivitySignal: ConnectivityPlusSignal(),
),
);
Auth Widgets #
final authedClient = client.withAuth(myAuthProvider);
ConvexAuthProvider<MyUser>(
client: authedClient,
child: ConvexAuthBuilder<MyUser>(
builder: (context, state) {
return switch (state) {
AuthLoading<MyUser>() => const CircularProgressIndicator(),
AuthAuthenticated<MyUser>(:final userInfo) => Text(userInfo.name),
AuthUnauthenticated<MyUser>() => const Text('Signed out'),
};
},
),
)
Query Widget #
ConvexQuery<List<Message>>(
query: 'messages:list',
args: const {'channel': 'general'},
decode: (value) => decodeMessages(value),
builder: (context, snapshot) {
if (snapshot.isLoading) return const CircularProgressIndicator();
if (snapshot.hasError) return Text(snapshot.error.toString());
final messages = snapshot.data ?? const <Message>[];
return ListView(
children: [for (final message in messages) Text(message.text)],
);
},
)
Mutation Widget #
ConvexMutation<String>(
mutation: 'messages:send',
builder: (context, mutate, snapshot) {
return FilledButton(
onPressed: snapshot.isLoading
? null
: () => mutate({'author': 'Flutter User', 'text': 'Hello'}),
child: Text(snapshot.isLoading ? 'Sending...' : 'Send'),
);
},
)
Cached Images #
Disk-backed image cache is not supported on Flutter web. For web builds, resolve a signed storage URL and render it with
Image.network; keepConvexCachedImage,ConvexOfflineImage, andConvexAssetCachefor native targets.
ConvexCachedImage(
storageId: message.imageStorageId,
getUrlAction: 'files:getUrl',
useAction: true,
width: 160,
height: 160,
fit: BoxFit.cover,
)
ConvexImage and ConvexCachedImage resolve storage URLs with a Convex query
by default. Set useAction: true when the resolver is implemented as an action.
Both widgets are native-only in this release; on web, use the same resolver and
pass the returned URL to Image.network.
Pagination #
PaginatedQueryBuilder is backed by the core reactive pagination engine: loaded
pages update live as their data changes and stay gapless at page boundaries. Its
public API (query / builder / fromJson / args / pageSize / client)
and PaginationStatus are unchanged.
PaginatedQueryBuilder<Message>(
query: 'messages:list',
args: const {'status': 'active'},
pageSize: 20,
fromJson: Message.fromJson,
builder: (context, items, loadMore, status) {
return ListView.builder(
itemCount: items.length + (status == PaginationStatus.allLoaded ? 0 : 1),
itemBuilder: (_, i) => i < items.length
? MessageTile(items[i])
: TextButton(onPressed: loadMore, child: const Text('Load more')),
);
},
)
Testing #
final client = FakeConvexClient()
..whenQuery('messages:list', returns: [mockMessage1, mockMessage2])
..whenMutation('messages:send', returns: {'id': 'xxx'});
await tester.pumpWidget(
ConvexProvider(client: client, child: MyApp()),
);
Optimistic Mutations #
Pass an OptimisticUpdate to ConvexMutation to overlay query results the
instant the mutation is sent; it rolls back automatically when the mutation
completes or fails:
ConvexMutation<String>(
mutation: 'messages:send',
optimisticUpdate: (store) {
final existing = store.getQuery('messages:list', const {'channel': 'general'});
final messages = existing is List ? List<dynamic>.from(existing) : <dynamic>[];
messages.add({'_id': 'optimistic', 'text': 'Hello'});
store.setQuery('messages:list', const {'channel': 'general'}, messages);
},
builder: (context, mutate, snapshot) {
return FilledButton(
onPressed: () => mutate({'channel': 'general', 'text': 'Hello'}),
child: const Text('Send'),
);
},
)
Connection Status #
ConvexConnectionStatusBuilder rebuilds on the rich ConnectionStatus (inflight
counts, retries, hasEverConnected, loading). The coarse ConvexConnectionBuilder
and ConvexConnectionIndicator are unchanged.
ConvexConnectionStatusBuilder(
builder: (context, status) {
if (status.isLoading) return const Text('Syncing…');
if (!status.isWebSocketConnected) {
return Text('Reconnecting (attempt ${status.connectionRetries})');
}
return Text('Online — ${status.inflightMutations} pending');
},
)
Auth Refreshing #
ConvexAuthRefreshingBuilder rebuilds with the client's auth-refreshing state
(true while auth is being recovered after a server rejection), so you can show
an indicator instead of surfacing the brief disconnect:
ConvexAuthRefreshingBuilder(
builder: (context, isRefreshing) {
return isRefreshing
? const LinearProgressIndicator()
: const SizedBox.shrink();
},
)
API Overview #
| Widget | Description |
|---|---|
ConvexProvider |
Provides client to widget tree via InheritedWidget |
ConvexQuery |
Reactive query with automatic re-rendering |
ConvexMutation |
Mutation trigger with loading/error state and optimistic updates |
ConvexAction |
Action trigger with loading/error state |
PaginatedQueryBuilder |
Cursor-based reactive gapless paginated query |
ConvexAuthBuilder |
Renders based on auth state |
ConvexAuthRefreshingBuilder |
Renders based on auth-refreshing state |
ConvexConnectionBuilder |
Renders based on coarse connection state |
ConvexConnectionStatusBuilder |
Renders based on rich connection status |
ConvexImage |
Native image from Convex storage |
ConvexCachedImage |
Native disk-cached image from Convex storage |
ConvexOfflineImage |
Native offline-capable image with caching |
FakeConvexClient |
Test double for widget tests |
Full Documentation #
See the Dartvex monorepo for full documentation and examples.