flutter_query_plus 0.0.6
flutter_query_plus: ^0.0.6 copied to clipboard
A powerful, React Query-inspired data fetching and server state management library for Flutter. Supports caching, retries, infinite queries, and mutations.
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_query_plus/flutter_query_plus.dart';
void main() {
runApp(
// 1. Initialize and Provide the QueryClient to the entire widget tree.
QueryProvider(
client: QueryClient(),
// 2. Add QueryDevtoolsOverlay to inspect queries during development
child: const QueryDevtoolsOverlay(
child: FlutterQueryDemoApp(),
),
),
);
}
class FlutterQueryDemoApp extends StatelessWidget {
const FlutterQueryDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Query Demo',
theme: ThemeData(
colorSchemeSeed: Colors.deepPurple,
useMaterial3: true,
),
home: const MainScreen(),
);
}
}
class MainScreen extends HookWidget {
const MainScreen({super.key});
@override
Widget build(BuildContext context) {
final selectedIndex = useState(0);
return Scaffold(
body: IndexedStack(
index: selectedIndex.value,
children: const [
BasicQueryView(),
InfiniteQueryView(),
],
),
bottomNavigationBar: NavigationBar(
selectedIndex: selectedIndex.value,
onDestinationSelected: (idx) => selectedIndex.value = idx,
destinations: const [
NavigationDestination(
icon: Icon(Icons.list),
label: 'Basic Query',
),
NavigationDestination(
icon: Icon(Icons.all_inclusive),
label: 'Infinite List',
),
],
),
);
}
}
// -----------------------------------------------------------------------------
// MOCK APIS
// -----------------------------------------------------------------------------
Future<List<String>> fetchProducts() async {
await Future.delayed(const Duration(seconds: 1)); // Simulate latency
return ['Mock Macbook', 'Mock iPhone', 'Mock AirPods', 'Mock iPad'];
}
Future<void> addProductApi(String product) async {
await Future.delayed(const Duration(seconds: 1)); // Simulate mutation
// In a real app we'd save it to the DB.
}
Future<List<String>> fetchPage(int? page) async {
await Future.delayed(const Duration(seconds: 1));
final p = page ?? 1; // Default to page 1
return List.generate(15, (i) => 'Item ${(p - 1) * 15 + i + 1}');
}
// -----------------------------------------------------------------------------
// BASIC QUERY & MUTATION EXAMPLES
// -----------------------------------------------------------------------------
class BasicQueryView extends HookWidget {
const BasicQueryView({super.key});
@override
Widget build(BuildContext context) {
// Access the queryClient to invalidate queries manually
final queryClient = useQueryClient();
final scaffoldMessenger = ScaffoldMessenger.of(context);
// useQuery: Automatically fetches and caches data
final productsQuery = useQuery<List<String>>(
key: 'products',
fetcher: fetchProducts,
);
// useMutation: Execute POST/PUT/DELETE networks requests securely
final addMutation = useMutation<void, Exception, String>(
(product) => addProductApi(product),
onSuccess: (_, __) {
// Automatically refetch the cached list when mutation succeeds
queryClient.invalidateQueries('products');
scaffoldMessenger.showSnackBar(
const SnackBar(content: Text('Product securely added!')),
);
},
);
return Scaffold(
appBar: AppBar(
title: const Text('Basic Query & Mutation'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Force Refetch',
onPressed: () => queryClient.invalidateQueries('products'),
),
],
),
body: Builder(
builder: (context) {
// Loading states are automatically handled
if (productsQuery.isLoading && !productsQuery.hasData) {
return const Center(child: CircularProgressIndicator());
}
// Error states
if (productsQuery.isError) {
return Center(
child: Text(
'Oops, an error occurred: ${productsQuery.error}',
style: const TextStyle(color: Colors.red),
),
);
}
final products = productsQuery.data ?? [];
return RefreshIndicator(
onRefresh: () async => queryClient.invalidateQueries('products'),
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 80),
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
leading: const CircleAvatar(child: Icon(Icons.inventory_2)),
title: Text(products[index]),
subtitle: const Text('In stock'),
);
},
),
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: addMutation.isLoading
? null // Disable while mutating
: () => addMutation.mutate('New Apple Device'),
icon: addMutation.isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.add),
label: const Text('Add Product'),
),
);
}
}
// -----------------------------------------------------------------------------
// INFINITE QUERY EXAMPLE
// -----------------------------------------------------------------------------
class InfiniteQueryView extends HookWidget {
const InfiniteQueryView({super.key});
@override
Widget build(BuildContext context) {
// useInfiniteQuery handles pagination automatically
final infiniteQuery = useInfiniteQuery<List<String>, int>(
key: 'infinite_items',
fetcher: fetchPage,
getNextPageParam: (lastPage, allPages) {
// In real apps, you'd check if lastPage has elements, or if
// a 'nextCursor' exists. Here, we limit to 5 pages.
if (allPages.length >= 5) return null;
return allPages.length + 1; // fetch next page
},
);
return Scaffold(
appBar: AppBar(title: const Text('Infinite queries')),
body: Builder(
builder: (context) {
if (infiniteQuery.isLoading && infiniteQuery.data == null) {
return const Center(child: CircularProgressIndicator());
}
if (infiniteQuery.isError) {
return Center(child: Text('Error: ${infiniteQuery.error}'));
}
final pages = infiniteQuery.data ?? [];
// Flatten standard paginated lists into generic items array
final allItems = pages.expand((page) => page).toList();
return ListView.builder(
itemCount: allItems.length + 1,
itemBuilder: (context, index) {
// Reached the end of the flat list
if (index == allItems.length) {
if (!infiniteQuery.hasNextPage) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text('No more items to load',
style: TextStyle(color: Colors.grey)),
),
);
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: ElevatedButton(
onPressed: infiniteQuery.isFetchingNextPage
? null
: infiniteQuery.fetchNextPage,
child: infiniteQuery.isFetchingNextPage
? const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Text('Load More'),
),
),
);
}
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(allItems[index]),
);
},
);
},
),
);
}
}