Hooks (flutter_hooks)
If your project uses flutter_hooks,
qora_hooks provides a hooks-based API with composable primitives for complex screens.
Installation
# You don't need to add `qora`: it's included transitively.
dependencies:
flutter_hooks: ^0.21.0
qora_hooks: ^ 1.3.1
useQuery
Fetches data and rebuilds the widget when the state changes.
Equivalent to QoraBuilder without a nested builder callback.
class UserScreen extends HookWidget {
final String userId;
const UserScreen({super.key, required this.userId});
@override
Widget build(BuildContext context) {
final state = useQuery<User>(
key: ['users', userId],
fetcher: () => Api.getUser(userId),
options: const QoraOptions(staleTime: Duration(minutes: 5)),
);
return switch (state) {
Initial() => const SizedBox.shrink(),
Loading(:final previousData) => previousData != null
? UserCard(previousData)
: const CircularProgressIndicator(),
Success(:final data) => UserCard(data),
Failure(:final error) => ErrorView(error),
};
}
}
useQuery parameters
| Parameter | Type | Description |
|---|---|---|
key | List<Object?> | Query identifier. Changes trigger refetch. |
fetcher | Future<T> Function() | Async function returning the data. |
options | QoraOptions? | Per-query caching/retry configuration. |
useQuery return value
Returns a QoraState<T>: a sealed class with four variants:
Initial, Loading, Success, Failure.
useMutation
Triggers a mutation and tracks its state.
class EditProfileScreen extends HookWidget {
@override
Widget build(BuildContext context) {
final mutation = useMutation<User, UpdateUserInput>(
mutator: (input) => Api.updateUser(input),
options: MutationOptions(
invalidates: [['users', currentUserId]],
),
);
return ElevatedButton(
onPressed: mutation.isPending
? null
: () => mutation.mutate(UpdateUserInput(name: 'Alice')),
child: mutation.isPending
? const CircularProgressIndicator()
: const Text('Save'),
);
}
}
useMutation parameters
| Parameter | Type | Description |
|---|---|---|
mutator | Future<TData> Function(TVariables) | Async function performing the mutation. |
options | MutationOptions? | Lifecycle callbacks and retry settings. |
useMutation return value
Returns a MutationHandle<TData, TVariables>:
| Property | Type | Description |
|---|---|---|
mutate | void Function(TVariables) | Fire-and-forget; errors go to state. |
mutateAsync | Future<TData?> Function(TVariables) | Returns the result or null on failure. |
reset | void Function() | Resets state to MutationIdle. |
isIdle | bool | true when no mutation has run yet. |
isPending | bool | true while the mutation is running. |
isSuccess | bool | true after successful completion. |
isError | bool | true after a failure. |
data | TData? | Available when isSuccess. |
error | Object? | Available when isError. |
useQueryClient
Returns the nearest QoraClient from the widget tree.
final client = useQueryClient();
client.invalidate(['posts']);
The widget must be wrapped in a QoraScope for this to work.
useIsFetching
Returns true while at least one query managed by the nearest QoraClient
has a network request in flight. Useful for a global loading indicator at the
top of the screen without coupling it to any individual query.
class AppLoadingBar extends HookWidget {
@override
Widget build(BuildContext context) {
final isFetching = useIsFetching();
return AnimatedOpacity(
opacity: isFetching ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: const LinearProgressIndicator(),
);
}
}
useIsFetching return value
bool: true when isFetchingCount > 0, false otherwise.
Rebuilds the widget only when the value crosses the zero boundary.
useIsMutating
Returns true while at least one mutation managed by the nearest QoraClient
is in MutationPending state. Handy for disabling a global submit button while
any write operation is in progress.
class SubmitButton extends HookWidget {
final VoidCallback onPressed;
const SubmitButton({super.key, required this.onPressed});
@override
Widget build(BuildContext context) {
final isMutating = useIsMutating();
return FilledButton(
onPressed: isMutating ? null : onPressed,
child: isMutating
? const SizedBox.square(
dimension: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Save'),
);
}
}
useIsMutating return value
bool: true when activeMutations.length > 0, false otherwise.
useInfiniteQuery
Manages paginated data: accumulates pages and exposes a
fetchNextPage function.
class PostsScreen extends HookWidget {
@override
Widget build(BuildContext context) {
final query = useInfiniteQuery<PostsPage, String?>(
key: const ['posts'],
fetcher: (cursor) => Api.getPosts(cursor: cursor),
getNextPageParam: (page) => page.nextCursor,
initialPageParam: null,
);
final posts = query.pages.expand((p) => p.posts).toList();
return ListView.builder(
itemCount: posts.length + (query.hasNextPage ? 1 : 0),
itemBuilder: (context, i) {
if (i == posts.length) {
if (!query.isFetchingNextPage) query.fetchNextPage();
return const Center(child: CircularProgressIndicator());
}
return PostTile(posts[i]);
},
);
}
}
useInfiniteQuery parameters
| Parameter | Type | Description |
|---|---|---|
key | List<Object?> | Query identifier. Changes trigger refetch. |
fetcher | Future<TData> Function(TPageParam) | Called with page param for each page. |
getNextPageParam | TPageParam? Function(TData) | Returns null to signal last page. |
initialPageParam | TPageParam | First page param passed to fetcher. |
useInfiniteQuery return value
Returns an InfiniteQueryHandle<TData, TPageParam>:
| Property | Type | Description |
|---|---|---|
pages | List<TData> | All fetched pages in order. |
isLoading | bool | true while the first page is loading. |
isFetchingNextPage | bool | true while a next page is loading. |
hasNextPage | bool | false when no more pages are available. |
fetchNextPage | Future<void> Function() | Loads next page. No-op if busy. |
error | Object? | Last error, or null. |
Combining hooks
Hooks compose naturally. The following example fetches a user, updates their profile, and displays an inline error in a single widget without setState or StreamBuilder.
class ProfileScreen extends HookWidget {
final String userId;
const ProfileScreen({super.key, required this.userId});
@override
Widget build(BuildContext context) {
final client = useQueryClient();
final userQuery = useQuery<User>(
key: ['users', userId],
fetcher: () => Api.getUser(userId),
);
final updateMutation = useMutation<User, UpdateUserInput>(
mutator: Api.updateUser,
options: MutationOptions(
invalidates: [['users', userId]],
),
);
return switch (userQuery) {
Loading() => const CircularProgressIndicator(),
Failure(:final error) => ErrorView(error),
Success(:final data) => ProfileForm(
user: data,
isSaving: updateMutation.isPending,
saveError: updateMutation.error,
onSave: (input) => updateMutation.mutate(input),
),
_ => const SizedBox.shrink(),
};
}
}
qora_hooks requires your widget to extend HookWidget (or use
HookConsumerWidget if you are combining with Riverpod). Standard
StatelessWidget and StatefulWidget are not supported: use
QoraBuilder and QoraMutationBuilder instead.